Merge pull request #165 from jazzband/jazzband-update
Jazzband updates for issue #164
23
.editorconfig
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# http://editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{py,rst,ini}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.{html,css,scss,json,yml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
311
.gitchangelog.rc
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
# -*- coding: utf-8; mode: python -*-
|
||||
#
|
||||
# Format
|
||||
#
|
||||
# ACTION: [AUDIENCE:] COMMIT_MSG [!TAG ...]
|
||||
#
|
||||
# Description
|
||||
#
|
||||
# ACTION is one of 'chg', 'fix', 'new'
|
||||
#
|
||||
# Is WHAT the change is about.
|
||||
#
|
||||
# 'chg' is for refactor, small improvement, cosmetic changes...
|
||||
# 'fix' is for bug fixes
|
||||
# 'new' is for new features, big improvement
|
||||
#
|
||||
# AUDIENCE is optional and one of 'dev', 'usr', 'pkg', 'test', 'doc'
|
||||
#
|
||||
# Is WHO is concerned by the change.
|
||||
#
|
||||
# 'dev' is for developpers (API changes, refactors...)
|
||||
# 'usr' is for final users (UI changes)
|
||||
# 'pkg' is for packagers (packaging changes)
|
||||
# 'test' is for testers (test only related changes)
|
||||
# 'doc' is for doc guys (doc only changes)
|
||||
#
|
||||
# COMMIT_MSG is ... well ... the commit message itself.
|
||||
#
|
||||
# TAGs are additionnal adjective as 'refactor' 'minor' 'cosmetic'
|
||||
#
|
||||
# They are preceded with a '!' or a '@' (prefer the former, as the
|
||||
# latter is wrongly interpreted in github.) Commonly used tags are:
|
||||
#
|
||||
# 'refactor' is obviously for refactoring code only
|
||||
# 'minor' is for a very meaningless change (a typo, adding a comment)
|
||||
# 'cosmetic' is for cosmetic driven change (re-indentation, 80-col...)
|
||||
# 'wip' is for partial functionality but complete subfunctionality.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# new: usr: support of bazaar implemented
|
||||
# chg: re-indentend some lines !cosmetic
|
||||
# new: dev: updated code to be compatible with last version of killer lib.
|
||||
# fix: pkg: updated year of licence coverage.
|
||||
# new: test: added a bunch of test around user usability of feature X.
|
||||
# fix: typo in spelling my name in comment. !minor
|
||||
#
|
||||
# Please note that multi-line commit message are supported, and only the
|
||||
# first line will be considered as the "summary" of the commit message. So
|
||||
# tags, and other rules only applies to the summary. The body of the commit
|
||||
# message will be displayed in the changelog without reformatting.
|
||||
|
||||
|
||||
#
|
||||
# ``ignore_regexps`` is a line of regexps
|
||||
#
|
||||
# Any commit having its full commit message matching any regexp listed here
|
||||
# will be ignored and won't be reported in the changelog.
|
||||
#
|
||||
ignore_regexps = [
|
||||
r'@minor', r'!minor',
|
||||
r'@cosmetic', r'!cosmetic',
|
||||
r'@refactor', r'!refactor',
|
||||
r'@wip', r'!wip',
|
||||
r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[p|P]kg:',
|
||||
r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[d|D]ev:',
|
||||
r'^(.{3,3}\s*:)?\s*[fF]irst commit.?\s*$',
|
||||
r'^Initial Commit.$',
|
||||
r'^Version updated.+$',
|
||||
r'^$', # ignore commits with empty messages
|
||||
r'^Merge branch .+',
|
||||
r'^Merge pull .+',
|
||||
]
|
||||
|
||||
|
||||
# ``section_regexps`` is a list of 2-tuples associating a string label and a
|
||||
# list of regexp
|
||||
#
|
||||
# Commit messages will be classified in sections thanks to this. Section
|
||||
# titles are the label, and a commit is classified under this section if any
|
||||
# of the regexps associated is matching.
|
||||
#
|
||||
# Please note that ``section_regexps`` will only classify commits and won't
|
||||
# make any changes to the contents. So you'll probably want to go check
|
||||
# ``subject_process`` (or ``body_process``) to do some changes to the subject,
|
||||
# whenever you are tweaking this variable.
|
||||
#
|
||||
section_regexps = [
|
||||
('New', [
|
||||
r'^\[?[nN][eE][wW]\]?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
|
||||
r'^\[?[aA][dD][dD]\]?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
|
||||
]),
|
||||
('Updates', [
|
||||
r'^\[?[uU][pP][dD][aA][tT][eE]\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
|
||||
r'^\[?[cC][hH][aA][nN][gG][eE][dD]?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
|
||||
]),
|
||||
('Fix', [
|
||||
r'^\[?[fF][iI][xX]\]?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
|
||||
]),
|
||||
|
||||
('Other', None # Match all lines
|
||||
),
|
||||
|
||||
]
|
||||
|
||||
|
||||
# ``body_process`` is a callable
|
||||
#
|
||||
# This callable will be given the original body and result will
|
||||
# be used in the changelog.
|
||||
#
|
||||
# Available constructs are:
|
||||
#
|
||||
# - any python callable that take one txt argument and return txt argument.
|
||||
#
|
||||
# - ReSub(pattern, replacement): will apply regexp substitution.
|
||||
#
|
||||
# - Indent(chars=" "): will indent the text with the prefix
|
||||
# Please remember that template engines gets also to modify the text and
|
||||
# will usually indent themselves the text if needed.
|
||||
#
|
||||
# - Wrap(regexp=r"\n\n"): re-wrap text in separate paragraph to fill 80-Columns
|
||||
#
|
||||
# - noop: do nothing
|
||||
#
|
||||
# - ucfirst: ensure the first letter is uppercase.
|
||||
# (usually used in the ``subject_process`` pipeline)
|
||||
#
|
||||
# - final_dot: ensure text finishes with a dot
|
||||
# (usually used in the ``subject_process`` pipeline)
|
||||
#
|
||||
# - strip: remove any spaces before or after the content of the string
|
||||
#
|
||||
# - SetIfEmpty(msg="No commit message."): will set the text to
|
||||
# whatever given ``msg`` if the current text is empty.
|
||||
#
|
||||
# Additionally, you can `pipe` the provided filters, for instance:
|
||||
# body_process = Wrap(regexp=r'\n(?=\w+\s*:)') | Indent(chars=" ")
|
||||
# body_process = Wrap(regexp=r'\n(?=\w+\s*:)')
|
||||
# body_process = noop
|
||||
body_process = ReSub(r'((^|\n)[A-Z]\w+(-\w+)*: .*(\n\s+.*)*)+$', r'') | strip
|
||||
|
||||
|
||||
# ``subject_process`` is a callable
|
||||
#
|
||||
# This callable will be given the original subject and result will
|
||||
# be used in the changelog.
|
||||
#
|
||||
# Available constructs are those listed in ``body_process`` doc.
|
||||
subject_process = (strip |
|
||||
ReSub(r'^(\[\w+\])\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n@]*)(@[a-z]+\s+)*$', r'\4') |
|
||||
SetIfEmpty("No commit message.") | ucfirst | final_dot)
|
||||
|
||||
|
||||
# ``tag_filter_regexp`` is a regexp
|
||||
#
|
||||
# Tags that will be used for the changelog must match this regexp.
|
||||
#
|
||||
tag_filter_regexp = r'^[0-9]+\.[0-9]+(\.[0-9]+)?$'
|
||||
|
||||
|
||||
# ``unreleased_version_label`` is a string or a callable that outputs a string
|
||||
#
|
||||
# This label will be used as the changelog Title of the last set of changes
|
||||
# between last valid tag and HEAD if any.
|
||||
def unreleased_date():
|
||||
import datetime
|
||||
return 'Unreleased ({})'.format(datetime.datetime.now().strftime('%Y-%m-%d'))
|
||||
unreleased_version_label = unreleased_date
|
||||
|
||||
|
||||
# ``output_engine`` is a callable
|
||||
#
|
||||
# This will change the output format of the generated changelog file
|
||||
#
|
||||
# Available choices are:
|
||||
#
|
||||
# - rest_py
|
||||
#
|
||||
# Legacy pure python engine, outputs ReSTructured text.
|
||||
# This is the default.
|
||||
#
|
||||
# - mustache(<template_name>)
|
||||
#
|
||||
# Template name could be any of the available templates in
|
||||
# ``templates/mustache/*.tpl``.
|
||||
# Requires python package ``pystache``.
|
||||
# Examples:
|
||||
# - mustache("markdown")
|
||||
# - mustache("restructuredtext")
|
||||
#
|
||||
# - makotemplate(<template_name>)
|
||||
#
|
||||
# Template name could be any of the available templates in
|
||||
# ``templates/mako/*.tpl``.
|
||||
# Requires python package ``mako``.
|
||||
# Examples:
|
||||
# - makotemplate("restructuredtext")
|
||||
#
|
||||
# output_engine = rest_py
|
||||
# output_engine = mustache("restructuredtext")
|
||||
output_engine = mustache("markdown")
|
||||
# output_engine = makotemplate("restructuredtext")
|
||||
|
||||
|
||||
# ``include_merge`` is a boolean
|
||||
#
|
||||
# This option tells git-log whether to include merge commits in the log.
|
||||
# The default is to include them.
|
||||
include_merge = True
|
||||
|
||||
|
||||
# ``log_encoding`` is a string identifier
|
||||
#
|
||||
# This option tells gitchangelog what encoding is outputed by ``git log``.
|
||||
# The default is to be clever about it: it checks ``git config`` for
|
||||
# ``i18n.logOutputEncoding``, and if not found will default to git's own
|
||||
# default: ``utf-8``.
|
||||
log_encoding = 'utf-8'
|
||||
|
||||
OUTPUT_FILE = "CHANGELOG.md"
|
||||
INSERT_POINT_REGEX = r'''(?isxu)
|
||||
^
|
||||
(
|
||||
\s*\#\s+Changelog\s*(\n|\r\n|\r) ## ``Changelog`` line
|
||||
)
|
||||
|
||||
( ## Match all between changelog and release rev
|
||||
(
|
||||
(?!
|
||||
(?<=(\n|\r)) ## look back for newline
|
||||
\#\#\s+%(rev)s ## revision
|
||||
\s+
|
||||
\([0-9]+-[0-9]{2}-[0-9]{2}\)(\n|\r\n|\r) ## date
|
||||
)
|
||||
.
|
||||
)*
|
||||
)
|
||||
|
||||
(?P<tail>\#\#\s+(?P<rev>%(rev)s))
|
||||
''' % {'rev': r"[0-9]+\.[0-9]+(\.[0-9]+)?"}
|
||||
|
||||
|
||||
# ``publish`` is a callable
|
||||
#
|
||||
# Sets what ``gitchangelog`` should do with the output generated by
|
||||
# the output engine. ``publish`` is a callable taking one argument
|
||||
# that is an interator on lines from the output engine.
|
||||
#
|
||||
# Some helper callable are provided:
|
||||
#
|
||||
# Available choices are:
|
||||
#
|
||||
# - stdout
|
||||
#
|
||||
# Outputs directly to standard output
|
||||
# (This is the default)
|
||||
#
|
||||
# - FileInsertAtFirstRegexMatch(file, pattern, idx=lamda m: m.start())
|
||||
#
|
||||
# Creates a callable that will parse given file for the given
|
||||
# regex pattern and will insert the output in the file.
|
||||
# ``idx`` is a callable that receive the matching object and
|
||||
# must return a integer index point where to insert the
|
||||
# the output in the file. Default is to return the position of
|
||||
# the start of the matched string.
|
||||
#
|
||||
# - FileRegexSubst(file, pattern, replace, flags)
|
||||
#
|
||||
# Apply a replace inplace in the given file. Your regex pattern must
|
||||
# take care of everything and might be more complex. Check the README
|
||||
# for a complete copy-pastable example.
|
||||
#
|
||||
publish = FileRegexSubst(OUTPUT_FILE, INSERT_POINT_REGEX, r"\1\o\n\g<tail>")
|
||||
|
||||
|
||||
# ``revs`` is a list of callable or a list of string
|
||||
#
|
||||
# callable will be called to resolve as strings and allow dynamical
|
||||
# computation of these. The result will be used as revisions for
|
||||
# gitchangelog (as if directly stated on the command line). This allows
|
||||
# to filter exaclty which commits will be read by gitchangelog.
|
||||
#
|
||||
# To get a full documentation on the format of these strings, please
|
||||
# refer to the ``git rev-list`` arguments. There are many examples.
|
||||
#
|
||||
# Using callables is especially useful, for instance, if you
|
||||
# are using gitchangelog to generate incrementally your changelog.
|
||||
#
|
||||
# Some helpers are provided, you can use them::
|
||||
#
|
||||
# - FileFirstRegexMatch(file, pattern): will return a callable that will
|
||||
# return the first string match for the given pattern in the given file.
|
||||
# If you use named sub-patterns in your regex pattern, it'll output only
|
||||
# the string matching the regex pattern named "rev".
|
||||
#
|
||||
# - Caret(rev): will return the rev prefixed by a "^", which is a
|
||||
# way to remove the given revision and all its ancestor.
|
||||
#
|
||||
# Please note that if you provide a rev-list on the command line, it'll
|
||||
# replace this value (which will then be ignored).
|
||||
#
|
||||
# If empty, then ``gitchangelog`` will act as it had to generate a full
|
||||
# changelog.
|
||||
#
|
||||
# The default is to use all commits to make the changelog.
|
||||
# revs = ["^1.0.3", ]
|
||||
revs = [
|
||||
Caret(FileFirstRegexMatch(OUTPUT_FILE, INSERT_POINT_REGEX)),
|
||||
"HEAD"
|
||||
]
|
||||
41
.github/workflows/publish-docs.yml
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
name: Build and publish documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: [ "*" ]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.9]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements/dev.txt
|
||||
|
||||
- name: Build documentation
|
||||
run: |
|
||||
make docs
|
||||
|
||||
- name: github pages deploy
|
||||
uses: peaceiris/actions-gh-pages@v3.8.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_branch: gh-pages
|
||||
publish_dir: docs
|
||||
force_orphan: true
|
||||
48
.github/workflows/publish-package.yml
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
name: Release package on PyPI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: [ "*" ]
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build package and publish to TestPyPI and PyPI
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/setup-python@v2
|
||||
name: Use Python 3.9
|
||||
with:
|
||||
python-version: 3.9
|
||||
|
||||
- name: Install pypa/build
|
||||
run: >-
|
||||
python -m
|
||||
pip install
|
||||
build
|
||||
--user
|
||||
|
||||
- name: Build a binary wheel and a source tarball
|
||||
run: >-
|
||||
python -m
|
||||
build
|
||||
--sdist
|
||||
--wheel
|
||||
--outdir dist/
|
||||
|
||||
- name: Publish package to Test PyPI
|
||||
uses: pypa/gh-action-pypi-publish@master
|
||||
with:
|
||||
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
|
||||
repository_url: https://test.pypi.org/legacy/
|
||||
|
||||
- name: Publish package
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||
uses: pypa/gh-action-pypi-publish@master
|
||||
with:
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
39
.github/workflows/python-package.yml
vendored
|
|
@ -1,39 +0,0 @@
|
|||
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
||||
|
||||
name: Python package
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.9]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install flake8 pytest
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
- name: Sphinx Pages
|
||||
# You may pin to the exact commit or the version.
|
||||
# uses: seanzhengw/sphinx-pages@70dd0557fc226cfcd41c617aec5e9ee4fce4afe2
|
||||
uses: seanzhengw/sphinx-pages@d29427677b3b89c1b5311d9eb135fb4168f4ba4a
|
||||
with:
|
||||
# Token for the repo. Can be passed in using $\{{ secrets.GITHUB_TOKEN }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Auto create a README.md file at branch gh-pages root with repo/branch/commit links
|
||||
source_dir: "doc_src"
|
||||
47
.github/workflows/run-tests.yaml
vendored
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
name: Run Tox tests
|
||||
on:
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [ 3.6, 3.7, 3.8, 3.9 ]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
|
||||
- name: Get pip cache dir
|
||||
id: pip-cache
|
||||
run: |
|
||||
echo "::set-output name=dir::$(pip cache dir)"
|
||||
|
||||
- name: Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.pip-cache.outputs.dir }}
|
||||
key:
|
||||
${{ matrix.python-version }}-v1-${{ hashFiles('**/requirements.txt') }}-${{ hashFiles('**/tox.ini') }}
|
||||
restore-keys: |
|
||||
${{ matrix.python-version }}-v1-
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install tox tox-gh-actions
|
||||
|
||||
- name: Test with tox
|
||||
run: tox
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
name: Python ${{ matrix.python-version }}
|
||||
193
.gitignore
vendored
|
|
@ -1,20 +1,181 @@
|
|||
*.pyc
|
||||
dev.db
|
||||
local_settings.py
|
||||
media/ugc
|
||||
### macOS ###
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
### Python ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
src/
|
||||
pip-log.txt
|
||||
media/js/*.r*.js
|
||||
media/css/*.r*.css
|
||||
*DS_Store
|
||||
*.egg-info
|
||||
doc_src/_build
|
||||
MANIFEST
|
||||
develop-eggs/
|
||||
dist/
|
||||
.venv
|
||||
example/media/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
htmlcov
|
||||
.nox/
|
||||
.coverage
|
||||
.python-version
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
dev.db
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
*/.ipynb_checkpoints/*
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# Mr Developer
|
||||
.mr.developer.cfg
|
||||
.project
|
||||
.pydevproject
|
||||
|
||||
# Pycharm/Intellij
|
||||
.idea
|
||||
|
||||
# VSCode
|
||||
.vscode
|
||||
|
||||
# Docker
|
||||
docker-compose*
|
||||
**/docker-compose*
|
||||
|
||||
# Complexity
|
||||
output/*.html
|
||||
output/*/index.html
|
||||
|
||||
# Testing artifacts
|
||||
junit-*.xml
|
||||
flake8-errors.txt
|
||||
example/media/
|
||||
|
||||
# Documentation building
|
||||
_build
|
||||
doc_src/api/categories*.rst
|
||||
docs
|
||||
|
||||
RELEASE.txt
|
||||
site-packages
|
||||
reports
|
||||
test-reports/*
|
||||
|
|
|
|||
53
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
repos:
|
||||
- repo: https://github.com/timothycrosley/isort
|
||||
rev: 5.10.1
|
||||
hooks:
|
||||
- id: isort
|
||||
additional_dependencies: [toml]
|
||||
- repo: https://github.com/python/black
|
||||
rev: 21.11b1
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.0.1
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: check-case-conflict
|
||||
- id: check-executables-have-shebangs
|
||||
- id: check-json
|
||||
- id: check-merge-conflict
|
||||
- id: check-shebang-scripts-are-executable
|
||||
- id: check-symlinks
|
||||
- id: check-toml
|
||||
- id: check-yaml
|
||||
- id: debug-statements
|
||||
- id: end-of-file-fixer
|
||||
exclude: "^tests/resources/"
|
||||
- id: fix-byte-order-marker
|
||||
- id: fix-encoding-pragma
|
||||
args: ["--remove"]
|
||||
- id: requirements-txt-fixer
|
||||
- repo: https://gitlab.com/pycqa/flake8
|
||||
rev: 3.9.2
|
||||
hooks:
|
||||
- id: flake8
|
||||
- repo: https://github.com/pycqa/pydocstyle
|
||||
rev: 6.1.1
|
||||
hooks:
|
||||
- id: pydocstyle
|
||||
exclude: test.*|custom_.*|\d\d\d\d_.*
|
||||
additional_dependencies: [toml]
|
||||
- repo: https://github.com/terrencepreilly/darglint
|
||||
rev: v1.8.1
|
||||
hooks:
|
||||
- id: darglint
|
||||
args:
|
||||
- -v 2
|
||||
- "--message-template={path}:{line} in `{obj}`:\n {msg_id}: {msg}"
|
||||
- --strictness=short
|
||||
exclude: test.*|custom_.*|\d\d\d\d_.*
|
||||
|
||||
- repo: https://github.com/econchick/interrogate
|
||||
rev: 1.5.0 # or master if you're bold
|
||||
hooks:
|
||||
- id: interrogate
|
||||
12
.travis.yml
|
|
@ -1,12 +0,0 @@
|
|||
sudo: false
|
||||
|
||||
dist: xenial
|
||||
|
||||
language: python
|
||||
|
||||
python:
|
||||
- "3.6"
|
||||
|
||||
install: pip install tox-travis
|
||||
|
||||
script: tox
|
||||
1228
CHANGELOG.md
Normal file
3
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[](https://jazzband.co/)
|
||||
|
||||
This is a [Jazzband](https://jazzband.co/) project. By contributing you agree to abide by the [Contributor Code of Conduct](https://jazzband.co/about/conduct) and follow the [guidelines](https://jazzband.co/about/guidelines).
|
||||
|
|
@ -199,4 +199,3 @@
|
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
|
|
|||
|
|
@ -9,10 +9,11 @@ recursive-include categories/static *.html *.gif *.png *.css *.txt *.js
|
|||
recursive-include categories/editor *.html *.gif *.png *.css *.js
|
||||
|
||||
recursive-include doc_src *.rst *.txt *.png *.css *.html *.js
|
||||
recursive-exclude doc_src/_build *.*
|
||||
include doc_src/Makefile
|
||||
include doc_src/make.bat
|
||||
|
||||
recursive-include categories/locale *.mo *.po
|
||||
recursive-include categories/editor/locale *.mo *.po
|
||||
|
||||
recursive-include requirements *.txt
|
||||
prune example
|
||||
|
|
|
|||
98
Makefile
|
|
@ -1,20 +1,96 @@
|
|||
help:
|
||||
@echo ""
|
||||
@echo "Available make commands:"
|
||||
@echo ""
|
||||
@echo "deps Install development dependencies"
|
||||
@echo "test Run tests"
|
||||
@echo "publish Publish a release to PyPi (requires permissions)"
|
||||
@echo ""
|
||||
.PHONY: clean clean-test clean-pyc clean-build docs help
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
deps:
|
||||
RELEASE_KIND := patch
|
||||
SOURCE_DIR := categories
|
||||
|
||||
BRANCH_NAME := $(shell echo $$(git rev-parse --abbrev-ref HEAD))
|
||||
SHORT_BRANCH_NAME := $(shell echo $(BRANCH_NAME) | cut -c 1-20)
|
||||
PRIMARY_BRANCH_NAME := master
|
||||
|
||||
EDIT_CHANGELOG_IF_EDITOR_SET := @bash -c "$(shell if [[ -n $$EDITOR ]] ; then echo "$$EDITOR CHANGELOG.md" ; else echo "" ; fi)"
|
||||
|
||||
help:
|
||||
@grep '^[a-zA-Z]' $(MAKEFILE_LIST) | sort | awk -F ':.*?## ' 'NF==2 {printf "\033[36m %-25s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts
|
||||
|
||||
clean-build: ## remove build artifacts
|
||||
rm -fr build/
|
||||
rm -fr dist/
|
||||
rm -fr .eggs/
|
||||
find . -name '*.egg-info' -exec rm -fr {} +
|
||||
find . -name '*.egg' -exec rm -f {} +
|
||||
|
||||
clean-pyc: ## remove Python file artifacts
|
||||
find . -name '*.pyc' -exec rm -f {} +
|
||||
find . -name '*.pyo' -exec rm -f {} +
|
||||
find . -name '*~' -exec rm -f {} +
|
||||
find . -name '__pycache__' -exec rm -fr {} +
|
||||
|
||||
clean-test: ## remove test and coverage artifacts
|
||||
rm -fr .tox/
|
||||
rm -f .coverage
|
||||
rm -fr htmlcov/
|
||||
rm -fr .pytest_cache
|
||||
|
||||
deps: ## Install development dependencies
|
||||
pip install -r requirements.txt
|
||||
pip install tox sphinx sphinx-autobuild twine
|
||||
|
||||
test:
|
||||
test: ## Run tests
|
||||
tox
|
||||
|
||||
publish:
|
||||
publish: ## Publish a release to PyPi (requires permissions)
|
||||
rm -fr build dist
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload dist/*
|
||||
|
||||
release-helper:
|
||||
## DO NOT CALL DIRECTLY. It is used by release-{patch,major,minor,dev}
|
||||
@echo "Branch In Use: $(BRANCH_NAME) $(SHORT_BRANCH_NAME)"
|
||||
ifeq ($(BRANCH_NAME), $(PRIMARY_BRANCH_NAME))
|
||||
ifeq ($(RELEASE_KIND), dev)
|
||||
@echo "Error! Can't bump $(RELEASE_KIND) while on the $(PRIMARY_BRANCH_NAME) branch."
|
||||
exit
|
||||
else ifneq ($(RELEASE_KIND), dev)
|
||||
@echo "Error! Must be on the $(PRIMARY_BRANCH_NAME) branch to bump $(RELEASE_KIND)."
|
||||
exit
|
||||
endif
|
||||
|
||||
git fetch -p --all
|
||||
gitchangelog
|
||||
$(EDIT_CHANGELOG_IF_EDITOR_SET)
|
||||
export BRANCH_NAME=$(SHORT_BRANCH_NAME);bumpversion $(RELEASE_KIND) --allow-dirty --tag
|
||||
git push origin $(BRANCH_NAME)
|
||||
git push --tags
|
||||
|
||||
set-release-major-env-var:
|
||||
$(eval RELEASE_KIND := major)
|
||||
|
||||
set-release-minor-env-var:
|
||||
$(eval RELEASE_KIND := minor)
|
||||
|
||||
set-release-patch-env-var:
|
||||
$(eval RELEASE_KIND := patch)
|
||||
|
||||
set-release-dev-env-var:
|
||||
$(eval RELEASE_KIND := dev)
|
||||
|
||||
release-dev: set-release-dev-env-var release-helper ## Release a new development version: 1.1.1 -> 1.1.1+branchname-0
|
||||
|
||||
release-patch: set-release-patch-env-var release-helper ## Release a new patch version: 1.1.1 -> 1.1.2
|
||||
|
||||
release-minor: set-release-minor-env-var release-helper ## Release a new minor version: 1.1.1 -> 1.2.0
|
||||
|
||||
release-major: set-release-major-env-var release-helper ## release a new major version: 1.1.1 -> 2.0.0
|
||||
|
||||
docs: ## generate Sphinx HTML documentation, including API docs
|
||||
mkdir -p docs
|
||||
rm -f doc_src/api/$(SOURCE_DIR)*.rst
|
||||
ls -A1 docs | xargs -I {} rm -rf docs/{}
|
||||
$(MAKE) -C doc_src clean html
|
||||
cp -a doc_src/_build/html/. docs
|
||||
|
||||
pubdocs: docs ## Publish the documentation to GitHub
|
||||
ghp-import -op docs
|
||||
|
|
|
|||
178
README.md
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
# Django Categories
|
||||
|
||||
[](https://jazzband.co/)
|
||||
[](https://codecov.io/gh/jazzband/django-categories)
|
||||
|
||||
Django Categories grew out of our need to provide a basic hierarchical taxonomy management system that multiple applications could use independently or in concert.
|
||||
|
||||
As a news site, our stories, photos, and other content get divided into "sections" and we wanted all the apps to use the same set of sections. As our needs grew, the Django Categories grew in the functionality it gave to category handling within web pages.
|
||||
|
||||
## Features of the project
|
||||
|
||||
Currently incompatible with Python 3.10.
|
||||
|
||||
**Multiple trees, or a single tree.**
|
||||
You can treat all the records as a single tree, shared by all the applications. You can also treat each of the top level records as individual trees, for different apps or uses.
|
||||
|
||||
**Easy handling of hierarchical data.**
|
||||
We use [Django MPTT](http://pypi.python.org/pypi/django-mptt) to manage the data efficiently and provide the extra access functions.
|
||||
|
||||
**Easy importation of data.**
|
||||
Import a tree or trees of space- or tab-indented data with a Django management command.
|
||||
|
||||
**Metadata for better SEO on web pages**
|
||||
Include all the metadata you want for easy inclusion on web pages.
|
||||
|
||||
**Link uncategorized objects to a category**
|
||||
Attach any number of objects to a category, even if the objects themselves aren't categorized.
|
||||
|
||||
**Hierarchical Admin**
|
||||
Shows the data in typical tree form with disclosure triangles
|
||||
|
||||
**Template Helpers**
|
||||
Easy ways for displaying the tree data in templates:
|
||||
|
||||
- **Show one level of a tree.** All root categories or just children of a specified category
|
||||
|
||||
- **Show multiple levels.** Ancestors of category, category and all children of category or a category and its children
|
||||
|
||||
## Changes
|
||||
|
||||
### New in 1.8
|
||||
|
||||
* Support for Django 3.1
|
||||
* Removed support for Python 2.7
|
||||
|
||||
### New in 1.7
|
||||
|
||||
* Support for Django 3
|
||||
|
||||
### New in 1.6
|
||||
|
||||
* Support for Django 2
|
||||
|
||||
### New in 1.5
|
||||
|
||||
* Support for Django 1.10
|
||||
|
||||
### New in 1.4
|
||||
|
||||
* Support for Python 2.7, 3.4 and 3.5
|
||||
* Support for Django 1.9
|
||||
* Dropped support for Django 1.7 and older
|
||||
|
||||
### New in 1.3
|
||||
|
||||
* Support for Django 1.6, 1.7 and 1.8
|
||||
* Dropped support for Django versions 1.5 and below
|
||||
|
||||
### New in 1.2
|
||||
|
||||
* Support for Django 1.5
|
||||
* Dropped support for Django 1.2
|
||||
* Dropped caching within the app
|
||||
* Removed the old settings compatibility layer. *Must use new dictionary-based settings!*
|
||||
|
||||
|
||||
### New in 1.1
|
||||
|
||||
* Fixed a cosmetic bug in the Django 1.4 admin. Action checkboxes now only appear once.
|
||||
|
||||
* Template tags are refactored to allow easy use of any model derived from ``CategoryBase``.
|
||||
|
||||
* Improved test suite.
|
||||
|
||||
* Improved some of the documentation.
|
||||
|
||||
|
||||
### Upgrade path from 1.0.2 to 1.0.3
|
||||
|
||||
Due to some data corruption with 1.0.2 migrations, a partially new set of migrations has been written in 1.0.3; and this will cause issues for users on 1.0.2 version. There is also an issue with South version 0.7.4. South version 0.7.3 or 0.7.5 or greater works fine.
|
||||
|
||||
For a clean upgrade from 1.0.2 to 1.0.3 you have to delete previous version of 0010 migration (named 0010_changed_category_relation.py) and fakes the new 00010, 0011 and 0012.
|
||||
|
||||
Therefore after installing new version of django-categories, for each project to upgrade you should execute the following commans in order
|
||||
|
||||
python manage.py migrate categories 0010_add_field_categoryrelation_category --fake --delete-ghost-migrations
|
||||
python manage.py migrate categories 0011_move_category_fks --fake
|
||||
python manage.py migrate categories 0012_remove_story_field --fake
|
||||
python manage.py migrate categories 0013_null_category_id
|
||||
|
||||
This way both the exact database layout and migration history is restored between the two installation paths (new installation from 1.0.3 and upgrade from 1.0.2 to 1.0.3).
|
||||
|
||||
Last migration is needed to set the correct null value for `category_id` field when upgrading from 1.0.2 while is a noop for 1.0.3.
|
||||
|
||||
### New in 1.0
|
||||
|
||||
**Abstract Base Class for generic hierarchical category models.**
|
||||
When you want a multiple types of categories and don't want them all part of the same model, you can now easily create new models by subclassing `CategoryBase`. You can also add additional metadata as necessary.
|
||||
|
||||
Your model's can subclass `CategoryBaseAdminForm` and `CategoryBaseAdmin` to get the hierarchical management in the admin.
|
||||
|
||||
See the docs for more information.
|
||||
|
||||
**Increased the default caching time on views.**
|
||||
The default setting for `CACHE_VIEW_LENGTH` was `0`, which means it would tell the browser to *never* cache the page. It is now `600`, which is the default for [CACHE_MIDDLEWARE_SECONDS](https://docs.djangoproject.com/en/1.3/ref/settings/#cache-middleware-seconds)
|
||||
|
||||
**Updated for use with Django-MPTT 0.5.**
|
||||
Just a few tweaks.
|
||||
|
||||
**Initial compatibility with Django 1.4.**
|
||||
More is coming, but at least it works.
|
||||
|
||||
**Slug transliteration for non-ASCII characters.**
|
||||
A new setting, ``SLUG_TRANSLITERATOR``, allows you to specify a function for converting the non-ASCII characters to ASCII characters before the slugification. Works great with [Unidecode](http://pypi.python.org/pypi/Unidecode).
|
||||
|
||||
### Updated in 0.8.8
|
||||
|
||||
The `editor` app was placed inside the categories app, `categories.editor`, to avoid any name clashes.
|
||||
|
||||
#### Upgrading
|
||||
|
||||
A setting change is all that is needed:
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'categories',
|
||||
'categories.editor',
|
||||
)
|
||||
|
||||
### New in 0.8
|
||||
|
||||
**Added an active field.** As an alternative to deleting categories, you can make them inactive.
|
||||
|
||||
Also added a manager method ``active()`` to query only the active categories and added Admin Actions to activate or deactivate an item.
|
||||
|
||||
**Improved import.** Previously the import saved items in the reverse order to the imported file. Now them import in order.
|
||||
|
||||
### New in 0.7
|
||||
|
||||
**Added South migrations.** All the previous SQL scripts have been converted to South migrations.
|
||||
|
||||
**Can add category fields via management command (and South).**
|
||||
The new ability to setup category relationships in ``settings.py`` works fine if you are starting from scratch, but not if you want to add it after you have set up the database. Now there is a management command to make sure all the correct fields and tables are created.
|
||||
|
||||
**Added an alternate_url field.**
|
||||
This allows the specification of a URL that is not derived from the category hierarchy.
|
||||
|
||||
**New JAVASCRIPT_URL setting.**
|
||||
This allows some customization of the `genericcollections.js` file.
|
||||
|
||||
**New get_latest_objects_by_category template tag.** This will do pretty much what it says.
|
||||
|
||||
|
||||
### New in 0.6
|
||||
|
||||
**Class-based views.**
|
||||
Works great with Django 1.3 or [django-cbv](http://pypi.python.org/pypi/django-cbv)
|
||||
|
||||
**New Settings infrastructure.**
|
||||
To be more like the Django project, we are migrating from individual `CATEGORIES_*` settings to a dictionary named `CATEGORIES_SETTINGS`. Use of the previous settings will still work but will generate a `DeprecationError`.
|
||||
|
||||
**The tree's initially expanded state is now configurable.**
|
||||
`EDITOR_TREE_INITIAL_STATE` allows a `collapsed` or `expanded` value. The default is `collapsed`.
|
||||
|
||||
**Optional Thumbnail field.**
|
||||
Have a thumbnail for each category!
|
||||
|
||||
**"Categorize" models in settings.**
|
||||
Now you don't have to modify the model to add a `Category` relationship. Use the new settings to "wire" categories to different models.
|
||||
203
README.rst
|
|
@ -1,203 +0,0 @@
|
|||
=================
|
||||
Django Categories
|
||||
=================
|
||||
|
||||
|BUILD|_
|
||||
|
||||
.. |BUILD| image::
|
||||
https://travis-ci.org/callowayproject/django-categories.svg?branch=master
|
||||
.. _BUILD: https://travis-ci.org/callowayproject/django-categories
|
||||
|
||||
|
||||
Django Categories grew out of our need to provide a basic hierarchical taxonomy management system that multiple applications could use independently or in concert.
|
||||
|
||||
As a news site, our stories, photos, and other content get divided into "sections" and we wanted all the apps to use the same set of sections. As our needs grew, the Django Categories grew in the functionality it gave to category handling within web pages.
|
||||
|
||||
New in 1.8
|
||||
==========
|
||||
|
||||
* Support for Django 3.1
|
||||
* Removed support for Python 2.7
|
||||
|
||||
New in 1.7
|
||||
==========
|
||||
|
||||
* Support for Django 3
|
||||
|
||||
New in 1.6
|
||||
==========
|
||||
|
||||
* Support for Django 2
|
||||
|
||||
New in 1.5
|
||||
==========
|
||||
|
||||
* Support for Django 1.10
|
||||
|
||||
New in 1.4
|
||||
==========
|
||||
|
||||
* Support for Python 2.7, 3.4 and 3.5
|
||||
* Support for Django 1.9
|
||||
* Dropped support for Django 1.7 and older
|
||||
|
||||
New in 1.3
|
||||
==========
|
||||
|
||||
* Support for Django 1.6, 1.7 and 1.8
|
||||
* Dropped support for Django versions 1.5 and below
|
||||
|
||||
New in 1.2
|
||||
==========
|
||||
|
||||
* Support for Django 1.5
|
||||
* Dropped support for Django 1.2
|
||||
* Dropped caching within the app
|
||||
* Removed the old settings compatibility layer. *Must use new dictionary-based settings!*
|
||||
|
||||
|
||||
|
||||
New in 1.1
|
||||
==========
|
||||
|
||||
* Fixed a cosmetic bug in the Django 1.4 admin. Action checkboxes now only appear once.
|
||||
|
||||
* Template tags are refactored to allow easy use of any model derived from ``CategoryBase``.
|
||||
|
||||
* Improved test suite.
|
||||
|
||||
* Improved some of the documentation.
|
||||
|
||||
|
||||
Upgrade path from 1.0.2 to 1.0.3
|
||||
================================
|
||||
|
||||
Due to some data corruption with 1.0.2 migrations, a partially new set of migrations has been written in 1.0.3; and this will cause issues for users on 1.0.2 version. There is also an issue with South version 0.7.4. South version 0.7.3 or 0.7.5 or greater works fine.
|
||||
|
||||
For a clean upgrade from 1.0.2 to 1.0.3 you have to delete previous version of 0010 migration (named 0010_changed_category_relation.py) and fakes the new 00010, 0011 and 0012.
|
||||
|
||||
Therefore after installing new version of django-categories, for each project to upgrade you should execute the following commans in order::
|
||||
|
||||
python manage.py migrate categories 0010_add_field_categoryrelation_category --fake --delete-ghost-migrations
|
||||
python manage.py migrate categories 0011_move_category_fks --fake
|
||||
python manage.py migrate categories 0012_remove_story_field --fake
|
||||
python manage.py migrate categories 0013_null_category_id
|
||||
|
||||
This way both the exact database layout and migration history is restored between the two installation paths (new installation from 1.0.3 and upgrade from 1.0.2 to 1.0.3).
|
||||
|
||||
Last migration is needed to set the correct null value for `category_id` field when upgrading from 1.0.2 while is a noop for 1.0.3.
|
||||
|
||||
New in 1.0
|
||||
==========
|
||||
|
||||
**Abstract Base Class for generic hierarchical category models**
|
||||
When you want a multiple types of categories and don't want them all part of the same model, you can now easily create new models by subclassing ``CategoryBase``. You can also add additional metadata as necessary.
|
||||
|
||||
Your model's can subclass ``CategoryBaseAdminForm`` and ``CategoryBaseAdmin`` to get the hierarchical management in the admin.
|
||||
|
||||
See the docs for more information.
|
||||
|
||||
**Increased the default caching time on views**
|
||||
The default setting for ``CACHE_VIEW_LENGTH`` was ``0``, which means it would tell the browser to *never* cache the page. It is now ``600``, which is the default for `CACHE_MIDDLEWARE_SECONDS <https://docs.djangoproject.com/en/1.3/ref/settings/#cache-middleware-seconds>`_
|
||||
|
||||
**Updated for use with Django-MPTT 0.5**
|
||||
Just a few tweaks.
|
||||
|
||||
**Initial compatibility with Django 1.4**
|
||||
More is coming, but at least it works.
|
||||
|
||||
**Slug transliteration for non-ASCII characters**
|
||||
A new setting, ``SLUG_TRANSLITERATOR``, allows you to specify a function for converting the non-ASCII characters to ASCII characters before the slugification. Works great with `Unidecode <http://pypi.python.org/pypi/Unidecode>`_.
|
||||
|
||||
Updated in 0.8.8
|
||||
================
|
||||
|
||||
The `editor` app was placed inside the categories app, `categories.editor`, to avoid any name clashes.
|
||||
|
||||
Upgrading
|
||||
---------
|
||||
|
||||
A setting change is all that is needed::
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'categories',
|
||||
'categories.editor',
|
||||
)
|
||||
|
||||
New in 0.8
|
||||
==========
|
||||
|
||||
**Added an active field**
|
||||
As an alternative to deleting categories, you can make them inactive.
|
||||
|
||||
Also added a manager method ``active()`` to query only the active categories and added Admin Actions to activate or deactivate an item.
|
||||
|
||||
**Improved import**
|
||||
Previously the import saved items in the reverse order to the imported file. Now them import in order.
|
||||
|
||||
New in 0.7
|
||||
==========
|
||||
|
||||
**Added South migrations**
|
||||
All the previous SQL scripts have been converted to South migrations.
|
||||
|
||||
**Can add category fields via management command (and South)**
|
||||
The new ability to setup category relationships in ``settings.py`` works fine if you are starting from scratch, but not if you want to add it after you have set up the database. Now there is a management command to make sure all the correct fields and tables are created.
|
||||
|
||||
**Added an alternate_url field**
|
||||
This allows the specification of a URL that is not derived from the category hierarchy.
|
||||
|
||||
**New JAVASCRIPT_URL setting**
|
||||
This allows some customization of the ``genericcollections.js`` file.
|
||||
|
||||
**New get_latest_objects_by_category template tag**
|
||||
This will do pretty much what it says.
|
||||
|
||||
|
||||
New in 0.6
|
||||
==========
|
||||
|
||||
**Class-based views**
|
||||
Works great with Django 1.3 or `django-cbv <http://pypi.python.org/pypi/django-cbv>`_
|
||||
|
||||
**New Settings infrastructure**
|
||||
To be more like the Django project, we are migrating from individual CATEGORIES_* settings to a dictionary named ``CATEGORIES_SETTINGS``\ . Use of the previous settings will still work but will generate a ``DeprecationError``\ .
|
||||
|
||||
**The tree's initially expanded state is now configurable**
|
||||
``EDITOR_TREE_INITIAL_STATE`` allows a ``collapsed`` or ``expanded`` value. The default is ``collapsed``\ .
|
||||
|
||||
**Optional Thumbnail field**
|
||||
Have a thumbnail for each category!
|
||||
|
||||
**"Categorize" models in settings**
|
||||
Now you don't have to modify the model to add a ``Category`` relationship. Use the new settings to "wire" categories to different models.
|
||||
|
||||
Features of the project
|
||||
=======================
|
||||
|
||||
**Multiple trees, or a single tree**
|
||||
You can treat all the records as a single tree, shared by all the applications. You can also treat each of the top level records as individual trees, for different apps or uses.
|
||||
|
||||
**Easy handling of hierarchical data**
|
||||
We use `Django MPTT <http://pypi.python.org/pypi/django-mptt>`_ to manage the data efficiently and provide the extra access functions.
|
||||
|
||||
**Easy importation of data**
|
||||
Import a tree or trees of space- or tab-indented data with a Django management command.
|
||||
|
||||
**Metadata for better SEO on web pages**
|
||||
Include all the metadata you want for easy inclusion on web pages.
|
||||
|
||||
**Link uncategorized objects to a category**
|
||||
Attach any number of objects to a category, even if the objects themselves aren't categorized.
|
||||
|
||||
**Hierarchical Admin**
|
||||
Shows the data in typical tree form with disclosure triangles
|
||||
|
||||
**Template Helpers**
|
||||
Easy ways for displaying the tree data in templates:
|
||||
|
||||
**Show one level of a tree**
|
||||
All root categories or just children of a specified category
|
||||
|
||||
**Show multiple levels**
|
||||
Ancestors of category, category and all children of category or a category and its children
|
||||
|
|
@ -1,23 +1,5 @@
|
|||
__version_info__ = {
|
||||
'major': 1,
|
||||
'minor': 8,
|
||||
'micro': 0,
|
||||
'releaselevel': 'final',
|
||||
'serial': 1
|
||||
}
|
||||
"""Django categories."""
|
||||
__version__ = "1.8.0"
|
||||
|
||||
|
||||
def get_version(short=False):
|
||||
assert __version_info__['releaselevel'] in ('alpha', 'beta', 'final')
|
||||
vers = ["%(major)i.%(minor)i" % __version_info__, ]
|
||||
if __version_info__['micro'] and not short:
|
||||
vers.append(".%(micro)i" % __version_info__)
|
||||
if __version_info__['releaselevel'] != 'final' and not short:
|
||||
vers.append('%s%i' % (__version_info__['releaselevel'][0], __version_info__['serial']))
|
||||
return ''.join(vers)
|
||||
|
||||
|
||||
__version__ = get_version()
|
||||
|
||||
|
||||
default_app_config = 'categories.apps.CategoriesConfig'
|
||||
default_app_config = "categories.apps.CategoriesConfig"
|
||||
|
|
|
|||
|
|
@ -1,70 +1,82 @@
|
|||
from django.contrib import admin
|
||||
"""Admin interface classes."""
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .base import CategoryBaseAdmin, CategoryBaseAdminForm
|
||||
from .genericcollection import GenericCollectionTabularInline
|
||||
from .settings import RELATION_MODELS, JAVASCRIPT_URL, REGISTER_ADMIN
|
||||
from .models import Category
|
||||
from .base import CategoryBaseAdminForm, CategoryBaseAdmin
|
||||
from .settings import MODEL_REGISTRY
|
||||
from .settings import JAVASCRIPT_URL, MODEL_REGISTRY, REGISTER_ADMIN, RELATION_MODELS
|
||||
|
||||
|
||||
class NullTreeNodeChoiceField(forms.ModelChoiceField):
|
||||
"""A ModelChoiceField for tree nodes."""
|
||||
def __init__(self, level_indicator='---', *args, **kwargs):
|
||||
|
||||
def __init__(self, level_indicator="---", *args, **kwargs):
|
||||
self.level_indicator = level_indicator
|
||||
super(NullTreeNodeChoiceField, self).__init__(*args, **kwargs)
|
||||
|
||||
def label_from_instance(self, obj):
|
||||
def label_from_instance(self, obj) -> str:
|
||||
"""
|
||||
Creates labels which represent the tree level of each node when
|
||||
generating option labels.
|
||||
Creates labels which represent the tree level of each node when generating option labels.
|
||||
"""
|
||||
return '%s %s' % (self.level_indicator * getattr(obj, obj._mptt_meta.level_attr), obj)
|
||||
return "%s %s" % (self.level_indicator * getattr(obj, obj._mptt_meta.level_attr), obj)
|
||||
|
||||
|
||||
if RELATION_MODELS:
|
||||
from .models import CategoryRelation
|
||||
|
||||
class InlineCategoryRelation(GenericCollectionTabularInline):
|
||||
"""The inline admin panel for category relations."""
|
||||
|
||||
model = CategoryRelation
|
||||
|
||||
|
||||
class CategoryAdminForm(CategoryBaseAdminForm):
|
||||
"""The form for a category in the admin."""
|
||||
|
||||
class Meta:
|
||||
model = Category
|
||||
fields = '__all__'
|
||||
fields = "__all__"
|
||||
|
||||
def clean_alternate_title(self):
|
||||
if self.instance is None or not self.cleaned_data['alternate_title']:
|
||||
return self.cleaned_data['name']
|
||||
def clean_alternate_title(self) -> str:
|
||||
"""Return either the name or alternate title for the category."""
|
||||
if self.instance is None or not self.cleaned_data["alternate_title"]:
|
||||
return self.cleaned_data["name"]
|
||||
else:
|
||||
return self.cleaned_data['alternate_title']
|
||||
return self.cleaned_data["alternate_title"]
|
||||
|
||||
|
||||
class CategoryAdmin(CategoryBaseAdmin):
|
||||
"""Admin for categories."""
|
||||
|
||||
form = CategoryAdminForm
|
||||
list_display = ('name', 'alternate_title', 'active')
|
||||
list_display = ("name", "alternate_title", "active")
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': ('parent', 'name', 'thumbnail', 'active')
|
||||
}),
|
||||
(_('Meta Data'), {
|
||||
'fields': ('alternate_title', 'alternate_url', 'description',
|
||||
'meta_keywords', 'meta_extra'),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
(_('Advanced'), {
|
||||
'fields': ('order', 'slug'),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
(None, {"fields": ("parent", "name", "thumbnail", "active")}),
|
||||
(
|
||||
_("Meta Data"),
|
||||
{
|
||||
"fields": ("alternate_title", "alternate_url", "description", "meta_keywords", "meta_extra"),
|
||||
"classes": ("collapse",),
|
||||
},
|
||||
),
|
||||
(
|
||||
_("Advanced"),
|
||||
{
|
||||
"fields": ("order", "slug"),
|
||||
"classes": ("collapse",),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
if RELATION_MODELS:
|
||||
inlines = [InlineCategoryRelation, ]
|
||||
inlines = [
|
||||
InlineCategoryRelation,
|
||||
]
|
||||
|
||||
class Media:
|
||||
js = (JAVASCRIPT_URL + 'genericcollections.js',)
|
||||
js = (JAVASCRIPT_URL + "genericcollections.js",)
|
||||
|
||||
|
||||
if REGISTER_ADMIN:
|
||||
|
|
@ -72,18 +84,21 @@ if REGISTER_ADMIN:
|
|||
|
||||
for model, modeladmin in list(admin.site._registry.items()):
|
||||
if model in list(MODEL_REGISTRY.values()) and modeladmin.fieldsets:
|
||||
fieldsets = getattr(modeladmin, 'fieldsets', ())
|
||||
fields = [cat.split('.')[2] for cat in MODEL_REGISTRY if MODEL_REGISTRY[cat] == model]
|
||||
fieldsets = getattr(modeladmin, "fieldsets", ())
|
||||
fields = [cat.split(".")[2] for cat in MODEL_REGISTRY if MODEL_REGISTRY[cat] == model]
|
||||
# check each field to see if already defined
|
||||
for cat in fields:
|
||||
for k, v in fieldsets:
|
||||
if cat in v['fields']:
|
||||
if cat in v["fields"]:
|
||||
fields.remove(cat)
|
||||
# if there are any fields left, add them under the categories fieldset
|
||||
if len(fields) > 0:
|
||||
admin.site.unregister(model)
|
||||
admin.site.register(model, type('newadmin', (modeladmin.__class__,), {
|
||||
'fieldsets': fieldsets + (('Categories', {
|
||||
'fields': fields
|
||||
}),)
|
||||
}))
|
||||
admin.site.register(
|
||||
model,
|
||||
type(
|
||||
"newadmin",
|
||||
(modeladmin.__class__,),
|
||||
{"fieldsets": fieldsets + (("Categories", {"fields": fields}),)},
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,17 +1,23 @@
|
|||
"""Django application setup."""
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CategoriesConfig(AppConfig):
|
||||
name = 'categories'
|
||||
"""Application configuration for categories."""
|
||||
|
||||
name = "categories"
|
||||
verbose_name = "Categories"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CategoriesConfig, self).__init__(*args, **kwargs)
|
||||
from django.db.models.signals import class_prepared
|
||||
|
||||
class_prepared.connect(handle_class_prepared)
|
||||
|
||||
def ready(self):
|
||||
"""Migrate the app after it is ready."""
|
||||
from django.db.models.signals import post_migrate
|
||||
|
||||
from .migration import migrate_app
|
||||
|
||||
post_migrate.connect(migrate_app)
|
||||
|
|
@ -19,19 +25,20 @@ class CategoriesConfig(AppConfig):
|
|||
|
||||
def handle_class_prepared(sender, **kwargs):
|
||||
"""
|
||||
See if this class needs registering of fields
|
||||
See if this class needs registering of fields.
|
||||
"""
|
||||
from .settings import M2M_REGISTRY, FK_REGISTRY
|
||||
from .registration import registry
|
||||
from .settings import FK_REGISTRY, M2M_REGISTRY
|
||||
|
||||
sender_app = sender._meta.app_label
|
||||
sender_name = sender._meta.model_name
|
||||
|
||||
for key, val in list(FK_REGISTRY.items()):
|
||||
app_name, model_name = key.split('.')
|
||||
app_name, model_name = key.split(".")
|
||||
if app_name == sender_app and sender_name == model_name:
|
||||
registry.register_model(app_name, sender, 'ForeignKey', val)
|
||||
registry.register_model(app_name, sender, "ForeignKey", val)
|
||||
|
||||
for key, val in list(M2M_REGISTRY.items()):
|
||||
app_name, model_name = key.split('.')
|
||||
app_name, model_name = key.split(".")
|
||||
if app_name == sender_app and sender_name == model_name:
|
||||
registry.register_model(app_name, sender, 'ManyToManyField', val)
|
||||
registry.register_model(app_name, sender, "ManyToManyField", val)
|
||||
|
|
|
|||
|
|
@ -1,69 +1,67 @@
|
|||
"""
|
||||
This is the base class on which to build a hierarchical category-like model
|
||||
with customizable metadata and its own name space.
|
||||
"""
|
||||
import sys
|
||||
This is the base class on which to build a hierarchical category-like model.
|
||||
|
||||
It provides customizable metadata and its own name space.
|
||||
"""
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.db import models
|
||||
from django import forms
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from mptt.models import MPTTModel
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from mptt.fields import TreeForeignKey
|
||||
from mptt.managers import TreeManager
|
||||
from mptt.models import MPTTModel
|
||||
from slugify import slugify
|
||||
|
||||
from .editor.tree_editor import TreeEditor
|
||||
from .settings import ALLOW_SLUG_CHANGE, SLUG_TRANSLITERATOR
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
if sys.version_info[0] < 3: # Remove this after dropping support of Python 2
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
else:
|
||||
def python_2_unicode_compatible(x):
|
||||
return x
|
||||
|
||||
|
||||
class CategoryManager(models.Manager):
|
||||
"""
|
||||
A manager that adds an "active()" method for all active categories
|
||||
A manager that adds an "active()" method for all active categories.
|
||||
"""
|
||||
|
||||
def active(self):
|
||||
"""
|
||||
Only categories that are active
|
||||
Only categories that are active.
|
||||
"""
|
||||
return self.get_queryset().filter(active=True)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class CategoryBase(MPTTModel):
|
||||
"""
|
||||
This base model includes the absolute bare bones fields and methods. One
|
||||
could simply subclass this model and do nothing else and it should work.
|
||||
This base model includes the absolute bare-bones fields and methods.
|
||||
|
||||
One could simply subclass this model, do nothing else, and it should work.
|
||||
"""
|
||||
|
||||
parent = TreeForeignKey(
|
||||
'self',
|
||||
"self",
|
||||
on_delete=models.CASCADE,
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name='children',
|
||||
verbose_name=_('parent'),
|
||||
related_name="children",
|
||||
verbose_name=_("parent"),
|
||||
)
|
||||
name = models.CharField(max_length=100, verbose_name=_('name'))
|
||||
slug = models.SlugField(verbose_name=_('slug'))
|
||||
active = models.BooleanField(default=True, verbose_name=_('active'))
|
||||
name = models.CharField(max_length=100, verbose_name=_("name"))
|
||||
slug = models.SlugField(verbose_name=_("slug"))
|
||||
active = models.BooleanField(default=True, verbose_name=_("active"))
|
||||
|
||||
objects = CategoryManager()
|
||||
tree = TreeManager()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""
|
||||
Save the category.
|
||||
|
||||
While you can activate an item without activating its descendants,
|
||||
It doesn't make sense that you can deactivate an item and have its
|
||||
decendants remain active.
|
||||
|
||||
Args:
|
||||
args: generic args
|
||||
kwargs: generic keyword arguments
|
||||
"""
|
||||
if not self.slug:
|
||||
self.slug = slugify(SLUG_TRANSLITERATOR(self.name))[:50]
|
||||
|
|
@ -78,26 +76,33 @@ class CategoryBase(MPTTModel):
|
|||
|
||||
def __str__(self):
|
||||
ancestors = self.get_ancestors()
|
||||
return ' > '.join([force_text(i.name) for i in ancestors] + [self.name, ])
|
||||
return " > ".join(
|
||||
[force_text(i.name) for i in ancestors]
|
||||
+ [
|
||||
self.name,
|
||||
]
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
unique_together = ('parent', 'name')
|
||||
ordering = ('tree_id', 'lft')
|
||||
unique_together = ("parent", "name")
|
||||
ordering = ("tree_id", "lft")
|
||||
|
||||
class MPTTMeta:
|
||||
order_insertion_by = 'name'
|
||||
order_insertion_by = "name"
|
||||
|
||||
|
||||
class CategoryBaseAdminForm(forms.ModelForm):
|
||||
"""Base admin form for categories."""
|
||||
|
||||
def clean_slug(self):
|
||||
if not self.cleaned_data.get('slug', None):
|
||||
if self.instance is None or not ALLOW_SLUG_CHANGE:
|
||||
self.cleaned_data['slug'] = slugify(SLUG_TRANSLITERATOR(self.cleaned_data['name']))
|
||||
return self.cleaned_data['slug'][:50]
|
||||
"""Prune and transliterate the slug."""
|
||||
if not self.cleaned_data.get("slug", None) and (self.instance is None or not ALLOW_SLUG_CHANGE):
|
||||
self.cleaned_data["slug"] = slugify(SLUG_TRANSLITERATOR(self.cleaned_data["name"]))
|
||||
return self.cleaned_data["slug"][:50]
|
||||
|
||||
def clean(self):
|
||||
|
||||
"""Clean the data passed from the admin interface."""
|
||||
super(CategoryBaseAdminForm, self).clean()
|
||||
|
||||
if not self.is_valid():
|
||||
|
|
@ -107,72 +112,74 @@ class CategoryBaseAdminForm(forms.ModelForm):
|
|||
|
||||
# Validate slug is valid in that level
|
||||
kwargs = {}
|
||||
if self.cleaned_data.get('parent', None) is None:
|
||||
kwargs['parent__isnull'] = True
|
||||
if self.cleaned_data.get("parent", None) is None:
|
||||
kwargs["parent__isnull"] = True
|
||||
else:
|
||||
kwargs['parent__pk'] = int(self.cleaned_data['parent'].id)
|
||||
this_level_slugs = [c['slug'] for c in opts.model.objects.filter(**kwargs).values('id', 'slug') if c['id'] != self.instance.id]
|
||||
if self.cleaned_data['slug'] in this_level_slugs:
|
||||
raise forms.ValidationError(_('The slug must be unique among '
|
||||
'the items at its level.'))
|
||||
kwargs["parent__pk"] = int(self.cleaned_data["parent"].id)
|
||||
this_level_slugs = [
|
||||
c["slug"] for c in opts.model.objects.filter(**kwargs).values("id", "slug") if c["id"] != self.instance.id
|
||||
]
|
||||
if self.cleaned_data["slug"] in this_level_slugs:
|
||||
raise forms.ValidationError(_("The slug must be unique among " "the items at its level."))
|
||||
|
||||
# Validate Category Parent
|
||||
# Make sure the category doesn't set itself or any of its children as
|
||||
# its parent.
|
||||
|
||||
if self.cleaned_data.get('parent', None) is None or self.instance.id is None:
|
||||
if self.cleaned_data.get("parent", None) is None or self.instance.id is None:
|
||||
return self.cleaned_data
|
||||
if self.instance.pk:
|
||||
decendant_ids = self.instance.get_descendants().values_list('id', flat=True)
|
||||
decendant_ids = self.instance.get_descendants().values_list("id", flat=True)
|
||||
else:
|
||||
decendant_ids = []
|
||||
|
||||
if self.cleaned_data['parent'].id == self.instance.id:
|
||||
raise forms.ValidationError(_("You can't set the parent of the "
|
||||
"item to itself."))
|
||||
elif self.cleaned_data['parent'].id in decendant_ids:
|
||||
raise forms.ValidationError(_("You can't set the parent of the "
|
||||
"item to a descendant."))
|
||||
if self.cleaned_data["parent"].id == self.instance.id:
|
||||
raise forms.ValidationError(_("You can't set the parent of the " "item to itself."))
|
||||
elif self.cleaned_data["parent"].id in decendant_ids:
|
||||
raise forms.ValidationError(_("You can't set the parent of the " "item to a descendant."))
|
||||
return self.cleaned_data
|
||||
|
||||
|
||||
class CategoryBaseAdmin(TreeEditor, admin.ModelAdmin):
|
||||
form = CategoryBaseAdminForm
|
||||
list_display = ('name', 'active')
|
||||
search_fields = ('name',)
|
||||
prepopulated_fields = {'slug': ('name',)}
|
||||
"""Base admin class for categories."""
|
||||
|
||||
actions = ['activate', 'deactivate']
|
||||
form = CategoryBaseAdminForm
|
||||
list_display = ("name", "active")
|
||||
search_fields = ("name",)
|
||||
prepopulated_fields = {"slug": ("name",)}
|
||||
|
||||
actions = ["activate", "deactivate"]
|
||||
|
||||
def get_actions(self, request):
|
||||
"""Get available actions for the admin interface."""
|
||||
actions = super(CategoryBaseAdmin, self).get_actions(request)
|
||||
if 'delete_selected' in actions:
|
||||
del actions['delete_selected']
|
||||
if "delete_selected" in actions:
|
||||
del actions["delete_selected"]
|
||||
return actions
|
||||
|
||||
def deactivate(self, request, queryset):
|
||||
def deactivate(self, request, queryset): # NOQA: queryset is not used.
|
||||
"""
|
||||
Set active to False for selected items
|
||||
Set active to False for selected items.
|
||||
"""
|
||||
selected_cats = self.model.objects.filter(
|
||||
pk__in=[int(x) for x in request.POST.getlist('_selected_action')])
|
||||
selected_cats = self.model.objects.filter(pk__in=[int(x) for x in request.POST.getlist("_selected_action")])
|
||||
|
||||
for item in selected_cats:
|
||||
if item.active:
|
||||
item.active = False
|
||||
item.save()
|
||||
item.children.all().update(active=False)
|
||||
deactivate.short_description = _('Deactivate selected categories and their children')
|
||||
|
||||
def activate(self, request, queryset):
|
||||
deactivate.short_description = _("Deactivate selected categories and their children")
|
||||
|
||||
def activate(self, request, queryset): # NOQA: queryset is not used.
|
||||
"""
|
||||
Set active to True for selected items
|
||||
Set active to True for selected items.
|
||||
"""
|
||||
selected_cats = self.model.objects.filter(
|
||||
pk__in=[int(x) for x in request.POST.getlist('_selected_action')])
|
||||
selected_cats = self.model.objects.filter(pk__in=[int(x) for x in request.POST.getlist("_selected_action")])
|
||||
|
||||
for item in selected_cats:
|
||||
item.active = True
|
||||
item.save()
|
||||
item.children.all().update(active=True)
|
||||
activate.short_description = _('Activate selected categories and their children')
|
||||
|
||||
activate.short_description = _("Activate selected categories and their children")
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
# Placeholder for Django
|
||||
"""Placeholder for Django."""
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
from django.conf import settings
|
||||
"""Settings management for the editor."""
|
||||
import django
|
||||
from django.conf import settings
|
||||
|
||||
DJANGO10_COMPAT = django.VERSION[0] < 1 or (django.VERSION[0] == 1 and django.VERSION[1] < 1)
|
||||
|
||||
STATIC_URL = getattr(settings, 'STATIC_URL', settings.MEDIA_URL)
|
||||
STATIC_URL = getattr(settings, "STATIC_URL", settings.MEDIA_URL)
|
||||
if STATIC_URL is None:
|
||||
STATIC_URL = settings.MEDIA_URL
|
||||
MEDIA_PATH = getattr(settings, 'EDITOR_MEDIA_PATH', '%seditor/' % STATIC_URL)
|
||||
MEDIA_PATH = getattr(settings, "EDITOR_MEDIA_PATH", "%seditor/" % STATIC_URL)
|
||||
|
||||
TREE_INITIAL_STATE = getattr(settings, 'EDITOR_TREE_INITIAL_STATE', 'collapsed')
|
||||
TREE_INITIAL_STATE = getattr(settings, "EDITOR_TREE_INITIAL_STATE", "collapsed")
|
||||
|
||||
IS_GRAPPELLI_INSTALLED = 'grappelli' in settings.INSTALLED_APPS
|
||||
IS_GRAPPELLI_INSTALLED = "grappelli" in settings.INSTALLED_APPS
|
||||
|
|
|
|||
|
|
@ -66,4 +66,4 @@
|
|||
.treeTable .ui-draggable-dragging {
|
||||
color: #000;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -460,4 +460,4 @@
|
|||
function parentOf(node) {
|
||||
return $(node).parentOf();
|
||||
}
|
||||
})(django.jQuery);
|
||||
})(django.jQuery);
|
||||
|
|
|
|||
|
|
@ -25,4 +25,4 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -36,4 +36,4 @@
|
|||
{% if action_form and actions_on_top and cl.full_result_count %}{% admin_actions %}{% endif %}
|
||||
{% result_tree_list cl %}
|
||||
{% if action_form and actions_on_bottom and cl.full_result_count %}{% admin_actions %}{% endif %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,33 @@
|
|||
"""Template tags used to render the tree editor."""
|
||||
import django
|
||||
from django.contrib.admin.templatetags.admin_list import _boolean_icon, result_headers
|
||||
from django.contrib.admin.utils import lookup_field
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import models
|
||||
from django.template import Library
|
||||
from django.contrib.admin.templatetags.admin_list import result_headers, _boolean_icon
|
||||
from django.contrib.admin.utils import lookup_field
|
||||
from categories.editor.utils import display_for_field
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.utils.encoding import smart_text, force_text
|
||||
from django.utils.html import escape, conditional_escape, escapejs, format_html
|
||||
from django.utils.encoding import force_text, smart_text
|
||||
from django.utils.html import conditional_escape, escape, escapejs, format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from categories.editor import settings
|
||||
from categories.editor.utils import display_for_field
|
||||
|
||||
register = Library()
|
||||
|
||||
TREE_LIST_RESULTS_TEMPLATE = 'admin/editor/tree_list_results.html'
|
||||
TREE_LIST_RESULTS_TEMPLATE = "admin/editor/tree_list_results.html"
|
||||
if settings.IS_GRAPPELLI_INSTALLED:
|
||||
TREE_LIST_RESULTS_TEMPLATE = 'admin/editor/grappelli_tree_list_results.html'
|
||||
TREE_LIST_RESULTS_TEMPLATE = "admin/editor/grappelli_tree_list_results.html"
|
||||
|
||||
|
||||
def get_empty_value_display(cl):
|
||||
if hasattr(cl.model_admin, 'get_empty_value_display'):
|
||||
"""Get the value to display when empty."""
|
||||
if hasattr(cl.model_admin, "get_empty_value_display"):
|
||||
return cl.model_admin.get_empty_value_display()
|
||||
else:
|
||||
# Django < 1.9
|
||||
from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
|
||||
return EMPTY_CHANGELIST_VALUE
|
||||
|
||||
# Django < 1.9
|
||||
from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
|
||||
|
||||
return EMPTY_CHANGELIST_VALUE
|
||||
|
||||
|
||||
def items_for_tree_result(cl, result, form):
|
||||
|
|
@ -34,7 +37,7 @@ def items_for_tree_result(cl, result, form):
|
|||
first = True
|
||||
pk = cl.lookup_opts.pk.attname
|
||||
for field_name in cl.list_display:
|
||||
row_class = ''
|
||||
row_class = ""
|
||||
try:
|
||||
f, attr, value = lookup_field(field_name, result, cl.model_admin)
|
||||
except (AttributeError, ObjectDoesNotExist):
|
||||
|
|
@ -42,10 +45,10 @@ def items_for_tree_result(cl, result, form):
|
|||
else:
|
||||
if f is None:
|
||||
if django.VERSION[0] == 1 and django.VERSION[1] == 4:
|
||||
if field_name == 'action_checkbox':
|
||||
if field_name == "action_checkbox":
|
||||
row_class = ' class="action-checkbox disclosure"'
|
||||
allow_tags = getattr(attr, 'allow_tags', False)
|
||||
boolean = getattr(attr, 'boolean', False)
|
||||
allow_tags = getattr(attr, "allow_tags", False)
|
||||
boolean = getattr(attr, "boolean", False)
|
||||
if boolean:
|
||||
allow_tags = True
|
||||
result_repr = _boolean_icon(value)
|
||||
|
|
@ -60,16 +63,16 @@ def items_for_tree_result(cl, result, form):
|
|||
else:
|
||||
if value is None:
|
||||
result_repr = get_empty_value_display(cl)
|
||||
if hasattr(f, 'rel') and isinstance(f.rel, models.ManyToOneRel):
|
||||
if hasattr(f, "rel") and isinstance(f.rel, models.ManyToOneRel):
|
||||
result_repr = escape(getattr(result, f.name))
|
||||
else:
|
||||
result_repr = display_for_field(value, f, '')
|
||||
result_repr = display_for_field(value, f, "")
|
||||
if isinstance(f, models.DateField) or isinstance(f, models.TimeField):
|
||||
row_class = ' class="nowrap"'
|
||||
if first:
|
||||
if django.VERSION[0] == 1 and django.VERSION[1] < 4:
|
||||
try:
|
||||
f, attr, checkbox_value = lookup_field('action_checkbox', result, cl.model_admin)
|
||||
f, attr, checkbox_value = lookup_field("action_checkbox", result, cl.model_admin)
|
||||
if row_class:
|
||||
row_class = "%s%s" % (row_class[:-1], ' disclosure"')
|
||||
else:
|
||||
|
|
@ -77,14 +80,14 @@ def items_for_tree_result(cl, result, form):
|
|||
except (AttributeError, ObjectDoesNotExist):
|
||||
pass
|
||||
|
||||
if force_text(result_repr) == '':
|
||||
result_repr = mark_safe(' ')
|
||||
if force_text(result_repr) == "":
|
||||
result_repr = mark_safe(" ")
|
||||
# If list_display_links not defined, add the link tag to the first field
|
||||
if (first and not cl.list_display_links) or field_name in cl.list_display_links:
|
||||
if django.VERSION[0] == 1 and django.VERSION[1] < 4:
|
||||
table_tag = 'td' # {True:'th', False:'td'}[first]
|
||||
table_tag = "td" # {True:'th', False:'td'}[first]
|
||||
else:
|
||||
table_tag = {True: 'th', False: 'td'}[first]
|
||||
table_tag = {True: "th", False: "td"}[first]
|
||||
|
||||
url = cl.url_for_result(result)
|
||||
# Convert the pk to something that can be used in Javascript.
|
||||
|
|
@ -104,9 +107,14 @@ def items_for_tree_result(cl, result, form):
|
|||
row_class,
|
||||
url,
|
||||
format_html(
|
||||
' onclick="opener.dismissRelatedLookupPopup(window, '
|
||||
''{}'); return false;"', result_id
|
||||
) if cl.is_popup else '', result_repr, table_tag)
|
||||
' onclick="opener.dismissRelatedLookupPopup(window, ' ''{}'); return false;"',
|
||||
result_id,
|
||||
)
|
||||
if cl.is_popup
|
||||
else "",
|
||||
result_repr,
|
||||
table_tag,
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
|
|
@ -118,20 +126,23 @@ def items_for_tree_result(cl, result, form):
|
|||
result_repr = mark_safe(force_text(bf.errors) + force_text(bf))
|
||||
else:
|
||||
result_repr = conditional_escape(result_repr)
|
||||
yield mark_safe(smart_text('<td%s>%s</td>' % (row_class, result_repr)))
|
||||
yield mark_safe(smart_text("<td%s>%s</td>" % (row_class, result_repr)))
|
||||
if form and not form[cl.model._meta.pk.name].is_hidden:
|
||||
yield mark_safe(smart_text('<td>%s</td>' % force_text(form[cl.model._meta.pk.name])))
|
||||
yield mark_safe(smart_text("<td>%s</td>" % force_text(form[cl.model._meta.pk.name])))
|
||||
|
||||
|
||||
class TreeList(list):
|
||||
"""A list subclass for tree result."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def tree_results(cl):
|
||||
"""Generates a list of results for the tree."""
|
||||
if cl.formset:
|
||||
for res, form in zip(cl.result_list, cl.formset.forms):
|
||||
result = TreeList(items_for_tree_result(cl, res, form))
|
||||
if hasattr(res, 'pk'):
|
||||
if hasattr(res, "pk"):
|
||||
result.pk = res.pk
|
||||
if res.parent:
|
||||
result.parent_pk = res.parent.pk
|
||||
|
|
@ -141,7 +152,7 @@ def tree_results(cl):
|
|||
else:
|
||||
for res in cl.result_list:
|
||||
result = TreeList(items_for_tree_result(cl, res, None))
|
||||
if hasattr(res, 'pk'):
|
||||
if hasattr(res, "pk"):
|
||||
result.pk = res.pk
|
||||
if res.parent:
|
||||
result.parent_pk = res.parent.pk
|
||||
|
|
@ -152,17 +163,15 @@ def tree_results(cl):
|
|||
|
||||
def result_tree_list(cl):
|
||||
"""
|
||||
Displays the headers and data list together
|
||||
Displays the headers and data list together.
|
||||
"""
|
||||
import django
|
||||
result = {
|
||||
'cl': cl,
|
||||
'result_headers': list(result_headers(cl)),
|
||||
'results': list(tree_results(cl))
|
||||
}
|
||||
|
||||
result = {"cl": cl, "result_headers": list(result_headers(cl)), "results": list(tree_results(cl))}
|
||||
if django.VERSION[0] == 1 and django.VERSION[1] > 2:
|
||||
from django.contrib.admin.templatetags.admin_list import result_hidden_fields
|
||||
result['result_hidden_fields'] = list(result_hidden_fields(cl))
|
||||
|
||||
result["result_hidden_fields"] = list(result_hidden_fields(cl))
|
||||
return result
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,23 @@
|
|||
from django.contrib import admin
|
||||
from django.db.models.query import QuerySet
|
||||
from django.contrib.admin.views.main import ChangeList
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.contrib.admin.options import IncorrectLookupParameters
|
||||
from django.shortcuts import render
|
||||
"""Classes for representing tree structures in Django's admin."""
|
||||
from typing import Any
|
||||
|
||||
import django
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin.options import IncorrectLookupParameters
|
||||
from django.contrib.admin.views.main import ChangeList
|
||||
from django.db.models.query import QuerySet
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from . import settings
|
||||
|
||||
|
||||
class TreeEditorQuerySet(QuerySet):
|
||||
"""
|
||||
The TreeEditorQuerySet is a special query set used only in the TreeEditor
|
||||
ChangeList page. The only difference to a regular QuerySet is that it
|
||||
will enforce:
|
||||
A special query set used only in the TreeEditor ChangeList page.
|
||||
|
||||
The only difference to a regular QuerySet is that it will enforce:
|
||||
|
||||
(a) The result is ordered in correct tree order so that
|
||||
the TreeAdmin works all right.
|
||||
|
|
@ -24,7 +26,9 @@ class TreeEditorQuerySet(QuerySet):
|
|||
in the result set, so the resulting tree display actually
|
||||
makes sense.
|
||||
"""
|
||||
|
||||
def iterator(self):
|
||||
"""Iterates through the items in thee query set."""
|
||||
qs = self
|
||||
# Reaching into the bowels of query sets to find out whether the qs is
|
||||
# actually filtered and we need to do the INCLUDE_ANCESTORS dance at all.
|
||||
|
|
@ -36,9 +40,9 @@ class TreeEditorQuerySet(QuerySet):
|
|||
# this cuts down the number of queries considerably since all ancestors
|
||||
# will already be in include_pages when they are checked, thus not
|
||||
# trigger additional queries.
|
||||
for p in super(TreeEditorQuerySet, self.order_by('rght')).iterator():
|
||||
for p in super(TreeEditorQuerySet, self.order_by("rght")).iterator():
|
||||
if p.parent_id and p.parent_id not in include_pages and p.id not in include_pages:
|
||||
ancestor_id_list = p.get_ancestors().values_list('id', flat=True)
|
||||
ancestor_id_list = p.get_ancestors().values_list("id", flat=True)
|
||||
include_pages.update(ancestor_id_list)
|
||||
|
||||
if include_pages:
|
||||
|
|
@ -54,52 +58,75 @@ class TreeEditorQuerySet(QuerySet):
|
|||
# def __getitem__(self, index):
|
||||
# return self # Don't even try to slice
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
def get(self, *args, **kwargs) -> Any:
|
||||
"""
|
||||
Quick and dirty hack to fix change_view and delete_view; they use
|
||||
self.queryset(request).get(...) to get the object they should work
|
||||
with. Our modifications to the queryset when INCLUDE_ANCESTORS is
|
||||
enabled make get() fail often with a MultipleObjectsReturned
|
||||
exception.
|
||||
Quick and dirty hack to fix change_view and delete_view.
|
||||
|
||||
They use ``self.queryset(request).get(...)`` to get the object they should work
|
||||
with. Our modifications to the queryset when ``INCLUDE_ANCESTORS`` is enabled make ``get()``
|
||||
fail often with a ``MultipleObjectsReturned`` exception.
|
||||
|
||||
Args:
|
||||
args: generic arguments
|
||||
kwargs: generic keyword arguments
|
||||
|
||||
Returns:
|
||||
The object they should work with.
|
||||
"""
|
||||
return self.model._default_manager.get(*args, **kwargs)
|
||||
|
||||
|
||||
class TreeChangeList(ChangeList):
|
||||
"""A change list for a tree."""
|
||||
|
||||
def _get_default_ordering(self):
|
||||
if django.VERSION[0] == 1 and django.VERSION[1] < 4:
|
||||
return '', '' # ('tree_id', 'lft')
|
||||
return "", "" # ('tree_id', 'lft')
|
||||
else:
|
||||
return []
|
||||
|
||||
def get_ordering(self, request=None, queryset=None):
|
||||
"""
|
||||
Return ordering information for the change list.
|
||||
|
||||
Always returns empty/default ordering.
|
||||
|
||||
Args:
|
||||
request: The incoming request.
|
||||
queryset: The current queryset
|
||||
|
||||
Returns:
|
||||
Either a tuple of empty strings or an empty list.
|
||||
"""
|
||||
if django.VERSION[0] == 1 and django.VERSION[1] < 4:
|
||||
return '', '' # ('tree_id', 'lft')
|
||||
return "", "" # ('tree_id', 'lft')
|
||||
else:
|
||||
return []
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
qs = super(TreeChangeList, self).get_queryset(*args, **kwargs).order_by('tree_id', 'lft')
|
||||
return qs
|
||||
"""Return a queryset."""
|
||||
return super(TreeChangeList, self).get_queryset(*args, **kwargs).order_by("tree_id", "lft")
|
||||
|
||||
|
||||
class TreeEditor(admin.ModelAdmin):
|
||||
"""A tree editor view for Django's admin."""
|
||||
|
||||
list_per_page = 999999999 # We can't have pagination
|
||||
list_max_show_all = 200 # new in django 1.4
|
||||
|
||||
class Media:
|
||||
css = {'all': (settings.MEDIA_PATH + "jquery.treeTable.css", )}
|
||||
css = {"all": (settings.MEDIA_PATH + "jquery.treeTable.css",)}
|
||||
js = []
|
||||
|
||||
js.extend((settings.MEDIA_PATH + "jquery.treeTable.js", ))
|
||||
js.extend((settings.MEDIA_PATH + "jquery.treeTable.js",))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TreeEditor, self).__init__(*args, **kwargs)
|
||||
|
||||
self.list_display = list(self.list_display)
|
||||
|
||||
if 'action_checkbox' in self.list_display:
|
||||
self.list_display.remove('action_checkbox')
|
||||
if "action_checkbox" in self.list_display:
|
||||
self.list_display.remove("action_checkbox")
|
||||
|
||||
opts = self.model._meta
|
||||
|
||||
|
|
@ -108,9 +135,9 @@ class TreeEditor(admin.ModelAdmin):
|
|||
grappelli_prefix = "grappelli_"
|
||||
|
||||
self.change_list_template = [
|
||||
'admin/%s/%s/editor/%stree_editor.html' % (opts.app_label, opts.object_name.lower(), grappelli_prefix),
|
||||
'admin/%s/editor/%stree_editor.html' % (opts.app_label, grappelli_prefix),
|
||||
'admin/editor/%stree_editor.html' % grappelli_prefix,
|
||||
"admin/%s/%s/editor/%stree_editor.html" % (opts.app_label, opts.object_name.lower(), grappelli_prefix),
|
||||
"admin/%s/editor/%stree_editor.html" % (opts.app_label, grappelli_prefix),
|
||||
"admin/editor/%stree_editor.html" % grappelli_prefix,
|
||||
]
|
||||
|
||||
def get_changelist(self, request, **kwargs):
|
||||
|
|
@ -120,11 +147,12 @@ class TreeEditor(admin.ModelAdmin):
|
|||
return TreeChangeList
|
||||
|
||||
def old_changelist_view(self, request, extra_context=None):
|
||||
"The 'change list' admin view for this model."
|
||||
"""The 'change list' admin view for this model."""
|
||||
from django.contrib.admin.views.main import ERROR_FLAG
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.translation import ungettext
|
||||
|
||||
opts = self.model._meta
|
||||
app_label = opts.app_label
|
||||
if not self.has_change_permission(request, None):
|
||||
|
|
@ -137,31 +165,56 @@ class TreeEditor(admin.ModelAdmin):
|
|||
list_display = list(self.list_display)
|
||||
if not actions:
|
||||
try:
|
||||
list_display.remove('action_checkbox')
|
||||
list_display.remove("action_checkbox")
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
if django.VERSION[0] == 1 and django.VERSION[1] < 4:
|
||||
params = (
|
||||
request, self.model, list_display,
|
||||
self.list_display_links, self.list_filter, self.date_hierarchy,
|
||||
self.search_fields, self.list_select_related,
|
||||
self.list_per_page, self.list_editable, self)
|
||||
request,
|
||||
self.model,
|
||||
list_display,
|
||||
self.list_display_links,
|
||||
self.list_filter,
|
||||
self.date_hierarchy,
|
||||
self.search_fields,
|
||||
self.list_select_related,
|
||||
self.list_per_page,
|
||||
self.list_editable,
|
||||
self,
|
||||
)
|
||||
elif django.VERSION[0] == 1 or (django.VERSION[0] == 2 and django.VERSION[1] < 1):
|
||||
params = (
|
||||
request, self.model, list_display,
|
||||
self.list_display_links, self.list_filter, self.date_hierarchy,
|
||||
self.search_fields, self.list_select_related,
|
||||
self.list_per_page, self.list_max_show_all,
|
||||
self.list_editable, self)
|
||||
request,
|
||||
self.model,
|
||||
list_display,
|
||||
self.list_display_links,
|
||||
self.list_filter,
|
||||
self.date_hierarchy,
|
||||
self.search_fields,
|
||||
self.list_select_related,
|
||||
self.list_per_page,
|
||||
self.list_max_show_all,
|
||||
self.list_editable,
|
||||
self,
|
||||
)
|
||||
else:
|
||||
params = (
|
||||
request, self.model, list_display,
|
||||
self.list_display_links, self.list_filter, self.date_hierarchy,
|
||||
self.search_fields, self.list_select_related,
|
||||
self.list_per_page, self.list_max_show_all,
|
||||
self.list_editable, self, self.sortable_by)
|
||||
request,
|
||||
self.model,
|
||||
list_display,
|
||||
self.list_display_links,
|
||||
self.list_filter,
|
||||
self.date_hierarchy,
|
||||
self.search_fields,
|
||||
self.list_select_related,
|
||||
self.list_per_page,
|
||||
self.list_max_show_all,
|
||||
self.list_editable,
|
||||
self,
|
||||
self.sortable_by,
|
||||
)
|
||||
cl = TreeChangeList(*params)
|
||||
except IncorrectLookupParameters:
|
||||
# Wacky lookup parameters were given, so redirect to the main
|
||||
|
|
@ -170,15 +223,13 @@ class TreeEditor(admin.ModelAdmin):
|
|||
# the 'invalid=1' parameter was already in the query string, something
|
||||
# is screwed up with the database, so display an error page.
|
||||
if ERROR_FLAG in list(request.GET.keys()):
|
||||
return render(
|
||||
request,
|
||||
'admin/invalid_setup.html', {'title': _('Database error')})
|
||||
return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1')
|
||||
return render(request, "admin/invalid_setup.html", {"title": _("Database error")})
|
||||
return HttpResponseRedirect(request.path + "?" + ERROR_FLAG + "=1")
|
||||
|
||||
# If the request was POSTed, this might be a bulk action or a bulk edit.
|
||||
# Try to look up an action first, but if this isn't an action the POST
|
||||
# will fall through to the bulk edit check, below.
|
||||
if actions and request.method == 'POST':
|
||||
if actions and request.method == "POST":
|
||||
response = self.response_action(request, queryset=cl.get_queryset())
|
||||
if response:
|
||||
return response
|
||||
|
|
@ -191,9 +242,7 @@ class TreeEditor(admin.ModelAdmin):
|
|||
# Handle POSTed bulk-edit data.
|
||||
if request.method == "POST" and self.list_editable:
|
||||
FormSet = self.get_changelist_formset(request)
|
||||
formset = cl.formset = FormSet(
|
||||
request.POST, request.FILES, queryset=cl.result_list
|
||||
)
|
||||
formset = cl.formset = FormSet(request.POST, request.FILES, queryset=cl.result_list)
|
||||
if formset.is_valid():
|
||||
changecount = 0
|
||||
for form in formset.forms:
|
||||
|
|
@ -210,12 +259,14 @@ class TreeEditor(admin.ModelAdmin):
|
|||
name = force_text(opts.verbose_name)
|
||||
else:
|
||||
name = force_text(opts.verbose_name_plural)
|
||||
msg = ungettext(
|
||||
"%(count)s %(name)s was changed successfully.",
|
||||
"%(count)s %(name)s were changed successfully.",
|
||||
changecount) % {'count': changecount,
|
||||
'name': name,
|
||||
'obj': force_text(obj)}
|
||||
msg = (
|
||||
ungettext(
|
||||
"%(count)s %(name)s was changed successfully.",
|
||||
"%(count)s %(name)s were changed successfully.",
|
||||
changecount,
|
||||
)
|
||||
% {"count": changecount, "name": name, "obj": force_text(obj)}
|
||||
)
|
||||
self.message_user(request, msg)
|
||||
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
|
|
@ -234,57 +285,70 @@ class TreeEditor(admin.ModelAdmin):
|
|||
# Build the action form and populate it with available actions.
|
||||
if actions:
|
||||
action_form = self.action_form(auto_id=None)
|
||||
action_form.fields['action'].choices = self.get_action_choices(request)
|
||||
action_form.fields["action"].choices = self.get_action_choices(request)
|
||||
else:
|
||||
action_form = None
|
||||
|
||||
context = {
|
||||
'title': cl.title,
|
||||
'is_popup': cl.is_popup,
|
||||
'cl': cl,
|
||||
'media': media,
|
||||
'has_add_permission': self.has_add_permission(request),
|
||||
'app_label': app_label,
|
||||
'action_form': action_form,
|
||||
'actions_on_top': self.actions_on_top,
|
||||
'actions_on_bottom': self.actions_on_bottom,
|
||||
"title": cl.title,
|
||||
"is_popup": cl.is_popup,
|
||||
"cl": cl,
|
||||
"media": media,
|
||||
"has_add_permission": self.has_add_permission(request),
|
||||
"app_label": app_label,
|
||||
"action_form": action_form,
|
||||
"actions_on_top": self.actions_on_top,
|
||||
"actions_on_bottom": self.actions_on_bottom,
|
||||
}
|
||||
if django.VERSION[0] == 1 and django.VERSION[1] < 4:
|
||||
context['root_path'] = self.admin_site.root_path
|
||||
context["root_path"] = self.admin_site.root_path
|
||||
elif django.VERSION[0] == 1 or (django.VERSION[0] == 2 and django.VERSION[1] < 1):
|
||||
selection_note_all = ungettext('%(total_count)s selected', 'All %(total_count)s selected', cl.result_count)
|
||||
selection_note_all = ungettext("%(total_count)s selected", "All %(total_count)s selected", cl.result_count)
|
||||
|
||||
context.update({
|
||||
'module_name': force_text(opts.verbose_name_plural),
|
||||
'selection_note': _('0 of %(cnt)s selected') % {'cnt': len(cl.result_list)},
|
||||
'selection_note_all': selection_note_all % {'total_count': cl.result_count},
|
||||
})
|
||||
context.update(
|
||||
{
|
||||
"module_name": force_text(opts.verbose_name_plural),
|
||||
"selection_note": _("0 of %(cnt)s selected") % {"cnt": len(cl.result_list)},
|
||||
"selection_note_all": selection_note_all % {"total_count": cl.result_count},
|
||||
}
|
||||
)
|
||||
else:
|
||||
context['opts'] = self.model._meta
|
||||
context["opts"] = self.model._meta
|
||||
|
||||
context.update(extra_context or {})
|
||||
return render(request, self.change_list_template or [
|
||||
'admin/%s/%s/change_list.html' % (app_label, opts.object_name.lower()),
|
||||
'admin/%s/change_list.html' % app_label,
|
||||
'admin/change_list.html'
|
||||
], context=context)
|
||||
return render(
|
||||
request,
|
||||
self.change_list_template
|
||||
or [
|
||||
"admin/%s/%s/change_list.html" % (app_label, opts.object_name.lower()),
|
||||
"admin/%s/change_list.html" % app_label,
|
||||
"admin/change_list.html",
|
||||
],
|
||||
context=context,
|
||||
)
|
||||
|
||||
def changelist_view(self, request, extra_context=None, *args, **kwargs):
|
||||
"""
|
||||
Handle the changelist view, the django view for the model instances
|
||||
change list/actions page.
|
||||
Handle the changelist view, the django view for the model instances change list/actions page.
|
||||
"""
|
||||
extra_context = extra_context or {}
|
||||
extra_context['EDITOR_MEDIA_PATH'] = settings.MEDIA_PATH
|
||||
extra_context['EDITOR_TREE_INITIAL_STATE'] = settings.TREE_INITIAL_STATE
|
||||
extra_context["EDITOR_MEDIA_PATH"] = settings.MEDIA_PATH
|
||||
extra_context["EDITOR_TREE_INITIAL_STATE"] = settings.TREE_INITIAL_STATE
|
||||
|
||||
# FIXME
|
||||
return self.old_changelist_view(request, extra_context)
|
||||
|
||||
def get_queryset(self, request):
|
||||
def get_queryset(self, request) -> TreeEditorQuerySet:
|
||||
"""
|
||||
Returns a QuerySet of all model instances that can be edited by the
|
||||
admin site. This is used by changelist_view.
|
||||
Returns a QuerySet of all model instances that can be edited by the admin site.
|
||||
|
||||
This is used by changelist_view.
|
||||
|
||||
Args:
|
||||
request: the incoming request.
|
||||
|
||||
Returns:
|
||||
A QuerySet of editable model instances
|
||||
"""
|
||||
qs = self.model._default_manager.get_queryset()
|
||||
qs.__class__ = TreeEditorQuerySet
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
"""
|
||||
Provides compatibility with Django 1.8
|
||||
Provides compatibility with Django 1.8.
|
||||
"""
|
||||
from django.contrib.admin.utils import display_for_field as _display_for_field
|
||||
|
||||
|
||||
def display_for_field(value, field, empty_value_display=None):
|
||||
"""Compatility for displaying a field in Django 1.8."""
|
||||
try:
|
||||
return _display_for_field(value, field, empty_value_display)
|
||||
except TypeError:
|
||||
|
|
|
|||
|
|
@ -1,25 +1,24 @@
|
|||
"""Custom category fields for other models."""
|
||||
from django.db.models import ForeignKey, ManyToManyField
|
||||
|
||||
|
||||
class CategoryM2MField(ManyToManyField):
|
||||
"""A many to many field to a Category model."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
from .models import Category
|
||||
if 'to' in kwargs:
|
||||
kwargs.pop('to')
|
||||
|
||||
if "to" in kwargs:
|
||||
kwargs.pop("to")
|
||||
super(CategoryM2MField, self).__init__(to=Category, **kwargs)
|
||||
|
||||
|
||||
class CategoryFKField(ForeignKey):
|
||||
"""A foreign key to the Category model."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
from .models import Category
|
||||
if 'to' in kwargs:
|
||||
kwargs.pop('to')
|
||||
|
||||
if "to" in kwargs:
|
||||
kwargs.pop("to")
|
||||
super(CategoryFKField, self).__init__(to=Category, **kwargs)
|
||||
|
||||
|
||||
try:
|
||||
from south.modelsinspector import add_introspection_rules
|
||||
add_introspection_rules([], [r"^categories\.fields\.CategoryFKField"])
|
||||
add_introspection_rules([], [r"^categories\.fields\.CategoryM2MField"])
|
||||
except ImportError:
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -4414,4 +4414,4 @@
|
|||
}
|
||||
}
|
||||
|
||||
]
|
||||
]
|
||||
|
|
|
|||
|
|
@ -5,4 +5,4 @@ Category 2
|
|||
Category 2-1
|
||||
Category 2-1-1
|
||||
Category 3
|
||||
Category 3-1
|
||||
Category 3-1
|
||||
|
|
|
|||
|
|
@ -5,4 +5,4 @@ Category 2
|
|||
Category 2-1
|
||||
Category 2-1-1
|
||||
Category 3
|
||||
Category 3-1
|
||||
Category 3-1
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
"""Special helpers for generic collections."""
|
||||
import json
|
||||
|
||||
from django.contrib import admin
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.urls import reverse, NoReverseMatch
|
||||
import json
|
||||
from django.urls import NoReverseMatch, reverse
|
||||
|
||||
|
||||
class GenericCollectionInlineModelAdmin(admin.options.InlineModelAdmin):
|
||||
"""Inline admin for generic model collections."""
|
||||
|
||||
ct_field = "content_type"
|
||||
ct_fk_field = "object_id"
|
||||
|
||||
def get_content_types(self):
|
||||
ctypes = ContentType.objects.all().order_by('id').values_list('id', 'app_label', 'model')
|
||||
"""Get the content types supported by this collection."""
|
||||
ctypes = ContentType.objects.all().order_by("id").values_list("id", "app_label", "model")
|
||||
elements = {}
|
||||
for x, y, z in ctypes:
|
||||
try:
|
||||
|
|
@ -19,18 +24,23 @@ class GenericCollectionInlineModelAdmin(admin.options.InlineModelAdmin):
|
|||
return json.dumps(elements)
|
||||
|
||||
def get_formset(self, request, obj=None, **kwargs):
|
||||
"""Get the formset for the generic collection."""
|
||||
result = super(GenericCollectionInlineModelAdmin, self).get_formset(request, obj, **kwargs)
|
||||
result.content_types = self.get_content_types()
|
||||
result.ct_fk_field = self.ct_fk_field
|
||||
return result
|
||||
|
||||
class Media:
|
||||
js = ('contentrelations/js/genericlookup.js', )
|
||||
js = ("contentrelations/js/genericlookup.js",)
|
||||
|
||||
|
||||
class GenericCollectionTabularInline(GenericCollectionInlineModelAdmin):
|
||||
template = 'admin/edit_inline/gen_coll_tabular.html'
|
||||
"""Tabular model admin for a generic collection."""
|
||||
|
||||
template = "admin/edit_inline/gen_coll_tabular.html"
|
||||
|
||||
|
||||
class GenericCollectionStackedInline(GenericCollectionInlineModelAdmin):
|
||||
template = 'admin/edit_inline/gen_coll_stacked.html'
|
||||
"""Stacked model admin for a generic collection."""
|
||||
|
||||
template = "admin/edit_inline/gen_coll_stacked.html"
|
||||
|
|
|
|||
|
|
@ -181,4 +181,3 @@ msgstr "Cancella?"
|
|||
#: templates/admin/edit_inline/gen_coll_tabular.html:24
|
||||
msgid "View on site"
|
||||
msgstr "Vedi sul sito"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +1,30 @@
|
|||
"""The add_category_fields command."""
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
Alter one or more models' tables with the registered attributes
|
||||
Alter one or more models' tables with the registered attributes.
|
||||
"""
|
||||
|
||||
help = "Alter the tables for all registered models, or just specified models"
|
||||
args = "[appname ...]"
|
||||
can_import_settings = True
|
||||
requires_system_checks = False
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('app_names', nargs='*')
|
||||
"""Add app_names argument to the command."""
|
||||
parser.add_argument("app_names", nargs="*")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""
|
||||
Alter the tables
|
||||
Alter the tables.
|
||||
"""
|
||||
|
||||
from categories.migration import migrate_app
|
||||
from categories.settings import MODEL_REGISTRY
|
||||
if options['app_names']:
|
||||
for app in options['app_names']:
|
||||
|
||||
if options["app_names"]:
|
||||
for app in options["app_names"]:
|
||||
migrate_app(None, app)
|
||||
else:
|
||||
for app in MODEL_REGISTRY:
|
||||
|
|
|
|||
|
|
@ -1,27 +1,30 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
from django.core.management.base import CommandError
|
||||
"""Alter one or more models' tables with the registered attributes."""
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
Alter one or more models' tables with the registered attributes
|
||||
Alter one or more models' tables with the registered attributes.
|
||||
"""
|
||||
|
||||
help = "Drop the given field from the given model's table"
|
||||
args = "appname modelname fieldname"
|
||||
can_import_settings = True
|
||||
requires_system_checks = False
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('app_name')
|
||||
parser.add_argument('model_name')
|
||||
parser.add_argument('field_name')
|
||||
"""Add app_name, model_name, and field_name arguments to the command."""
|
||||
parser.add_argument("app_name")
|
||||
parser.add_argument("model_name")
|
||||
parser.add_argument("field_name")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""
|
||||
Alter the tables
|
||||
Alter the tables.
|
||||
"""
|
||||
from categories.migration import drop_field
|
||||
if 'app_name' not in options or 'model_name' not in options or 'field_name' not in options:
|
||||
|
||||
if "app_name" not in options or "model_name" not in options or "field_name" not in options:
|
||||
raise CommandError("You must specify an Application name, a Model name and a Field name")
|
||||
|
||||
drop_field(options['app_name'], options['model_name'], options['field_name'])
|
||||
drop_field(options["app_name"], options["model_name"], options["field_name"])
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"""Import category trees from a file."""
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import transaction
|
||||
|
||||
from slugify import slugify
|
||||
|
||||
from categories.models import Category
|
||||
|
|
@ -10,35 +11,37 @@ from categories.settings import SLUG_TRANSLITERATOR
|
|||
class Command(BaseCommand):
|
||||
"""Import category trees from a file."""
|
||||
|
||||
help = "Imports category tree(s) from a file. Sub categories must be indented by the same multiple of spaces or tabs."
|
||||
help = (
|
||||
"Imports category tree(s) from a file. Sub categories must be indented by the same multiple of spaces or tabs."
|
||||
)
|
||||
args = "file_path [file_path ...]"
|
||||
|
||||
def get_indent(self, string):
|
||||
"""
|
||||
Look through the string and count the spaces
|
||||
Look through the string and count the spaces.
|
||||
"""
|
||||
indent_amt = 0
|
||||
|
||||
if string[0] == '\t':
|
||||
return '\t'
|
||||
if string[0] == "\t":
|
||||
return "\t"
|
||||
for char in string:
|
||||
if char == ' ':
|
||||
if char == " ":
|
||||
indent_amt += 1
|
||||
else:
|
||||
return ' ' * indent_amt
|
||||
return " " * indent_amt
|
||||
|
||||
@transaction.atomic
|
||||
def make_category(self, string, parent=None, order=1):
|
||||
"""
|
||||
Make and save a category object from a string
|
||||
Make and save a category object from a string.
|
||||
"""
|
||||
cat = Category(
|
||||
name=string.strip(),
|
||||
slug=slugify(SLUG_TRANSLITERATOR(string.strip()))[:49],
|
||||
# arent=parent,
|
||||
order=order
|
||||
order=order,
|
||||
)
|
||||
cat._tree_manager.insert_node(cat, parent, 'last-child', True)
|
||||
cat._tree_manager.insert_node(cat, parent, "last-child", True)
|
||||
cat.save()
|
||||
if parent:
|
||||
parent.rght = cat.rght + 1
|
||||
|
|
@ -47,12 +50,12 @@ class Command(BaseCommand):
|
|||
|
||||
def parse_lines(self, lines):
|
||||
"""
|
||||
Do the work of parsing each line
|
||||
Do the work of parsing each line.
|
||||
"""
|
||||
indent = ''
|
||||
indent = ""
|
||||
level = 0
|
||||
|
||||
if lines[0][0] == ' ' or lines[0][0] == '\t':
|
||||
if lines[0][0] in [" ", "\t"]:
|
||||
raise CommandError("The first line in the file cannot start with a space or tab.")
|
||||
|
||||
# This keeps track of the current parents at a given level
|
||||
|
|
@ -61,10 +64,10 @@ class Command(BaseCommand):
|
|||
for line in lines:
|
||||
if len(line) == 0:
|
||||
continue
|
||||
if line[0] == ' ' or line[0] == '\t':
|
||||
if indent == '':
|
||||
if line[0] in [" ", "\t"]:
|
||||
if indent == "":
|
||||
indent = self.get_indent(line)
|
||||
elif not line[0] in indent:
|
||||
elif line[0] not in indent:
|
||||
raise CommandError("You can't mix spaces and tabs for indents")
|
||||
level = line.count(indent)
|
||||
current_parents[level] = self.make_category(line, parent=current_parents[level - 1])
|
||||
|
|
@ -75,7 +78,7 @@ class Command(BaseCommand):
|
|||
|
||||
def handle(self, *file_paths, **options):
|
||||
"""
|
||||
Handle the basic import
|
||||
Handle the basic import.
|
||||
"""
|
||||
import os
|
||||
|
||||
|
|
@ -83,8 +86,6 @@ class Command(BaseCommand):
|
|||
if not os.path.isfile(file_path):
|
||||
print("File %s not found." % file_path)
|
||||
continue
|
||||
f = open(file_path, 'r')
|
||||
data = f.readlines()
|
||||
f.close()
|
||||
|
||||
with open(file_path, "r") as f:
|
||||
data = f.readlines()
|
||||
self.parse_lines(data)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from django.db import connection, transaction
|
||||
"""Adds and removes category relations on the database."""
|
||||
from django.apps import apps
|
||||
from django.db import connection, transaction
|
||||
from django.db.utils import ProgrammingError
|
||||
|
||||
|
||||
def table_exists(table_name):
|
||||
"""
|
||||
Check if a table exists in the database
|
||||
Check if a table exists in the database.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
|
@ -25,7 +23,7 @@ def field_exists(app_name, model_name, field_name):
|
|||
|
||||
# Return True if the many to many table exists
|
||||
field = model._meta.get_field(field_name)
|
||||
if hasattr(field, 'm2m_db_table'):
|
||||
if hasattr(field, "m2m_db_table"):
|
||||
m2m_table_name = field.m2m_db_table()
|
||||
m2m_field_info = connection.introspection.get_table_description(cursor, m2m_table_name)
|
||||
if m2m_field_info:
|
||||
|
|
@ -36,7 +34,7 @@ def field_exists(app_name, model_name, field_name):
|
|||
|
||||
def drop_field(app_name, model_name, field_name):
|
||||
"""
|
||||
Drop the given field from the app's model
|
||||
Drop the given field from the app's model.
|
||||
"""
|
||||
app_config = apps.get_app_config(app_name)
|
||||
model = app_config.get_model(model_name)
|
||||
|
|
@ -47,12 +45,13 @@ def drop_field(app_name, model_name, field_name):
|
|||
|
||||
def migrate_app(sender, *args, **kwargs):
|
||||
"""
|
||||
Migrate all models of this app registered
|
||||
Migrate all models of this app registered.
|
||||
"""
|
||||
from .registration import registry
|
||||
if 'app_config' not in kwargs:
|
||||
|
||||
if "app_config" not in kwargs:
|
||||
return
|
||||
app_config = kwargs['app_config']
|
||||
app_config = kwargs["app_config"]
|
||||
|
||||
app_name = app_config.label
|
||||
|
||||
|
|
@ -60,7 +59,7 @@ def migrate_app(sender, *args, **kwargs):
|
|||
|
||||
sid = transaction.savepoint()
|
||||
for fld in fields:
|
||||
model_name, field_name = fld.split('.')[1:]
|
||||
model_name, field_name = fld.split(".")[1:]
|
||||
if field_exists(app_name, model_name, field_name):
|
||||
continue
|
||||
model = app_config.get_model(model_name)
|
||||
|
|
|
|||
|
|
@ -1,59 +1,121 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from django.db import models, migrations
|
||||
import django.core.files.storage
|
||||
import mptt.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0001_initial'),
|
||||
("contenttypes", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Category',
|
||||
name="Category",
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.CharField(max_length=100, verbose_name='name')),
|
||||
('slug', models.SlugField(verbose_name='slug')),
|
||||
('active', models.BooleanField(default=True, verbose_name='active')),
|
||||
('thumbnail', models.FileField(storage=django.core.files.storage.FileSystemStorage(), null=True, upload_to='uploads/categories/thumbnails', blank=True)),
|
||||
('thumbnail_width', models.IntegerField(null=True, blank=True)),
|
||||
('thumbnail_height', models.IntegerField(null=True, blank=True)),
|
||||
('order', models.IntegerField(default=0)),
|
||||
('alternate_title', models.CharField(default='', help_text='An alternative title to use on pages with this category.', max_length=100, blank=True)),
|
||||
('alternate_url', models.CharField(help_text='An alternative URL to use instead of the one derived from the category hierarchy.', max_length=200, blank=True)),
|
||||
('description', models.TextField(null=True, blank=True)),
|
||||
('meta_keywords', models.CharField(default='', help_text='Comma-separated keywords for search engines.', max_length=255, blank=True)),
|
||||
('meta_extra', models.TextField(default='', help_text='(Advanced) Any additional HTML to be placed verbatim in the <head>', blank=True)),
|
||||
('lft', models.PositiveIntegerField(editable=False, db_index=True)),
|
||||
('rght', models.PositiveIntegerField(editable=False, db_index=True)),
|
||||
('tree_id', models.PositiveIntegerField(editable=False, db_index=True)),
|
||||
('level', models.PositiveIntegerField(editable=False, db_index=True)),
|
||||
('parent', mptt.fields.TreeForeignKey(related_name='children', verbose_name='parent', blank=True, to='categories.Category', on_delete=models.CASCADE, null=True)),
|
||||
("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)),
|
||||
("name", models.CharField(max_length=100, verbose_name="name")),
|
||||
("slug", models.SlugField(verbose_name="slug")),
|
||||
("active", models.BooleanField(default=True, verbose_name="active")),
|
||||
(
|
||||
"thumbnail",
|
||||
models.FileField(
|
||||
storage=django.core.files.storage.FileSystemStorage(),
|
||||
null=True,
|
||||
upload_to="uploads/categories/thumbnails",
|
||||
blank=True,
|
||||
),
|
||||
),
|
||||
("thumbnail_width", models.IntegerField(null=True, blank=True)),
|
||||
("thumbnail_height", models.IntegerField(null=True, blank=True)),
|
||||
("order", models.IntegerField(default=0)),
|
||||
(
|
||||
"alternate_title",
|
||||
models.CharField(
|
||||
default="",
|
||||
help_text="An alternative title to use on pages with this category.",
|
||||
max_length=100,
|
||||
blank=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"alternate_url",
|
||||
models.CharField(
|
||||
help_text="An alternative URL to use instead of the one derived from the category hierarchy.",
|
||||
max_length=200,
|
||||
blank=True,
|
||||
),
|
||||
),
|
||||
("description", models.TextField(null=True, blank=True)),
|
||||
(
|
||||
"meta_keywords",
|
||||
models.CharField(
|
||||
default="",
|
||||
help_text="Comma-separated keywords for search engines.",
|
||||
max_length=255,
|
||||
blank=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"meta_extra",
|
||||
models.TextField(
|
||||
default="",
|
||||
help_text="(Advanced) Any additional HTML to be placed verbatim in the <head>",
|
||||
blank=True,
|
||||
),
|
||||
),
|
||||
("lft", models.PositiveIntegerField(editable=False, db_index=True)),
|
||||
("rght", models.PositiveIntegerField(editable=False, db_index=True)),
|
||||
("tree_id", models.PositiveIntegerField(editable=False, db_index=True)),
|
||||
("level", models.PositiveIntegerField(editable=False, db_index=True)),
|
||||
(
|
||||
"parent",
|
||||
mptt.fields.TreeForeignKey(
|
||||
related_name="children",
|
||||
verbose_name="parent",
|
||||
blank=True,
|
||||
to="categories.Category",
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'ordering': ('tree_id', 'lft'),
|
||||
'abstract': False,
|
||||
'verbose_name': 'category',
|
||||
'verbose_name_plural': 'categories',
|
||||
"ordering": ("tree_id", "lft"),
|
||||
"abstract": False,
|
||||
"verbose_name": "category",
|
||||
"verbose_name_plural": "categories",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CategoryRelation',
|
||||
name="CategoryRelation",
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('object_id', models.PositiveIntegerField(verbose_name='object id')),
|
||||
('relation_type', models.CharField(help_text="A generic text field to tag a relation, like 'leadphoto'.", max_length='200', null=True, verbose_name='relation type', blank=True)),
|
||||
('category', models.ForeignKey(verbose_name='category', to='categories.Category', on_delete=models.CASCADE)),
|
||||
('content_type', models.ForeignKey(verbose_name='content type', to='contenttypes.ContentType', on_delete=models.CASCADE)),
|
||||
("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)),
|
||||
("object_id", models.PositiveIntegerField(verbose_name="object id")),
|
||||
(
|
||||
"relation_type",
|
||||
models.CharField(
|
||||
help_text="A generic text field to tag a relation, like 'leadphoto'.",
|
||||
max_length="200",
|
||||
null=True,
|
||||
verbose_name="relation type",
|
||||
blank=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"category",
|
||||
models.ForeignKey(verbose_name="category", to="categories.Category", on_delete=models.CASCADE),
|
||||
),
|
||||
(
|
||||
"content_type",
|
||||
models.ForeignKey(
|
||||
verbose_name="content type", to="contenttypes.ContentType", on_delete=models.CASCADE
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='category',
|
||||
unique_together=set([('parent', 'name')]),
|
||||
name="category",
|
||||
unique_together=set([("parent", "name")]),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,27 +1,32 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.5 on 2017-02-17 11:11
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.manager
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('categories', '0001_initial'),
|
||||
("categories", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelManagers(
|
||||
name='category',
|
||||
name="category",
|
||||
managers=[
|
||||
('tree', django.db.models.manager.Manager()),
|
||||
("tree", django.db.models.manager.Manager()),
|
||||
],
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='categoryrelation',
|
||||
name='relation_type',
|
||||
field=models.CharField(blank=True, help_text="A generic text field to tag a relation, like 'leadphoto'.", max_length=200, null=True, verbose_name='relation type'),
|
||||
model_name="categoryrelation",
|
||||
name="relation_type",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="A generic text field to tag a relation, like 'leadphoto'.",
|
||||
max_length=200,
|
||||
null=True,
|
||||
verbose_name="relation type",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,35 +1,44 @@
|
|||
# Generated by Django 3.0.4 on 2020-03-06 10:50
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('categories', '0002_auto_20170217_1111'),
|
||||
("contenttypes", "0002_remove_content_type_name"),
|
||||
("categories", "0002_auto_20170217_1111"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='category',
|
||||
name='level',
|
||||
model_name="category",
|
||||
name="level",
|
||||
field=models.PositiveIntegerField(editable=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='category',
|
||||
name='lft',
|
||||
model_name="category",
|
||||
name="lft",
|
||||
field=models.PositiveIntegerField(editable=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='category',
|
||||
name='rght',
|
||||
model_name="category",
|
||||
name="rght",
|
||||
field=models.PositiveIntegerField(editable=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='categoryrelation',
|
||||
name='content_type',
|
||||
field=models.ForeignKey(limit_choices_to=models.Q(models.Q(('app_label', 'simpletext'), ('model', 'simpletext')), models.Q(('app_label', 'flatpages'), ('model', 'flatpage')), _connector='OR'), on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType', verbose_name='content type'),
|
||||
model_name="categoryrelation",
|
||||
name="content_type",
|
||||
field=models.ForeignKey(
|
||||
limit_choices_to=models.Q(
|
||||
models.Q(("app_label", "simpletext"), ("model", "simpletext")),
|
||||
models.Q(("app_label", "flatpages"), ("model", "flatpage")),
|
||||
_connector="OR",
|
||||
),
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="contenttypes.ContentType",
|
||||
verbose_name="content type",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,20 +1,22 @@
|
|||
# Generated by Django 3.0.6 on 2020-05-17 18:32
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('categories', '0003_auto_20200306_1050'),
|
||||
("contenttypes", "0002_remove_content_type_name"),
|
||||
("categories", "0003_auto_20200306_1050"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='categoryrelation',
|
||||
name='content_type',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType', verbose_name='content type'),
|
||||
model_name="categoryrelation",
|
||||
name="content_type",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="contenttypes.ContentType", verbose_name="content type"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,86 +1,95 @@
|
|||
from django.core.files.images import get_image_dimensions
|
||||
from django.urls import reverse
|
||||
from django.db import models
|
||||
from django.utils.encoding import force_text
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
"""Category models."""
|
||||
from functools import reduce
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.files.images import get_image_dimensions
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
try:
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
except ImportError:
|
||||
from django.contrib.contenttypes.generic import GenericForeignKey
|
||||
from django.core.files.storage import get_storage_class
|
||||
|
||||
from django.core.files.storage import get_storage_class
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .settings import (RELATION_MODELS, RELATIONS, THUMBNAIL_UPLOAD_PATH, THUMBNAIL_STORAGE)
|
||||
|
||||
from .base import CategoryBase
|
||||
from .settings import (
|
||||
RELATION_MODELS,
|
||||
RELATIONS,
|
||||
THUMBNAIL_STORAGE,
|
||||
THUMBNAIL_UPLOAD_PATH,
|
||||
)
|
||||
|
||||
STORAGE = get_storage_class(THUMBNAIL_STORAGE)
|
||||
|
||||
|
||||
class Category(CategoryBase):
|
||||
"""A basic category model."""
|
||||
|
||||
thumbnail = models.FileField(
|
||||
upload_to=THUMBNAIL_UPLOAD_PATH,
|
||||
null=True, blank=True,
|
||||
storage=STORAGE(),)
|
||||
null=True,
|
||||
blank=True,
|
||||
storage=STORAGE(),
|
||||
)
|
||||
thumbnail_width = models.IntegerField(blank=True, null=True)
|
||||
thumbnail_height = models.IntegerField(blank=True, null=True)
|
||||
order = models.IntegerField(default=0)
|
||||
alternate_title = models.CharField(
|
||||
blank=True,
|
||||
default="",
|
||||
max_length=100,
|
||||
help_text="An alternative title to use on pages with this category.")
|
||||
blank=True, default="", max_length=100, help_text="An alternative title to use on pages with this category."
|
||||
)
|
||||
alternate_url = models.CharField(
|
||||
blank=True,
|
||||
max_length=200,
|
||||
help_text="An alternative URL to use instead of the one derived from "
|
||||
"the category hierarchy.")
|
||||
help_text="An alternative URL to use instead of the one derived from " "the category hierarchy.",
|
||||
)
|
||||
description = models.TextField(blank=True, null=True)
|
||||
meta_keywords = models.CharField(
|
||||
blank=True,
|
||||
default="",
|
||||
max_length=255,
|
||||
help_text="Comma-separated keywords for search engines.")
|
||||
blank=True, default="", max_length=255, help_text="Comma-separated keywords for search engines."
|
||||
)
|
||||
meta_extra = models.TextField(
|
||||
blank=True,
|
||||
default="",
|
||||
help_text="(Advanced) Any additional HTML to be placed verbatim "
|
||||
"in the <head>")
|
||||
blank=True, default="", help_text="(Advanced) Any additional HTML to be placed verbatim " "in the <head>"
|
||||
)
|
||||
|
||||
@property
|
||||
def short_title(self):
|
||||
"""Return the name."""
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""Return a path"""
|
||||
"""Return a path."""
|
||||
from django.urls import NoReverseMatch
|
||||
|
||||
if self.alternate_url:
|
||||
return self.alternate_url
|
||||
try:
|
||||
prefix = reverse('categories_tree_list')
|
||||
prefix = reverse("categories_tree_list")
|
||||
except NoReverseMatch:
|
||||
prefix = '/'
|
||||
ancestors = list(self.get_ancestors()) + [self, ]
|
||||
return prefix + '/'.join([force_text(i.slug) for i in ancestors]) + '/'
|
||||
prefix = "/"
|
||||
ancestors = list(self.get_ancestors()) + [
|
||||
self,
|
||||
]
|
||||
return prefix + "/".join([force_text(i.slug) for i in ancestors]) + "/"
|
||||
|
||||
if RELATION_MODELS:
|
||||
|
||||
def get_related_content_type(self, content_type):
|
||||
"""
|
||||
Get all related items of the specified content type
|
||||
Get all related items of the specified content type.
|
||||
"""
|
||||
return self.categoryrelation_set.filter(
|
||||
content_type__name=content_type)
|
||||
return self.categoryrelation_set.filter(content_type__name=content_type)
|
||||
|
||||
def get_relation_type(self, relation_type):
|
||||
"""
|
||||
Get all relations of the specified relation type
|
||||
Get all relations of the specified relation type.
|
||||
"""
|
||||
return self.categoryrelation_set.filter(relation_type=relation_type)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Save the category."""
|
||||
if self.thumbnail:
|
||||
width, height = get_image_dimensions(self.thumbnail.file)
|
||||
else:
|
||||
|
|
@ -92,11 +101,11 @@ class Category(CategoryBase):
|
|||
super(Category, self).save(*args, **kwargs)
|
||||
|
||||
class Meta(CategoryBase.Meta):
|
||||
verbose_name = _('category')
|
||||
verbose_name_plural = _('categories')
|
||||
verbose_name = _("category")
|
||||
verbose_name_plural = _("categories")
|
||||
|
||||
class MPTTMeta:
|
||||
order_insertion_by = ('order', 'name')
|
||||
order_insertion_by = ("order", "name")
|
||||
|
||||
|
||||
if RELATIONS:
|
||||
|
|
@ -106,6 +115,8 @@ else:
|
|||
|
||||
|
||||
class CategoryRelationManager(models.Manager):
|
||||
"""Custom access functions for category relations."""
|
||||
|
||||
def get_content_type(self, content_type):
|
||||
"""
|
||||
Get all the items of the given content type related to this item.
|
||||
|
|
@ -122,18 +133,24 @@ class CategoryRelationManager(models.Manager):
|
|||
|
||||
|
||||
class CategoryRelation(models.Model):
|
||||
"""Related category item"""
|
||||
category = models.ForeignKey(Category, verbose_name=_('category'), on_delete=models.CASCADE)
|
||||
"""Related category item."""
|
||||
|
||||
category = models.ForeignKey(Category, verbose_name=_("category"), on_delete=models.CASCADE)
|
||||
content_type = models.ForeignKey(
|
||||
ContentType, on_delete=models.CASCADE, limit_choices_to=CATEGORY_RELATION_LIMITS, verbose_name=_('content type'))
|
||||
object_id = models.PositiveIntegerField(verbose_name=_('object id'))
|
||||
content_object = GenericForeignKey('content_type', 'object_id')
|
||||
ContentType,
|
||||
on_delete=models.CASCADE,
|
||||
limit_choices_to=CATEGORY_RELATION_LIMITS,
|
||||
verbose_name=_("content type"),
|
||||
)
|
||||
object_id = models.PositiveIntegerField(verbose_name=_("object id"))
|
||||
content_object = GenericForeignKey("content_type", "object_id")
|
||||
relation_type = models.CharField(
|
||||
verbose_name=_('relation type'),
|
||||
verbose_name=_("relation type"),
|
||||
max_length=200,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_("A generic text field to tag a relation, like 'leadphoto'."))
|
||||
help_text=_("A generic text field to tag a relation, like 'leadphoto'."),
|
||||
)
|
||||
|
||||
objects = CategoryRelationManager()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,44 +1,60 @@
|
|||
"""
|
||||
These functions handle the adding of fields to other models
|
||||
These functions handle the adding of fields to other models.
|
||||
"""
|
||||
from django.db.models import ForeignKey, ManyToManyField, CASCADE
|
||||
from django.core.exceptions import FieldDoesNotExist
|
||||
from . import fields
|
||||
from typing import Optional, Type, Union
|
||||
|
||||
import collections
|
||||
|
||||
from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
|
||||
from django.db.models import CASCADE, ForeignKey, ManyToManyField
|
||||
|
||||
# from settings import self._field_registry, self._model_registry
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
from . import fields
|
||||
|
||||
FIELD_TYPES = {
|
||||
'ForeignKey': ForeignKey,
|
||||
'ManyToManyField': ManyToManyField,
|
||||
"ForeignKey": ForeignKey,
|
||||
"ManyToManyField": ManyToManyField,
|
||||
}
|
||||
|
||||
|
||||
class Registry(object):
|
||||
"""Keeps track of fields and models registered."""
|
||||
|
||||
def __init__(self):
|
||||
self._field_registry = {}
|
||||
self._model_registry = {}
|
||||
|
||||
def register_model(self, app, model_name, field_type, field_definitions):
|
||||
def register_model(
|
||||
self, app: str, model_name, field_type: str, field_definitions: Union[str, collections.Iterable]
|
||||
):
|
||||
"""
|
||||
Process for Django 1.7 +
|
||||
app: app name/label
|
||||
model_name: name of the model
|
||||
field_definitions: a string, tuple or list of field configurations
|
||||
field_type: either 'ForeignKey' or 'ManyToManyField'
|
||||
Registration process for Django 1.7+.
|
||||
|
||||
Args:
|
||||
app: app name/label
|
||||
model_name: name of the model
|
||||
field_definitions: a string, tuple or list of field configurations
|
||||
field_type: either 'ForeignKey' or 'ManyToManyField'
|
||||
|
||||
Raises:
|
||||
ImproperlyConfigured: For incorrect parameter types or missing model.
|
||||
"""
|
||||
from django.apps import apps
|
||||
import collections
|
||||
|
||||
from django.apps import apps
|
||||
|
||||
app_label = app
|
||||
|
||||
if isinstance(field_definitions, str):
|
||||
field_definitions = [field_definitions]
|
||||
elif not isinstance(field_definitions, collections.Iterable):
|
||||
raise ImproperlyConfigured(_('Field configuration for %(app)s should be a string or iterable') % {'app': app})
|
||||
raise ImproperlyConfigured(
|
||||
_("Field configuration for %(app)s should be a string or iterable") % {"app": app}
|
||||
)
|
||||
|
||||
if field_type not in ('ForeignKey', 'ManyToManyField'):
|
||||
if field_type not in ("ForeignKey", "ManyToManyField"):
|
||||
raise ImproperlyConfigured(_('`field_type` must be either `"ForeignKey"` or `"ManyToManyField"`.'))
|
||||
|
||||
try:
|
||||
|
|
@ -55,30 +71,31 @@ class Registry(object):
|
|||
if model not in self._model_registry[app_label]:
|
||||
self._model_registry[app_label].append(model)
|
||||
except LookupError:
|
||||
raise ImproperlyConfigured('Model "%(model)s" doesn\'t exist in app "%(app)s".' % {'model': model_name, 'app': app})
|
||||
raise ImproperlyConfigured(
|
||||
'Model "%(model)s" doesn\'t exist in app "%(app)s".' % {"model": model_name, "app": app}
|
||||
)
|
||||
|
||||
if not isinstance(field_definitions, (tuple, list)):
|
||||
field_definitions = [field_definitions]
|
||||
|
||||
for fld in field_definitions:
|
||||
extra_params = {'to': 'categories.Category', 'blank': True}
|
||||
if field_type != 'ManyToManyField':
|
||||
extra_params['on_delete'] = CASCADE
|
||||
extra_params['null'] = True
|
||||
extra_params = {"to": "categories.Category", "blank": True}
|
||||
if field_type != "ManyToManyField":
|
||||
extra_params["on_delete"] = CASCADE
|
||||
extra_params["null"] = True
|
||||
if isinstance(fld, str):
|
||||
field_name = fld
|
||||
elif isinstance(fld, dict):
|
||||
if 'name' in fld:
|
||||
field_name = fld.pop('name')
|
||||
if "name" in fld:
|
||||
field_name = fld.pop("name")
|
||||
else:
|
||||
continue
|
||||
extra_params.update(fld)
|
||||
else:
|
||||
raise ImproperlyConfigured(
|
||||
_("%(settings)s doesn't recognize the value of %(app)s.%(model)s") % {
|
||||
'settings': 'CATEGORY_SETTINGS',
|
||||
'app': app,
|
||||
'model': model_name})
|
||||
_("%(settings)s doesn't recognize the value of %(app)s.%(model)s")
|
||||
% {"settings": "CATEGORY_SETTINGS", "app": app, "model": model_name}
|
||||
)
|
||||
registry_name = ".".join([app_label, model_name.lower(), field_name])
|
||||
if registry_name in self._field_registry:
|
||||
continue
|
||||
|
|
@ -89,14 +106,26 @@ class Registry(object):
|
|||
self._field_registry[registry_name] = FIELD_TYPES[field_type](**extra_params)
|
||||
self._field_registry[registry_name].contribute_to_class(model, field_name)
|
||||
|
||||
def register_m2m(self, model, field_name='categories', extra_params={}):
|
||||
def register_m2m(self, model, field_name: str = "categories", extra_params: Optional[dict] = None):
|
||||
"""Register a field name to the model as a many to many field."""
|
||||
extra_params = extra_params or {}
|
||||
return self._register(model, field_name, extra_params, fields.CategoryM2MField)
|
||||
|
||||
def register_fk(self, model, field_name='category', extra_params={}):
|
||||
def register_fk(self, model, field_name: str = "category", extra_params: Optional[dict] = None):
|
||||
"""Register a field name to the model as a foreign key."""
|
||||
extra_params = extra_params or {}
|
||||
return self._register(model, field_name, extra_params, fields.CategoryFKField)
|
||||
|
||||
def _register(self, model, field_name, extra_params={}, field=fields.CategoryFKField):
|
||||
def _register(
|
||||
self,
|
||||
model,
|
||||
field_name: str,
|
||||
extra_params: Optional[dict] = None,
|
||||
field: Type = fields.CategoryFKField,
|
||||
):
|
||||
"""Does the heavy lifting for registering a field to a model."""
|
||||
app_label = model._meta.app_label
|
||||
extra_params = extra_params or {}
|
||||
registry_name = ".".join((app_label, model.__name__, field_name)).lower()
|
||||
|
||||
if registry_name in self._field_registry:
|
||||
|
|
@ -118,30 +147,34 @@ registry = Registry()
|
|||
|
||||
def _process_registry(registry, call_func):
|
||||
"""
|
||||
Given a dictionary, and a registration function, process the registry
|
||||
Given a dictionary, and a registration function, process the registry.
|
||||
"""
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.apps import apps
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
for key, value in list(registry.items()):
|
||||
model = apps.get_model(*key.split('.'))
|
||||
model = apps.get_model(*key.split("."))
|
||||
if model is None:
|
||||
raise ImproperlyConfigured(_('%(key)s is not a model') % {'key': key})
|
||||
raise ImproperlyConfigured(_("%(key)s is not a model") % {"key": key})
|
||||
if isinstance(value, (tuple, list)):
|
||||
for item in value:
|
||||
if isinstance(item, str):
|
||||
call_func(model, item)
|
||||
elif isinstance(item, dict):
|
||||
field_name = item.pop('name')
|
||||
field_name = item.pop("name")
|
||||
call_func(model, field_name, extra_params=item)
|
||||
else:
|
||||
raise ImproperlyConfigured(_("%(settings)s doesn't recognize the value of %(key)s") %
|
||||
{'settings': 'CATEGORY_SETTINGS', 'key': key})
|
||||
raise ImproperlyConfigured(
|
||||
_("%(settings)s doesn't recognize the value of %(key)s")
|
||||
% {"settings": "CATEGORY_SETTINGS", "key": key}
|
||||
)
|
||||
elif isinstance(value, str):
|
||||
call_func(model, value)
|
||||
elif isinstance(value, dict):
|
||||
field_name = value.pop('name')
|
||||
field_name = value.pop("name")
|
||||
call_func(model, field_name, extra_params=value)
|
||||
else:
|
||||
raise ImproperlyConfigured(_("%(settings)s doesn't recognize the value of %(key)s") %
|
||||
{'settings': 'CATEGORY_SETTINGS', 'key': key})
|
||||
raise ImproperlyConfigured(
|
||||
_("%(settings)s doesn't recognize the value of %(key)s")
|
||||
% {"settings": "CATEGORY_SETTINGS", "key": key}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,42 +1,47 @@
|
|||
"""Manages settings for the categories application."""
|
||||
import collections
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
import collections
|
||||
|
||||
DEFAULT_SETTINGS = {
|
||||
'ALLOW_SLUG_CHANGE': False,
|
||||
'M2M_REGISTRY': {},
|
||||
'FK_REGISTRY': {},
|
||||
'THUMBNAIL_UPLOAD_PATH': 'uploads/categories/thumbnails',
|
||||
'THUMBNAIL_STORAGE': settings.DEFAULT_FILE_STORAGE,
|
||||
'JAVASCRIPT_URL': getattr(settings, 'STATIC_URL', settings.MEDIA_URL) + 'js/',
|
||||
'SLUG_TRANSLITERATOR': '',
|
||||
'REGISTER_ADMIN': True,
|
||||
'RELATION_MODELS': [],
|
||||
"ALLOW_SLUG_CHANGE": False,
|
||||
"M2M_REGISTRY": {},
|
||||
"FK_REGISTRY": {},
|
||||
"THUMBNAIL_UPLOAD_PATH": "uploads/categories/thumbnails",
|
||||
"THUMBNAIL_STORAGE": settings.DEFAULT_FILE_STORAGE,
|
||||
"JAVASCRIPT_URL": getattr(settings, "STATIC_URL", settings.MEDIA_URL) + "js/",
|
||||
"SLUG_TRANSLITERATOR": "",
|
||||
"REGISTER_ADMIN": True,
|
||||
"RELATION_MODELS": [],
|
||||
}
|
||||
|
||||
DEFAULT_SETTINGS.update(getattr(settings, 'CATEGORIES_SETTINGS', {}))
|
||||
DEFAULT_SETTINGS.update(getattr(settings, "CATEGORIES_SETTINGS", {}))
|
||||
|
||||
if DEFAULT_SETTINGS['SLUG_TRANSLITERATOR']:
|
||||
if isinstance(DEFAULT_SETTINGS['SLUG_TRANSLITERATOR'], collections.Callable):
|
||||
if DEFAULT_SETTINGS["SLUG_TRANSLITERATOR"]:
|
||||
if isinstance(DEFAULT_SETTINGS["SLUG_TRANSLITERATOR"], collections.Callable):
|
||||
pass
|
||||
elif isinstance(DEFAULT_SETTINGS['SLUG_TRANSLITERATOR'], str):
|
||||
elif isinstance(DEFAULT_SETTINGS["SLUG_TRANSLITERATOR"], str):
|
||||
from django.utils.importlib import import_module
|
||||
bits = DEFAULT_SETTINGS['SLUG_TRANSLITERATOR'].split(".")
|
||||
|
||||
bits = DEFAULT_SETTINGS["SLUG_TRANSLITERATOR"].split(".")
|
||||
module = import_module(".".join(bits[:-1]))
|
||||
DEFAULT_SETTINGS['SLUG_TRANSLITERATOR'] = getattr(module, bits[-1])
|
||||
DEFAULT_SETTINGS["SLUG_TRANSLITERATOR"] = getattr(module, bits[-1])
|
||||
else:
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
raise ImproperlyConfigured(_('%(transliterator) must be a callable or a string.') %
|
||||
{'transliterator': 'SLUG_TRANSLITERATOR'})
|
||||
|
||||
raise ImproperlyConfigured(
|
||||
_("%(transliterator) must be a callable or a string.") % {"transliterator": "SLUG_TRANSLITERATOR"}
|
||||
)
|
||||
else:
|
||||
DEFAULT_SETTINGS['SLUG_TRANSLITERATOR'] = lambda x: x
|
||||
DEFAULT_SETTINGS["SLUG_TRANSLITERATOR"] = lambda x: x
|
||||
|
||||
|
||||
# Add all the keys/values to the module's namespace
|
||||
globals().update(DEFAULT_SETTINGS)
|
||||
|
||||
RELATIONS = [Q(app_label=al, model=m) for al, m in [x.split('.') for x in DEFAULT_SETTINGS['RELATION_MODELS']]]
|
||||
RELATIONS = [Q(app_label=al, model=m) for al, m in [x.split(".") for x in DEFAULT_SETTINGS["RELATION_MODELS"]]]
|
||||
|
||||
# The field registry keeps track of the individual fields created.
|
||||
# {'app.model.field': Field(**extra_params)}
|
||||
|
|
|
|||
|
|
@ -17,4 +17,4 @@ function showGenericRelatedObjectLookupPopup(triggeringLink, ctArray) {
|
|||
var win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes');
|
||||
win.focus();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,4 +8,4 @@
|
|||
{% else %}<a href="{{ node.get_absolute_url }}">{{ node.name }}</a>
|
||||
{% endifequal %}
|
||||
{% for level in structure.closed_levels %}</li></ul>{% endfor %}
|
||||
{% endfor %}</li></ul>
|
||||
{% endfor %}</li></ul>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
{% spaceless %}{% for item in category.get_ancestors %}
|
||||
<a href="{{ item.get_absolute_url }}">{{ item.name }}</a>{{ separator }}{% endfor %}{{ category.name }}
|
||||
{% endspaceless %}
|
||||
{% endspaceless %}
|
||||
|
|
|
|||
|
|
@ -14,4 +14,4 @@
|
|||
{% if category.parent %}<h3>Go up to <a href="{{ category.parent.get_absolute_url }}">{{ category.parent }}</a></h3>{% endif %}
|
||||
{% if category.description %}<p>{{ category.description }}</p>{% endif %}
|
||||
{% if category.children.count %}<h3>Subcategories</h3><ul>{% for child in category.children.all %}<li><a href="{{ child.get_absolute_url }}">{{ child }}</a></li>{% endfor %}</ul>{% endif %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -2,4 +2,4 @@
|
|||
{% block content %}
|
||||
<h2>Categories</h2>
|
||||
<ul>{% for category in object_list %}<li><a href="{{ category.get_absolute_url }}">{{ category }}</a></li>{% endfor %}</ul>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -8,4 +8,4 @@
|
|||
{% else %}<a href="{{ node.get_absolute_url }}">{{ node.name }}</a>
|
||||
{% endifequal %}
|
||||
{% for level in structure.closed_levels %}</li></ul>{% endfor %}
|
||||
{% endfor %}</li></ul>{% endspaceless %}
|
||||
{% endfor %}</li></ul>{% endspaceless %}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,20 @@
|
|||
"""Template tags for categories."""
|
||||
from typing import Any, Type, Union
|
||||
|
||||
from django import template
|
||||
from django.apps import apps
|
||||
from django.template import (Node, TemplateSyntaxError, VariableDoesNotExist)
|
||||
from django.template import Node, TemplateSyntaxError, VariableDoesNotExist
|
||||
from django.template.base import FilterExpression
|
||||
from six import string_types
|
||||
from mptt.templatetags.mptt_tags import (
|
||||
RecurseTreeNode,
|
||||
full_tree_for_model,
|
||||
tree_info,
|
||||
tree_path,
|
||||
)
|
||||
from mptt.utils import drilldown_tree_for_node
|
||||
|
||||
from categories.base import CategoryBase
|
||||
from categories.models import Category
|
||||
from mptt.utils import drilldown_tree_for_node
|
||||
from mptt.templatetags.mptt_tags import (tree_path, tree_info, RecurseTreeNode,
|
||||
full_tree_for_model)
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
|
@ -16,7 +23,8 @@ register.filter(tree_info)
|
|||
register.tag("full_tree_for_category", full_tree_for_model)
|
||||
|
||||
|
||||
def resolve(var, context):
|
||||
def resolve(var: Any, context: dict) -> Any:
|
||||
"""Aggressively resolve a variable."""
|
||||
try:
|
||||
return var.resolve(context)
|
||||
except VariableDoesNotExist:
|
||||
|
|
@ -28,10 +36,11 @@ def resolve(var, context):
|
|||
|
||||
def get_cat_model(model):
|
||||
"""
|
||||
Return a class from a string or class
|
||||
Return a class from a string or class.
|
||||
"""
|
||||
model_class = None
|
||||
try:
|
||||
if isinstance(model, string_types):
|
||||
if isinstance(model, str):
|
||||
model_class = apps.get_model(*model.split("."))
|
||||
elif issubclass(model, CategoryBase):
|
||||
model_class = model
|
||||
|
|
@ -42,15 +51,22 @@ def get_cat_model(model):
|
|||
return model_class
|
||||
|
||||
|
||||
def get_category(category_string, model=Category):
|
||||
def get_category(category_string, model: Union[str, Type] = Category) -> Any:
|
||||
"""
|
||||
Convert a string, including a path, and return the Category object
|
||||
Convert a string, including a path, and return the Category object.
|
||||
|
||||
Args:
|
||||
category_string: The name or path of the category
|
||||
model: The name of or Category model to search in
|
||||
|
||||
Returns:
|
||||
The found category object or None if no category was found
|
||||
"""
|
||||
model_class = get_cat_model(model)
|
||||
category = str(category_string).strip("'\"")
|
||||
category = category.strip('/')
|
||||
category = category.strip("/")
|
||||
|
||||
cat_list = category.split('/')
|
||||
cat_list = category.split("/")
|
||||
if len(cat_list) == 0:
|
||||
return None
|
||||
try:
|
||||
|
|
@ -61,176 +77,217 @@ def get_category(category_string, model=Category):
|
|||
# if the parent matches the parent passed in the string
|
||||
if len(categories) == 1:
|
||||
return categories[0]
|
||||
else:
|
||||
for item in categories:
|
||||
if item.parent.name == cat_list[-2]:
|
||||
return item
|
||||
|
||||
for item in categories:
|
||||
if item.parent.name == cat_list[-2]:
|
||||
return item
|
||||
except model_class.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
class CategoryDrillDownNode(template.Node):
|
||||
"""A category drill down template node."""
|
||||
|
||||
def __init__(self, category, varname, model):
|
||||
self.category = category
|
||||
self.varname = varname
|
||||
self.model = model
|
||||
|
||||
def render(self, context):
|
||||
"""Render this node."""
|
||||
category = resolve(self.category, context)
|
||||
if isinstance(category, CategoryBase):
|
||||
cat = category
|
||||
else:
|
||||
cat = get_category(category, self.model)
|
||||
try:
|
||||
if cat is not None:
|
||||
context[self.varname] = drilldown_tree_for_node(cat)
|
||||
else:
|
||||
context[self.varname] = []
|
||||
context[self.varname] = drilldown_tree_for_node(cat) if cat is not None else []
|
||||
except Exception:
|
||||
context[self.varname] = []
|
||||
return ''
|
||||
return ""
|
||||
|
||||
|
||||
@register.tag
|
||||
def get_category_drilldown(parser, token):
|
||||
"""
|
||||
Retrieves the specified category, its ancestors and its immediate children
|
||||
as an iterable.
|
||||
Retrieves the specified category, its ancestors and its immediate children as an Iterable.
|
||||
|
||||
Syntax::
|
||||
The basic syntax::
|
||||
|
||||
{% get_category_drilldown "category name" [using "app.Model"] as varname %}
|
||||
|
||||
Example::
|
||||
Example:
|
||||
Using a string for the category name::
|
||||
|
||||
{% get_category_drilldown "/Grandparent/Parent" [using "app.Model"] as family %}
|
||||
{% get_category_drilldown "/Grandparent/Parent" as family %}
|
||||
|
||||
or ::
|
||||
or using a variable for the category::
|
||||
|
||||
{% get_category_drilldown category_obj as family %}
|
||||
{% get_category_drilldown category_obj as family %}
|
||||
|
||||
Sets family to::
|
||||
Sets family to::
|
||||
|
||||
Grandparent, Parent, Child 1, Child 2, Child n
|
||||
Grandparent, Parent, Child 1, Child 2, Child n
|
||||
|
||||
Args:
|
||||
parser: The Django template parser.
|
||||
token: The tag contents
|
||||
|
||||
Returns:
|
||||
The recursive tree node.
|
||||
|
||||
Raises:
|
||||
TemplateSyntaxError: If the tag is malformed.
|
||||
"""
|
||||
bits = token.split_contents()
|
||||
error_str = '%(tagname)s tag should be in the format {%% %(tagname)s ' \
|
||||
'"category name" [using "app.Model"] as varname %%} or ' \
|
||||
'{%% %(tagname)s category_obj as varname %%}.'
|
||||
error_str = (
|
||||
"%(tagname)s tag should be in the format {%% %(tagname)s "
|
||||
'"category name" [using "app.Model"] as varname %%} or '
|
||||
"{%% %(tagname)s category_obj as varname %%}."
|
||||
)
|
||||
varname = model = ""
|
||||
if len(bits) == 4:
|
||||
if bits[2] != 'as':
|
||||
raise template.TemplateSyntaxError(error_str % {'tagname': bits[0]})
|
||||
if bits[2] == 'as':
|
||||
if bits[2] != "as":
|
||||
raise template.TemplateSyntaxError(error_str % {"tagname": bits[0]})
|
||||
if bits[2] == "as":
|
||||
varname = bits[3].strip("'\"")
|
||||
model = "categories.category"
|
||||
if len(bits) == 6:
|
||||
if bits[2] not in ('using', 'as') or bits[4] not in ('using', 'as'):
|
||||
raise template.TemplateSyntaxError(error_str % {'tagname': bits[0]})
|
||||
if bits[2] == 'as':
|
||||
elif len(bits) == 6:
|
||||
if bits[2] not in ("using", "as") or bits[4] not in ("using", "as"):
|
||||
raise template.TemplateSyntaxError(error_str % {"tagname": bits[0]})
|
||||
if bits[2] == "as":
|
||||
varname = bits[3].strip("'\"")
|
||||
model = bits[5].strip("'\"")
|
||||
if bits[2] == 'using':
|
||||
if bits[2] == "using":
|
||||
varname = bits[5].strip("'\"")
|
||||
model = bits[3].strip("'\"")
|
||||
else:
|
||||
raise template.TemplateSyntaxError(error_str % {"tagname": bits[0]})
|
||||
category = FilterExpression(bits[1], parser)
|
||||
return CategoryDrillDownNode(category, varname, model)
|
||||
|
||||
|
||||
@register.inclusion_tag('categories/breadcrumbs.html')
|
||||
def breadcrumbs(category_string, separator=' > ', using='categories.category'):
|
||||
@register.inclusion_tag("categories/breadcrumbs.html")
|
||||
def breadcrumbs(category_string, separator=" > ", using="categories.category"):
|
||||
"""
|
||||
{% breadcrumbs category separator="::" using="categories.category" %}
|
||||
Render breadcrumbs, using the ``categories/breadcrumbs.html`` template.
|
||||
|
||||
Render breadcrumbs, using the ``categories/breadcrumbs.html`` template,
|
||||
using the optional ``separator`` argument.
|
||||
The basic syntax::
|
||||
|
||||
{% breadcrumbs "category" [separator=" > "] [using="categories.category"] %}
|
||||
|
||||
Args:
|
||||
category_string: A variable reference to or the name of the category to display
|
||||
separator: The string to separate the categories
|
||||
using: A variable reference to or the name of the category model to search for.
|
||||
|
||||
Returns:
|
||||
The inclusion template
|
||||
"""
|
||||
cat = get_category(category_string, using)
|
||||
|
||||
return {'category': cat, 'separator': separator}
|
||||
return {"category": cat, "separator": separator}
|
||||
|
||||
|
||||
@register.inclusion_tag('categories/ul_tree.html')
|
||||
def display_drilldown_as_ul(category, using='categories.Category'):
|
||||
@register.inclusion_tag("categories/ul_tree.html")
|
||||
def display_drilldown_as_ul(category, using="categories.Category"):
|
||||
"""
|
||||
Render the category with ancestors and children using the
|
||||
``categories/ul_tree.html`` template.
|
||||
Render the category with ancestors and children using the ``categories/ul_tree.html`` template.
|
||||
|
||||
Example::
|
||||
Example:
|
||||
The template code of::
|
||||
|
||||
{% display_drilldown_as_ul "/Grandparent/Parent" %}
|
||||
{% display_drilldown_as_ul "/Grandparent/Parent" %}
|
||||
|
||||
or ::
|
||||
or::
|
||||
|
||||
{% display_drilldown_as_ul category_obj %}
|
||||
{% display_drilldown_as_ul category_obj %}
|
||||
|
||||
Returns::
|
||||
might render as::
|
||||
|
||||
<ul>
|
||||
<li><a href="/categories/">Top</a>
|
||||
<ul>
|
||||
<li><a href="/categories/grandparent/">Grandparent</a>
|
||||
<ul>
|
||||
<li><a href="/categories/grandparent/parent/">Parent</a>
|
||||
<li><a href="/categories/">Top</a>
|
||||
<ul>
|
||||
<li><a href="/categories/grandparent/parent/child1">Child1</a></li>
|
||||
<li><a href="/categories/grandparent/parent/child2">Child2</a></li>
|
||||
<li><a href="/categories/grandparent/parent/child3">Child3</a></li>
|
||||
<li><a href="/categories/grandparent/">Grandparent</a>
|
||||
<ul>
|
||||
<li><a href="/categories/grandparent/parent/">Parent</a>
|
||||
<ul>
|
||||
<li><a href="/categories/grandparent/parent/child1">Child1</a></li>
|
||||
<li><a href="/categories/grandparent/parent/child2">Child2</a></li>
|
||||
<li><a href="/categories/grandparent/parent/child3">Child3</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
Args:
|
||||
category: A variable reference to or the name of the category to display
|
||||
using: A variable reference to or the name of the category model to search for.
|
||||
|
||||
Returns:
|
||||
The inclusion template
|
||||
"""
|
||||
cat = get_category(category, using)
|
||||
if cat is None:
|
||||
return {'category': cat, 'path': []}
|
||||
return {"category": cat, "path": []}
|
||||
else:
|
||||
return {'category': cat, 'path': drilldown_tree_for_node(cat)}
|
||||
return {"category": cat, "path": drilldown_tree_for_node(cat)}
|
||||
|
||||
|
||||
@register.inclusion_tag('categories/ul_tree.html')
|
||||
def display_path_as_ul(category, using='categories.Category'):
|
||||
@register.inclusion_tag("categories/ul_tree.html")
|
||||
def display_path_as_ul(category, using="categories.Category"):
|
||||
"""
|
||||
Render the category with ancestors, but no children using the
|
||||
``categories/ul_tree.html`` template.
|
||||
Render the category with ancestors, but no children using the ``categories/ul_tree.html`` template.
|
||||
|
||||
Example::
|
||||
Examples:
|
||||
The template code of::
|
||||
|
||||
{% display_path_as_ul "/Grandparent/Parent" %}
|
||||
{% display_path_as_ul "/Grandparent/Parent" %}
|
||||
|
||||
or ::
|
||||
or::
|
||||
|
||||
{% display_path_as_ul category_obj %}
|
||||
{% display_path_as_ul category_obj %}
|
||||
|
||||
Returns::
|
||||
might render as::
|
||||
|
||||
<ul>
|
||||
<li><a href="/categories/">Top</a>
|
||||
<ul>
|
||||
<li><a href="/categories/grandparent/">Grandparent</a></li>
|
||||
<li><a href="/categories/">Top</a>
|
||||
<ul>
|
||||
<li><a href="/categories/grandparent/">Grandparent</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
Args:
|
||||
category: A variable reference to or the name of the category to display
|
||||
using: A variable reference to or the name of the category model to search for.
|
||||
|
||||
Returns:
|
||||
The inclusion template
|
||||
"""
|
||||
if isinstance(category, CategoryBase):
|
||||
cat = category
|
||||
else:
|
||||
cat = get_category(category)
|
||||
cat = get_category(category, using)
|
||||
|
||||
return {'category': cat, 'path': cat.get_ancestors() or []}
|
||||
return {"category": cat, "path": cat.get_ancestors() or []}
|
||||
|
||||
|
||||
class TopLevelCategoriesNode(template.Node):
|
||||
"""Template node for the top level categories."""
|
||||
|
||||
def __init__(self, varname, model):
|
||||
self.varname = varname
|
||||
self.model = model
|
||||
|
||||
def render(self, context):
|
||||
"""Render this node."""
|
||||
model = get_cat_model(self.model)
|
||||
context[self.varname] = model.objects.filter(parent=None).order_by('name')
|
||||
return ''
|
||||
context[self.varname] = model.objects.filter(parent=None).order_by("name")
|
||||
return ""
|
||||
|
||||
|
||||
@register.tag
|
||||
|
|
@ -238,51 +295,63 @@ def get_top_level_categories(parser, token):
|
|||
"""
|
||||
Retrieves an alphabetical list of all the categories that have no parents.
|
||||
|
||||
Syntax::
|
||||
The basic syntax is::
|
||||
|
||||
{% get_top_level_categories [using "app.Model"] as categories %}
|
||||
|
||||
Returns an list of categories [<category>, <category>, <category, ...]
|
||||
Args:
|
||||
parser: The Django template parser.
|
||||
token: The tag contents
|
||||
|
||||
Returns:
|
||||
Returns a list of categories [<category>, <category>, <category, ...]
|
||||
|
||||
Raises:
|
||||
TemplateSyntaxError: If a queryset isn't provided
|
||||
"""
|
||||
bits = token.split_contents()
|
||||
usage = 'Usage: {%% %s [using "app.Model"] as <variable> %%}' % bits[0]
|
||||
|
||||
if len(bits) == 3:
|
||||
if bits[1] != 'as':
|
||||
if bits[1] != "as":
|
||||
raise template.TemplateSyntaxError(usage)
|
||||
varname = bits[2]
|
||||
model = "categories.category"
|
||||
elif len(bits) == 5:
|
||||
if bits[1] not in ('as', 'using') and bits[3] not in ('as', 'using'):
|
||||
if bits[1] not in ("as", "using") and bits[3] not in ("as", "using"):
|
||||
raise template.TemplateSyntaxError(usage)
|
||||
if bits[1] == 'using':
|
||||
if bits[1] == "using":
|
||||
model = bits[2].strip("'\"")
|
||||
varname = bits[4].strip("'\"")
|
||||
else:
|
||||
model = bits[4].strip("'\"")
|
||||
varname = bits[2].strip("'\"")
|
||||
else:
|
||||
raise template.TemplateSyntaxError(usage)
|
||||
|
||||
return TopLevelCategoriesNode(varname, model)
|
||||
|
||||
|
||||
def get_latest_objects_by_category(category, app_label, model_name, set_name, date_field='pub_date', num=15):
|
||||
def get_latest_objects_by_category(category, app_label, model_name, set_name, date_field="pub_date", num=15):
|
||||
"""Return a queryset of the latest objects of ``app_label.model_name`` in given category."""
|
||||
m = apps.get_model(app_label, model_name)
|
||||
if not isinstance(category, CategoryBase):
|
||||
category = Category.objects.get(slug=str(category))
|
||||
children = category.children.all()
|
||||
ids = []
|
||||
for cat in list(children) + [category]:
|
||||
if hasattr(cat, '%s_set' % set_name):
|
||||
ids.extend([x.pk for x in getattr(cat, '%s_set' % set_name).all()[:num]])
|
||||
if hasattr(cat, "%s_set" % set_name):
|
||||
ids.extend([x.pk for x in getattr(cat, "%s_set" % set_name).all()[:num]])
|
||||
|
||||
return m.objects.filter(pk__in=ids).order_by('-%s' % date_field)[:num]
|
||||
return m.objects.filter(pk__in=ids).order_by("-%s" % date_field)[:num]
|
||||
|
||||
|
||||
class LatestObjectsNode(Node):
|
||||
def __init__(self, var_name, category, app_label, model_name, set_name,
|
||||
date_field='pub_date', num=15):
|
||||
"""
|
||||
Get latest objects of app_label.model_name
|
||||
"""
|
||||
"""
|
||||
Get latest objects of app_label.model_name.
|
||||
"""
|
||||
|
||||
def __init__(self, var_name, category, app_label, model_name, set_name, date_field="pub_date", num=15):
|
||||
self.category = category
|
||||
self.app_label = app_label
|
||||
self.model_name = model_name
|
||||
|
|
@ -293,7 +362,7 @@ class LatestObjectsNode(Node):
|
|||
|
||||
def render(self, context):
|
||||
"""
|
||||
Render this sucker
|
||||
Render this sucker.
|
||||
"""
|
||||
category = resolve(self.category, context)
|
||||
app_label = resolve(self.app_label, context)
|
||||
|
|
@ -305,19 +374,35 @@ class LatestObjectsNode(Node):
|
|||
result = get_latest_objects_by_category(category, app_label, model_name, set_name, date_field, num)
|
||||
context[self.var_name] = result
|
||||
|
||||
return ''
|
||||
return ""
|
||||
|
||||
|
||||
def do_get_latest_objects_by_category(parser, token):
|
||||
"""
|
||||
Get the latest objects by category
|
||||
Get the latest objects by category.
|
||||
|
||||
{% get_latest_objects_by_category category app_name model_name set_name [date_field] [number] as [var_name] %}
|
||||
The basic syntax is::
|
||||
|
||||
{% get_latest_objects_by_category category app_name model_name set_name [date_field] [number] as [var_name] %}
|
||||
|
||||
|
||||
Args:
|
||||
parser: The Django template parser.
|
||||
token: The tag contents
|
||||
|
||||
Returns:
|
||||
The latet objects node.
|
||||
|
||||
Raises:
|
||||
TemplateSyntaxError: If the tag is malformed
|
||||
"""
|
||||
proper_form = "{% get_latest_objects_by_category category app_name model_name set_name [date_field] [number] as [var_name] %}"
|
||||
proper_form = (
|
||||
"{% get_latest_objects_by_category category app_name model_name set_name "
|
||||
"[date_field] [number] as [var_name] %}"
|
||||
)
|
||||
bits = token.split_contents()
|
||||
|
||||
if bits[-2] != 'as':
|
||||
if bits[-2] != "as":
|
||||
raise TemplateSyntaxError("%s tag shoud be in the form: %s" % (bits[0], proper_form))
|
||||
if len(bits) < 7:
|
||||
raise TemplateSyntaxError("%s tag shoud be in the form: %s" % (bits[0], proper_form))
|
||||
|
|
@ -328,11 +413,11 @@ def do_get_latest_objects_by_category(parser, token):
|
|||
model_name = FilterExpression(bits[3], parser)
|
||||
set_name = FilterExpression(bits[4], parser)
|
||||
var_name = bits[-1]
|
||||
if bits[5] != 'as':
|
||||
if bits[5] != "as":
|
||||
date_field = FilterExpression(bits[5], parser)
|
||||
else:
|
||||
date_field = FilterExpression(None, parser)
|
||||
if bits[6] != 'as':
|
||||
if bits[6] != "as":
|
||||
num = FilterExpression(bits[6], parser)
|
||||
else:
|
||||
num = FilterExpression(None, parser)
|
||||
|
|
@ -345,11 +430,20 @@ register.tag("get_latest_objects_by_category", do_get_latest_objects_by_category
|
|||
@register.filter
|
||||
def tree_queryset(value):
|
||||
"""
|
||||
Converts a normal queryset from an MPTT model to include all the ancestors
|
||||
so a filtered subset of items can be formatted correctly
|
||||
Converts a normal queryset from an MPTT model to include all the ancestors.
|
||||
|
||||
Allows a filtered subset of items to be formatted correctly
|
||||
|
||||
Args:
|
||||
value: The queryset to convert
|
||||
|
||||
Returns:
|
||||
The converted queryset
|
||||
"""
|
||||
from django.db.models.query import QuerySet
|
||||
from copy import deepcopy
|
||||
|
||||
from django.db.models.query import QuerySet
|
||||
|
||||
if not isinstance(value, QuerySet):
|
||||
return value
|
||||
|
||||
|
|
@ -365,9 +459,9 @@ def tree_queryset(value):
|
|||
# this cuts down the number of queries considerably since all ancestors
|
||||
# will already be in include_pages when they are checked, thus not
|
||||
# trigger additional queries.
|
||||
for p in qs2.order_by('rght').iterator():
|
||||
for p in qs2.order_by("rght").iterator():
|
||||
if p.parent_id and p.parent_id not in include_pages and p.id not in include_pages:
|
||||
ancestor_id_list = p.get_ancestors().values_list('id', flat=True)
|
||||
ancestor_id_list = p.get_ancestors().values_list("id", flat=True)
|
||||
include_pages.update(ancestor_id_list)
|
||||
|
||||
if include_pages:
|
||||
|
|
@ -381,10 +475,13 @@ def tree_queryset(value):
|
|||
def recursetree(parser, token):
|
||||
"""
|
||||
Iterates over the nodes in the tree, and renders the contained block for each node.
|
||||
|
||||
This tag will recursively render children into the template variable {{ children }}.
|
||||
Only one database query is required (children are cached for the whole tree)
|
||||
|
||||
Usage:
|
||||
Example:
|
||||
Basic usage example::
|
||||
|
||||
<ul>
|
||||
{% recursetree nodes %}
|
||||
<li>
|
||||
|
|
@ -397,13 +494,23 @@ def recursetree(parser, token):
|
|||
</li>
|
||||
{% endrecursetree %}
|
||||
</ul>
|
||||
|
||||
Args:
|
||||
parser: The Django template parser.
|
||||
token: The tag contents
|
||||
|
||||
Returns:
|
||||
The recursive tree node.
|
||||
|
||||
Raises:
|
||||
TemplateSyntaxError: If a queryset isn't provided.
|
||||
"""
|
||||
bits = token.contents.split()
|
||||
if len(bits) != 2:
|
||||
raise template.TemplateSyntaxError('%s tag requires a queryset' % bits[0])
|
||||
raise template.TemplateSyntaxError("%s tag requires a queryset" % bits[0])
|
||||
queryset_var = FilterExpression(bits[1], parser)
|
||||
|
||||
template_nodes = parser.parse(('endrecursetree',))
|
||||
template_nodes = parser.parse(("endrecursetree",))
|
||||
parser.delete_first_token()
|
||||
|
||||
return RecurseTreeNode(template_nodes, queryset_var)
|
||||
|
|
|
|||
|
|
@ -1,68 +1,67 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.urls import reverse
|
||||
from django.test import Client, TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import smart_text
|
||||
|
||||
from categories.models import Category
|
||||
|
||||
|
||||
class TestCategoryAdmin(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
|
||||
def test_adding_parent_and_child(self):
|
||||
User.objects.create_superuser('testuser', 'testuser@example.com', 'password')
|
||||
self.client.login(username='testuser', password='password')
|
||||
url = reverse('admin:categories_category_add')
|
||||
User.objects.create_superuser("testuser", "testuser@example.com", "password")
|
||||
self.client.login(username="testuser", password="password")
|
||||
url = reverse("admin:categories_category_add")
|
||||
data = {
|
||||
'parent': '',
|
||||
'name': smart_text('Parent Catégory'),
|
||||
'thumbnail': '',
|
||||
'filename': '',
|
||||
'active': 'on',
|
||||
'alternate_title': '',
|
||||
'alternate_url': '',
|
||||
'description': '',
|
||||
'meta_keywords': '',
|
||||
'meta_extra': '',
|
||||
'order': 0,
|
||||
'slug': 'parent',
|
||||
'_save': '_save',
|
||||
'categoryrelation_set-TOTAL_FORMS': '0',
|
||||
'categoryrelation_set-INITIAL_FORMS': '0',
|
||||
'categoryrelation_set-MIN_NUM_FORMS': '1000',
|
||||
'categoryrelation_set-MAX_NUM_FORMS': '1000',
|
||||
"parent": "",
|
||||
"name": smart_text("Parent Catégory"),
|
||||
"thumbnail": "",
|
||||
"filename": "",
|
||||
"active": "on",
|
||||
"alternate_title": "",
|
||||
"alternate_url": "",
|
||||
"description": "",
|
||||
"meta_keywords": "",
|
||||
"meta_extra": "",
|
||||
"order": 0,
|
||||
"slug": "parent",
|
||||
"_save": "_save",
|
||||
"categoryrelation_set-TOTAL_FORMS": "0",
|
||||
"categoryrelation_set-INITIAL_FORMS": "0",
|
||||
"categoryrelation_set-MIN_NUM_FORMS": "1000",
|
||||
"categoryrelation_set-MAX_NUM_FORMS": "1000",
|
||||
}
|
||||
resp = self.client.post(url, data=data)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(1, Category.objects.count())
|
||||
|
||||
# update parent
|
||||
data.update({'name': smart_text('Parent Catégory (Changed)')})
|
||||
resp = self.client.post(reverse('admin:categories_category_change', args=(1,)), data=data)
|
||||
data.update({"name": smart_text("Parent Catégory (Changed)")})
|
||||
resp = self.client.post(reverse("admin:categories_category_change", args=(1,)), data=data)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(1, Category.objects.count())
|
||||
|
||||
# add a child
|
||||
data.update({
|
||||
'parent': '1',
|
||||
'name': smart_text('Child Catégory'),
|
||||
'slug': smart_text('child-category'),
|
||||
})
|
||||
data.update(
|
||||
{
|
||||
"parent": "1",
|
||||
"name": smart_text("Child Catégory"),
|
||||
"slug": smart_text("child-category"),
|
||||
}
|
||||
)
|
||||
resp = self.client.post(url, data=data)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(2, Category.objects.count())
|
||||
|
||||
# update child
|
||||
data.update({'name': 'Child (Changed)'})
|
||||
resp = self.client.post(reverse('admin:categories_category_change', args=(2,)), data=data)
|
||||
data.update({"name": "Child (Changed)"})
|
||||
resp = self.client.post(reverse("admin:categories_category_change", args=(2,)), data=data)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(2, Category.objects.count())
|
||||
|
||||
# test the admin list view
|
||||
url = reverse('admin:categories_category_changelist')
|
||||
url = reverse("admin:categories_category_changelist")
|
||||
resp = self.client.get(url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
|
|
|||
|
|
@ -4,20 +4,21 @@
|
|||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import TestCase, override_settings
|
||||
from categories.models import Category
|
||||
from categories.management.commands.import_categories import Command
|
||||
from django.core.management.base import CommandError
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from categories.management.commands.import_categories import Command
|
||||
from categories.models import Category
|
||||
|
||||
|
||||
@override_settings(INSTALLED_APPS=(app for app in settings.INSTALLED_APPS if app != 'django.contrib.flatpages'))
|
||||
@override_settings(INSTALLED_APPS=(app for app in settings.INSTALLED_APPS if app != "django.contrib.flatpages"))
|
||||
class CategoryImportTest(TestCase):
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def _import_file(self, filename):
|
||||
root_cats = ['Category 1', 'Category 2', 'Category 3']
|
||||
testfile = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'fixtures', filename))
|
||||
root_cats = ["Category 1", "Category 2", "Category 3"]
|
||||
testfile = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), "fixtures", filename))
|
||||
cmd = Command()
|
||||
cmd.handle(testfile)
|
||||
roots = Category.tree.root_nodes()
|
||||
|
|
@ -26,31 +27,31 @@ class CategoryImportTest(TestCase):
|
|||
for item in roots:
|
||||
assert item.name in root_cats
|
||||
|
||||
cat2 = Category.objects.get(name='Category 2')
|
||||
cat2 = Category.objects.get(name="Category 2")
|
||||
cat21 = cat2.children.all()[0]
|
||||
self.assertEqual(cat21.name, 'Category 2-1')
|
||||
self.assertEqual(cat21.name, "Category 2-1")
|
||||
cat211 = cat21.children.all()[0]
|
||||
self.assertEqual(cat211.name, 'Category 2-1-1')
|
||||
self.assertEqual(cat211.name, "Category 2-1-1")
|
||||
|
||||
def testImportSpaceDelimited(self):
|
||||
Category.objects.all().delete()
|
||||
self._import_file('test_category_spaces.txt')
|
||||
self._import_file("test_category_spaces.txt")
|
||||
|
||||
items = Category.objects.all()
|
||||
|
||||
self.assertEqual(items[0].name, 'Category 1')
|
||||
self.assertEqual(items[1].name, 'Category 1-1')
|
||||
self.assertEqual(items[2].name, 'Category 1-2')
|
||||
self.assertEqual(items[0].name, "Category 1")
|
||||
self.assertEqual(items[1].name, "Category 1-1")
|
||||
self.assertEqual(items[2].name, "Category 1-2")
|
||||
|
||||
def testImportTabDelimited(self):
|
||||
Category.objects.all().delete()
|
||||
self._import_file('test_category_tabs.txt')
|
||||
self._import_file("test_category_tabs.txt")
|
||||
|
||||
items = Category.objects.all()
|
||||
|
||||
self.assertEqual(items[0].name, 'Category 1')
|
||||
self.assertEqual(items[1].name, 'Category 1-1')
|
||||
self.assertEqual(items[2].name, 'Category 1-2')
|
||||
self.assertEqual(items[0].name, "Category 1")
|
||||
self.assertEqual(items[1].name, "Category 1-1")
|
||||
self.assertEqual(items[2].name, "Category 1-2")
|
||||
|
||||
def testMixingTabsSpaces(self):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
# test active returns only active items
|
||||
from django.test import TestCase
|
||||
|
||||
from categories.models import Category
|
||||
|
||||
|
||||
class CategoryManagerTest(TestCase):
|
||||
fixtures = ['categories.json']
|
||||
fixtures = ["categories.json"]
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
|
@ -16,7 +17,7 @@ class CategoryManagerTest(TestCase):
|
|||
all_count = Category.objects.all().count()
|
||||
self.assertEqual(Category.objects.active().count(), all_count)
|
||||
|
||||
cat1 = Category.objects.get(name='Category 1')
|
||||
cat1 = Category.objects.get(name="Category 1")
|
||||
cat1.active = False
|
||||
cat1.save()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from django.core import management
|
||||
from django.core.management.base import CommandError
|
||||
from django.db import connection
|
||||
|
|
@ -6,7 +5,6 @@ from django.test import TestCase
|
|||
|
||||
|
||||
class TestMgmtCommands(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
connection.disable_constraint_checking()
|
||||
|
|
@ -18,13 +16,13 @@ class TestMgmtCommands(TestCase):
|
|||
connection.enable_constraint_checking()
|
||||
|
||||
def test_add_category_fields(self):
|
||||
management.call_command('add_category_fields', verbosity=0)
|
||||
management.call_command("add_category_fields", verbosity=0)
|
||||
|
||||
def test_add_category_fields_app(self):
|
||||
management.call_command('add_category_fields', 'flatpages', verbosity=0)
|
||||
management.call_command("add_category_fields", "flatpages", verbosity=0)
|
||||
|
||||
def test_drop_category_field(self):
|
||||
management.call_command('drop_category_field', 'flatpages', 'flatpage', 'category', verbosity=0)
|
||||
management.call_command("drop_category_field", "flatpages", "flatpage", "category", verbosity=0)
|
||||
|
||||
def test_drop_category_field_error(self):
|
||||
self.assertRaises(CommandError, management.call_command, 'drop_category_field', verbosity=0)
|
||||
self.assertRaises(CommandError, management.call_command, "drop_category_field", verbosity=0)
|
||||
|
|
|
|||
|
|
@ -2,20 +2,19 @@ import os
|
|||
|
||||
from django.core.files import File
|
||||
from django.core.files.uploadedfile import UploadedFile
|
||||
from django.test import TestCase
|
||||
|
||||
from categories.models import Category
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestCategoryThumbnail(TestCase):
|
||||
|
||||
def test_thumbnail(self):
|
||||
|
||||
file_name = 'test_image.jpg'
|
||||
file_name = "test_image.jpg"
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), file_name), 'rb') as f:
|
||||
test_image = UploadedFile(File(f), content_type='image/jpeg')
|
||||
category = Category.objects.create(name='Test Category', slug='test-category', thumbnail=test_image)
|
||||
with open(os.path.join(os.path.dirname(__file__), file_name), "rb") as f:
|
||||
test_image = UploadedFile(File(f), content_type="image/jpeg")
|
||||
category = Category.objects.create(name="Test Category", slug="test-category", thumbnail=test_image)
|
||||
self.assertEqual(category.pk, 1)
|
||||
self.assertEqual(category.thumbnail_width, 640)
|
||||
self.assertEqual(category.thumbnail_height, 480)
|
||||
|
|
|
|||
|
|
@ -14,43 +14,39 @@ class CategoryRegistrationTest(TestCase):
|
|||
"""
|
||||
|
||||
def test_foreignkey_string(self):
|
||||
FK_REGISTRY = {
|
||||
'flatpages.flatpage': 'category'
|
||||
}
|
||||
FK_REGISTRY = {"flatpages.flatpage": "category"}
|
||||
_process_registry(FK_REGISTRY, registry.register_fk)
|
||||
from django.contrib.flatpages.models import FlatPage
|
||||
self.assertTrue('category' in [f.name for f in FlatPage()._meta.get_fields()])
|
||||
|
||||
self.assertTrue("category" in [f.name for f in FlatPage()._meta.get_fields()])
|
||||
|
||||
def test_foreignkey_dict(self):
|
||||
FK_REGISTRY = {
|
||||
'flatpages.flatpage': {'name': 'category'}
|
||||
}
|
||||
FK_REGISTRY = {"flatpages.flatpage": {"name": "category"}}
|
||||
_process_registry(FK_REGISTRY, registry.register_fk)
|
||||
from django.contrib.flatpages.models import FlatPage
|
||||
self.assertTrue('category' in [f.name for f in FlatPage()._meta.get_fields()])
|
||||
|
||||
self.assertTrue("category" in [f.name for f in FlatPage()._meta.get_fields()])
|
||||
|
||||
def test_foreignkey_list(self):
|
||||
FK_REGISTRY = {
|
||||
'flatpages.flatpage': (
|
||||
{'name': 'category', 'related_name': 'cats'},
|
||||
)
|
||||
}
|
||||
FK_REGISTRY = {"flatpages.flatpage": ({"name": "category", "related_name": "cats"},)}
|
||||
_process_registry(FK_REGISTRY, registry.register_fk)
|
||||
from django.contrib.flatpages.models import FlatPage
|
||||
self.assertTrue('category' in [f.name for f in FlatPage()._meta.get_fields()])
|
||||
|
||||
self.assertTrue("category" in [f.name for f in FlatPage()._meta.get_fields()])
|
||||
|
||||
if django.VERSION[1] >= 7:
|
||||
|
||||
def test_new_foreignkey_string(self):
|
||||
registry.register_model('flatpages', 'flatpage', 'ForeignKey', 'category')
|
||||
registry.register_model("flatpages", "flatpage", "ForeignKey", "category")
|
||||
from django.contrib.flatpages.models import FlatPage
|
||||
self.assertTrue('category' in [f.name for f in FlatPage()._meta.get_fields()])
|
||||
|
||||
self.assertTrue("category" in [f.name for f in FlatPage()._meta.get_fields()])
|
||||
|
||||
|
||||
class Categorym2mTest(TestCase):
|
||||
def test_m2m_string(self):
|
||||
M2M_REGISTRY = {
|
||||
'flatpages.flatpage': 'categories'
|
||||
}
|
||||
M2M_REGISTRY = {"flatpages.flatpage": "categories"}
|
||||
_process_registry(M2M_REGISTRY, registry.register_m2m)
|
||||
from django.contrib.flatpages.models import FlatPage
|
||||
self.assertTrue('category' in [f.name for f in FlatPage()._meta.get_fields()])
|
||||
|
||||
self.assertTrue("category" in [f.name for f in FlatPage()._meta.get_fields()])
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
from django.test import TestCase
|
||||
from django import template
|
||||
import re
|
||||
|
||||
from django import template
|
||||
from django.test import TestCase
|
||||
|
||||
from categories.models import Category
|
||||
|
||||
|
||||
class CategoryTagsTest(TestCase):
|
||||
|
||||
fixtures = ['musicgenres.json']
|
||||
fixtures = ["musicgenres.json"]
|
||||
|
||||
def render_template(self, template_string, context={}):
|
||||
"""
|
||||
|
|
@ -21,7 +22,9 @@ class CategoryTagsTest(TestCase):
|
|||
"""
|
||||
Ensure that get_category raises an exception if there aren't enough arguments.
|
||||
"""
|
||||
self.assertRaises(template.TemplateSyntaxError, self.render_template, '{% load category_tags %}{% get_category %}')
|
||||
self.assertRaises(
|
||||
template.TemplateSyntaxError, self.render_template, "{% load category_tags %}{% get_category %}"
|
||||
)
|
||||
|
||||
def testBasicUsage(self):
|
||||
"""
|
||||
|
|
@ -30,46 +33,50 @@ class CategoryTagsTest(TestCase):
|
|||
# display_path_as_ul
|
||||
rock_resp = '<ul><li><a href="/categories/">Top</a></li></ul>'
|
||||
resp = self.render_template('{% load category_tags %}{% display_path_as_ul "/Rock" %}')
|
||||
resp = re.sub(r'\n$', "", resp)
|
||||
resp = re.sub(r"\n$", "", resp)
|
||||
self.assertEqual(resp, rock_resp)
|
||||
|
||||
# display_drilldown_as_ul
|
||||
expected_resp = '<ul><li><a href="/categories/">Top</a><ul><li><a href="/categories/world/">World</a><ul><li><strong>Worldbeat</strong><ul><li><a href="/categories/world/worldbeat/afrobeat/">Afrobeat</a></li></ul></li></ul></li></ul></li></ul>'
|
||||
resp = self.render_template(
|
||||
'{% load category_tags %}'
|
||||
'{% display_drilldown_as_ul "/World/Worldbeat" "categories.category" %}')
|
||||
resp = re.sub(r'\n$', "", resp)
|
||||
"{% load category_tags %}" '{% display_drilldown_as_ul "/World/Worldbeat" "categories.category" %}'
|
||||
)
|
||||
resp = re.sub(r"\n$", "", resp)
|
||||
self.assertEqual(resp, expected_resp)
|
||||
|
||||
# breadcrumbs
|
||||
expected_resp = '<a href="/categories/world/">World</a> > Worldbeat'
|
||||
expected_resp = '<a href="/categories/world/">World</a> > Worldbeat\n'
|
||||
resp = self.render_template(
|
||||
'{% load category_tags %}'
|
||||
'{% breadcrumbs "/World/Worldbeat" " > " "categories.category" %}')
|
||||
"{% load category_tags %}" '{% breadcrumbs "/World/Worldbeat" " > " "categories.category" %}'
|
||||
)
|
||||
self.assertEqual(resp, expected_resp)
|
||||
|
||||
# get_top_level_categories
|
||||
expected_resp = 'Avant-garde|Blues|Country|Easy listening|Electronic|Hip hop/Rap music|Jazz|Latin|Modern folk|Pop|Reggae|Rhythm and blues|Rock|World|'
|
||||
expected_resp = "Avant-garde|Blues|Country|Easy listening|Electronic|Hip hop/Rap music|Jazz|Latin|Modern folk|Pop|Reggae|Rhythm and blues|Rock|World|"
|
||||
resp = self.render_template(
|
||||
'{% load category_tags %}'
|
||||
"{% load category_tags %}"
|
||||
'{% get_top_level_categories using "categories.category" as varname %}'
|
||||
'{% for item in varname %}{{ item }}|{% endfor %}')
|
||||
"{% for item in varname %}{{ item }}|{% endfor %}"
|
||||
)
|
||||
self.assertEqual(resp, expected_resp)
|
||||
|
||||
# get_category_drilldown
|
||||
expected_resp = "World|World > Worldbeat|"
|
||||
resp = self.render_template(
|
||||
'{% load category_tags %}'
|
||||
"{% load category_tags %}"
|
||||
'{% get_category_drilldown "/World" using "categories.category" as var %}'
|
||||
'{% for item in var %}{{ item }}|{% endfor %}')
|
||||
"{% for item in var %}{{ item }}|{% endfor %}"
|
||||
)
|
||||
self.assertEqual(resp, expected_resp)
|
||||
|
||||
# recursetree
|
||||
expected_resp = '<ul><li>Country<ul><li>Country pop<ul><li>Urban Cowboy</li></ul></li></ul></li><li>World<ul><li>Worldbeat<ul></ul></li></ul></li></ul>'
|
||||
ctxt = {'nodes': Category.objects.filter(name__in=("Worldbeat", "Urban Cowboy"))}
|
||||
expected_resp = "<ul><li>Country<ul><li>Country pop<ul><li>Urban Cowboy</li></ul></li></ul></li><li>World<ul><li>Worldbeat<ul></ul></li></ul></li></ul>"
|
||||
ctxt = {"nodes": Category.objects.filter(name__in=("Worldbeat", "Urban Cowboy"))}
|
||||
resp = self.render_template(
|
||||
'{% load category_tags %}'
|
||||
'<ul>{% recursetree nodes|tree_queryset %}<li>{{ node.name }}'
|
||||
'{% if not node.is_leaf_node %}<ul>{{ children }}'
|
||||
'</ul>{% endif %}</li>{% endrecursetree %}</ul>', ctxt)
|
||||
"{% load category_tags %}"
|
||||
"<ul>{% recursetree nodes|tree_queryset %}<li>{{ node.name }}"
|
||||
"{% if not node.is_leaf_node %}<ul>{{ children }}"
|
||||
"</ul>{% endif %}</li>{% endrecursetree %}</ul>",
|
||||
ctxt,
|
||||
)
|
||||
self.assertEqual(resp, expected_resp)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
from django.http import Http404
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.test import Client, TestCase, RequestFactory
|
||||
from categories.models import Category, CategoryRelation
|
||||
from django.http import Http404
|
||||
from django.test import Client, RequestFactory, TestCase
|
||||
|
||||
from categories import views
|
||||
from categories.models import Category, CategoryRelation
|
||||
|
||||
|
||||
class MyCategoryRelationView(views.CategoryRelatedDetail):
|
||||
|
|
@ -10,16 +11,18 @@ class MyCategoryRelationView(views.CategoryRelatedDetail):
|
|||
|
||||
|
||||
class TestCategoryViews(TestCase):
|
||||
fixtures = ['musicgenres.json', ]
|
||||
fixtures = [
|
||||
"musicgenres.json",
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def test_category_detail(self):
|
||||
cat0 = Category.objects.get(slug='country', level=0)
|
||||
cat1 = cat0.children.get(slug='country-pop')
|
||||
cat2 = Category.objects.get(slug='urban-cowboy')
|
||||
cat0 = Category.objects.get(slug="country", level=0)
|
||||
cat1 = cat0.children.get(slug="country-pop")
|
||||
cat2 = Category.objects.get(slug="urban-cowboy")
|
||||
url = cat0.get_absolute_url()
|
||||
response = self.client.get(url)
|
||||
self.assertEquals(response.status_code, 200)
|
||||
|
|
@ -33,51 +36,48 @@ class TestCategoryViews(TestCase):
|
|||
self.assertEquals(response.status_code, 404)
|
||||
|
||||
def test_get_category_for_path(self):
|
||||
cat0 = Category.objects.get(slug='country', level=0)
|
||||
cat1 = cat0.children.get(slug='country-pop')
|
||||
cat2 = Category.objects.get(slug='urban-cowboy')
|
||||
cat0 = Category.objects.get(slug="country", level=0)
|
||||
cat1 = cat0.children.get(slug="country-pop")
|
||||
cat2 = Category.objects.get(slug="urban-cowboy")
|
||||
|
||||
result = views.get_category_for_path('/country/country-pop/urban-cowboy/')
|
||||
result = views.get_category_for_path("/country/country-pop/urban-cowboy/")
|
||||
self.assertEquals(result, cat2)
|
||||
result = views.get_category_for_path('/country/country-pop/')
|
||||
result = views.get_category_for_path("/country/country-pop/")
|
||||
self.assertEquals(result, cat1)
|
||||
result = views.get_category_for_path('/country/')
|
||||
result = views.get_category_for_path("/country/")
|
||||
self.assertEquals(result, cat0)
|
||||
|
||||
def test_categorydetailview(self):
|
||||
request = self.factory.get('')
|
||||
request = self.factory.get("")
|
||||
request.user = AnonymousUser()
|
||||
self.assertRaises(AttributeError, views.CategoryDetailView.as_view(), request)
|
||||
|
||||
request = self.factory.get('')
|
||||
request = self.factory.get("")
|
||||
request.user = AnonymousUser()
|
||||
response = views.CategoryDetailView.as_view()(request, path='/country/country-pop/urban-cowboy/')
|
||||
response = views.CategoryDetailView.as_view()(request, path="/country/country-pop/urban-cowboy/")
|
||||
self.assertEquals(response.status_code, 200)
|
||||
|
||||
request = self.factory.get('')
|
||||
request = self.factory.get("")
|
||||
request.user = AnonymousUser()
|
||||
self.assertRaises(Http404, views.CategoryDetailView.as_view(), request, path='/country/country-pop/foo/')
|
||||
self.assertRaises(Http404, views.CategoryDetailView.as_view(), request, path="/country/country-pop/foo/")
|
||||
|
||||
def test_categoryrelateddetailview(self):
|
||||
from simpletext.models import SimpleText
|
||||
stext = SimpleText.objects.create(
|
||||
name='Test',
|
||||
description='test description'
|
||||
)
|
||||
cat = Category.objects.get(slug='urban-cowboy')
|
||||
cat_rel = CategoryRelation.objects.create( # NOQA
|
||||
category=cat,
|
||||
content_object=stext
|
||||
)
|
||||
request = self.factory.get('')
|
||||
|
||||
stext = SimpleText.objects.create(name="Test", description="test description")
|
||||
cat = Category.objects.get(slug="urban-cowboy")
|
||||
cat_rel = CategoryRelation.objects.create(category=cat, content_object=stext) # NOQA
|
||||
request = self.factory.get("")
|
||||
request.user = AnonymousUser()
|
||||
self.assertRaises(AttributeError, MyCategoryRelationView.as_view(), request)
|
||||
|
||||
request = self.factory.get('')
|
||||
request = self.factory.get("")
|
||||
request.user = AnonymousUser()
|
||||
response = MyCategoryRelationView.as_view()(request, category_path='/country/country-pop/urban-cowboy/')
|
||||
response = MyCategoryRelationView.as_view()(request, category_path="/country/country-pop/urban-cowboy/")
|
||||
self.assertEquals(response.status_code, 200)
|
||||
|
||||
request = self.factory.get('')
|
||||
request = self.factory.get("")
|
||||
request.user = AnonymousUser()
|
||||
self.assertRaises(Http404, MyCategoryRelationView.as_view(), request, category_path='/country/country-pop/foo/')
|
||||
self.assertRaises(
|
||||
Http404, MyCategoryRelationView.as_view(), request, category_path="/country/country-pop/foo/"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,19 +1,12 @@
|
|||
"""URL patterns for the categories app."""
|
||||
from django.conf.urls import url
|
||||
from django.views.generic import ListView
|
||||
from .models import Category
|
||||
|
||||
from . import views
|
||||
from .models import Category
|
||||
|
||||
categorytree_dict = {"queryset": Category.objects.filter(level=0)}
|
||||
|
||||
categorytree_dict = {
|
||||
'queryset': Category.objects.filter(level=0)
|
||||
}
|
||||
urlpatterns = (url(r"^$", ListView.as_view(**categorytree_dict), name="categories_tree_list"),)
|
||||
|
||||
urlpatterns = (
|
||||
url(
|
||||
r'^$', ListView.as_view(**categorytree_dict), name='categories_tree_list'
|
||||
),
|
||||
)
|
||||
|
||||
urlpatterns += (
|
||||
url(r'^(?P<path>.+)/$', views.category_detail, name='categories_category'),
|
||||
)
|
||||
urlpatterns += (url(r"^(?P<path>.+)/$", views.category_detail, name="categories_category"),)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
"""View functions for categories."""
|
||||
from typing import Optional
|
||||
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.http import HttpResponse, Http404
|
||||
from django.template.loader import select_template
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.generic import DetailView, ListView
|
||||
|
|
@ -7,139 +10,143 @@ from django.views.generic import DetailView, ListView
|
|||
from .models import Category
|
||||
|
||||
|
||||
def category_detail(request, path, template_name='categories/category_detail.html', extra_context={}):
|
||||
path_items = path.strip('/').split('/')
|
||||
def category_detail(
|
||||
request, path, template_name="categories/category_detail.html", extra_context: Optional[dict] = None
|
||||
):
|
||||
"""Render the detail page for a category."""
|
||||
extra_context = extra_context or {}
|
||||
path_items = path.strip("/").split("/")
|
||||
if len(path_items) >= 2:
|
||||
category = get_object_or_404(
|
||||
Category,
|
||||
slug__iexact=path_items[-1],
|
||||
level=len(path_items) - 1,
|
||||
parent__slug__iexact=path_items[-2])
|
||||
Category, slug__iexact=path_items[-1], level=len(path_items) - 1, parent__slug__iexact=path_items[-2]
|
||||
)
|
||||
else:
|
||||
category = get_object_or_404(
|
||||
Category,
|
||||
slug__iexact=path_items[-1],
|
||||
level=len(path_items) - 1)
|
||||
category = get_object_or_404(Category, slug__iexact=path_items[-1], level=len(path_items) - 1)
|
||||
|
||||
templates = []
|
||||
while path_items:
|
||||
templates.append('categories/%s.html' % '_'.join(path_items))
|
||||
templates.append("categories/%s.html" % "_".join(path_items))
|
||||
path_items.pop()
|
||||
templates.append(template_name)
|
||||
|
||||
context = {'category': category}
|
||||
context = {"category": category}
|
||||
if extra_context:
|
||||
context.update(extra_context)
|
||||
return HttpResponse(select_template(templates).render(context))
|
||||
|
||||
|
||||
def get_category_for_path(path, queryset=Category.objects.all()):
|
||||
path_items = path.strip('/').split('/')
|
||||
"""Return the category for a path."""
|
||||
path_items = path.strip("/").split("/")
|
||||
if len(path_items) >= 2:
|
||||
queryset = queryset.filter(
|
||||
slug__iexact=path_items[-1],
|
||||
level=len(path_items) - 1,
|
||||
parent__slug__iexact=path_items[-2])
|
||||
slug__iexact=path_items[-1], level=len(path_items) - 1, parent__slug__iexact=path_items[-2]
|
||||
)
|
||||
else:
|
||||
queryset = queryset.filter(
|
||||
slug__iexact=path_items[-1],
|
||||
level=len(path_items) - 1)
|
||||
queryset = queryset.filter(slug__iexact=path_items[-1], level=len(path_items) - 1)
|
||||
return queryset.get()
|
||||
|
||||
|
||||
class CategoryDetailView(DetailView):
|
||||
"""Detail view for a category."""
|
||||
|
||||
model = Category
|
||||
path_field = 'path'
|
||||
path_field = "path"
|
||||
|
||||
def get_object(self, **kwargs):
|
||||
"""Get the category."""
|
||||
if self.path_field not in self.kwargs:
|
||||
raise AttributeError("Category detail view %s must be called with "
|
||||
"a %s." % (self.__class__.__name__, self.path_field))
|
||||
raise AttributeError(
|
||||
"Category detail view %s must be called with " "a %s." % (self.__class__.__name__, self.path_field)
|
||||
)
|
||||
if self.queryset is None:
|
||||
queryset = self.get_queryset()
|
||||
try:
|
||||
return get_category_for_path(self.kwargs[self.path_field], self.model.objects.all())
|
||||
except Category.DoesNotExist:
|
||||
raise Http404(_("No %(verbose_name)s found matching the query") %
|
||||
{'verbose_name': queryset.model._meta.verbose_name})
|
||||
raise Http404(
|
||||
_("No %(verbose_name)s found matching the query") % {"verbose_name": queryset.model._meta.verbose_name}
|
||||
)
|
||||
|
||||
def get_template_names(self):
|
||||
"""Get the potential template names."""
|
||||
names = []
|
||||
path_items = self.kwargs[self.path_field].strip('/').split('/')
|
||||
path_items = self.kwargs[self.path_field].strip("/").split("/")
|
||||
while path_items:
|
||||
names.append('categories/%s.html' % '_'.join(path_items))
|
||||
names.append("categories/%s.html" % "_".join(path_items))
|
||||
path_items.pop()
|
||||
names.extend(super(CategoryDetailView, self).get_template_names())
|
||||
return names
|
||||
|
||||
|
||||
class CategoryRelatedDetail(DetailView):
|
||||
path_field = 'category_path'
|
||||
"""Detailed view for a category relation."""
|
||||
|
||||
path_field = "category_path"
|
||||
object_name_field = None
|
||||
|
||||
def get_object(self, **kwargs):
|
||||
"""Get the object to render."""
|
||||
if self.path_field not in self.kwargs:
|
||||
raise AttributeError("Category detail view %s must be called with "
|
||||
"a %s." % (self.__class__.__name__, self.path_field))
|
||||
raise AttributeError(
|
||||
"Category detail view %s must be called with " "a %s." % (self.__class__.__name__, self.path_field)
|
||||
)
|
||||
queryset = super(CategoryRelatedDetail, self).get_queryset()
|
||||
try:
|
||||
category = get_category_for_path(self.kwargs[self.path_field])
|
||||
except Category.DoesNotExist:
|
||||
raise Http404(_("No %(verbose_name)s found matching the query") %
|
||||
{'verbose_name': queryset.model._meta.verbose_name})
|
||||
raise Http404(
|
||||
_("No %(verbose_name)s found matching the query") % {"verbose_name": queryset.model._meta.verbose_name}
|
||||
)
|
||||
return queryset.get(category=category)
|
||||
|
||||
def get_template_names(self):
|
||||
"""Get all template names."""
|
||||
names = []
|
||||
opts = self.object._meta
|
||||
path_items = self.kwargs[self.path_field].strip('/').split('/')
|
||||
path_items = self.kwargs[self.path_field].strip("/").split("/")
|
||||
if self.object_name_field:
|
||||
path_items.append(getattr(self.object, self.object_name_field))
|
||||
while path_items:
|
||||
names.append('%s/category_%s_%s%s.html' % (
|
||||
opts.app_label,
|
||||
'_'.join(path_items),
|
||||
opts.object_name.lower(),
|
||||
self.template_name_suffix)
|
||||
names.append(
|
||||
"%s/category_%s_%s%s.html"
|
||||
% (opts.app_label, "_".join(path_items), opts.object_name.lower(), self.template_name_suffix)
|
||||
)
|
||||
path_items.pop()
|
||||
names.append('%s/category_%s%s.html' % (
|
||||
opts.app_label,
|
||||
opts.object_name.lower(),
|
||||
self.template_name_suffix)
|
||||
)
|
||||
names.append("%s/category_%s%s.html" % (opts.app_label, opts.object_name.lower(), self.template_name_suffix))
|
||||
names.extend(super(CategoryRelatedDetail, self).get_template_names())
|
||||
return names
|
||||
|
||||
|
||||
class CategoryRelatedList(ListView):
|
||||
path_field = 'category_path'
|
||||
"""List related category items."""
|
||||
|
||||
path_field = "category_path"
|
||||
|
||||
def get_queryset(self):
|
||||
"""Get the list of items."""
|
||||
if self.path_field not in self.kwargs:
|
||||
raise AttributeError("Category detail view %s must be called with "
|
||||
"a %s." % (self.__class__.__name__, self.path_field))
|
||||
raise AttributeError(
|
||||
"Category detail view %s must be called with " "a %s." % (self.__class__.__name__, self.path_field)
|
||||
)
|
||||
queryset = super(CategoryRelatedList, self).get_queryset()
|
||||
category = get_category_for_path(self.kwargs[self.path_field])
|
||||
return queryset.filter(category=category)
|
||||
|
||||
def get_template_names(self):
|
||||
"""Get the template names."""
|
||||
names = []
|
||||
if hasattr(self.object_list, 'model'):
|
||||
if hasattr(self.object_list, "model"):
|
||||
opts = self.object_list.model._meta
|
||||
path_items = self.kwargs[self.path_field].strip('/').split('/')
|
||||
path_items = self.kwargs[self.path_field].strip("/").split("/")
|
||||
while path_items:
|
||||
names.append('%s/category_%s_%s%s.html' % (
|
||||
opts.app_label,
|
||||
'_'.join(path_items),
|
||||
opts.object_name.lower(),
|
||||
self.template_name_suffix)
|
||||
names.append(
|
||||
"%s/category_%s_%s%s.html"
|
||||
% (opts.app_label, "_".join(path_items), opts.object_name.lower(), self.template_name_suffix)
|
||||
)
|
||||
path_items.pop()
|
||||
names.append('%s/category_%s%s.html' % (
|
||||
opts.app_label,
|
||||
opts.object_name.lower(),
|
||||
self.template_name_suffix)
|
||||
names.append(
|
||||
"%s/category_%s%s.html" % (opts.app_label, opts.object_name.lower(), self.template_name_suffix)
|
||||
)
|
||||
names.extend(super(CategoryRelatedList, self).get_template_names())
|
||||
return names
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ SPHINXOPTS = -a
|
|||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = _build
|
||||
DESTDIR = ..
|
||||
DESTDIR = _build
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
|
|
@ -32,9 +32,9 @@ clean:
|
|||
-rm -rf $(BUILDDIR)/*
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(DESTDIR)/docs
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(DESTDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(DESTDIR)/docs."
|
||||
@echo "Build finished. The HTML pages are in $(DESTDIR)/html."
|
||||
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 136 B |
32
doc_src/_static/css/custom.css
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
.sig-prename.descclassname {
|
||||
display: none;
|
||||
}
|
||||
dl.attribute {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
dl.field-list {
|
||||
display: block;
|
||||
}
|
||||
dl.field-list > dt {
|
||||
padding-left: 0;
|
||||
}
|
||||
dl.field-list > dd {
|
||||
padding-left: 1em;
|
||||
border-left: 0;
|
||||
}
|
||||
dl.field-list > dd > ul.simple {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
dl.field-list > dd + dt {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
dd {
|
||||
margin-left: 0;
|
||||
padding-left: 30px;
|
||||
border-left: 1px solid #c9c9c9;
|
||||
}
|
||||
.table.autosummary td {
|
||||
border-top: 0;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
|
@ -1,773 +0,0 @@
|
|||
/**
|
||||
* Sphinx stylesheet -- basic theme
|
||||
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
*/
|
||||
h3 {
|
||||
color:#000000;
|
||||
font-size: 17px;
|
||||
margin-bottom:0.5em;
|
||||
margin-top:2em;
|
||||
}
|
||||
/* -- main layout ----------------------------------------------------------- */
|
||||
|
||||
div.clearer {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/* -- header ---------------------------------------------------------------- */
|
||||
|
||||
#header #title {
|
||||
background:#29334F url(title_background.png) repeat-x scroll 0 0;
|
||||
border-bottom:1px solid #B6B6B6;
|
||||
height:25px;
|
||||
overflow:hidden;
|
||||
}
|
||||
#headerButtons {
|
||||
position: absolute;
|
||||
list-style: none outside;
|
||||
top: 26px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
border-top: 1px solid #2B334F;
|
||||
border-bottom: 1px solid #EDEDED;
|
||||
height: 20px;
|
||||
font-size: 8pt;
|
||||
overflow: hidden;
|
||||
background-color: #D8D8D8;
|
||||
}
|
||||
|
||||
#headerButtons li {
|
||||
background-repeat:no-repeat;
|
||||
display:inline;
|
||||
margin-top:0;
|
||||
padding:0;
|
||||
}
|
||||
|
||||
.headerButton {
|
||||
display: inline;
|
||||
height:20px;
|
||||
}
|
||||
|
||||
.headerButton a {
|
||||
text-decoration: none;
|
||||
float: right;
|
||||
height: 20px;
|
||||
padding: 4px 15px;
|
||||
border-left: 1px solid #ACACAC;
|
||||
font-family:'Lucida Grande',Geneva,Helvetica,Arial,sans-serif;
|
||||
color: black;
|
||||
}
|
||||
.headerButton a:hover {
|
||||
color: white;
|
||||
background-color: #787878;
|
||||
|
||||
}
|
||||
|
||||
li#toc_button {
|
||||
text-align:left;
|
||||
}
|
||||
|
||||
li#toc_button .headerButton a {
|
||||
width:198px;
|
||||
padding-top: 4px;
|
||||
font-family:'Lucida Grande',Geneva,Helvetica,Arial,sans-serif;
|
||||
color: black;
|
||||
float: left;
|
||||
padding-left:15px;
|
||||
border-right:1px solid #ACACAC;
|
||||
background:transparent url(triangle_open.png) no-repeat scroll 4px 6px;
|
||||
}
|
||||
|
||||
li#toc_button .headerButton a:hover {
|
||||
background-color: #787878;
|
||||
color: white;
|
||||
}
|
||||
|
||||
li#page_buttons {
|
||||
position:absolute;
|
||||
right:0;
|
||||
}
|
||||
|
||||
#breadcrumbs {
|
||||
color: black;
|
||||
background-image:url(breadcrumb_background.png);
|
||||
border-top:1px solid #2B334F;
|
||||
bottom:0;
|
||||
font-size:10px;
|
||||
height:15px;
|
||||
left:0;
|
||||
overflow:hidden;
|
||||
padding:3px 10px 0;
|
||||
position:absolute;
|
||||
right:0;
|
||||
white-space:nowrap;
|
||||
z-index:901;
|
||||
}
|
||||
#breadcrumbs a {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
}
|
||||
#breadcrumbs a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
#breadcrumbs img {
|
||||
padding-left: 3px;
|
||||
}
|
||||
/* -- sidebar --------------------------------------------------------------- */
|
||||
#sphinxsidebar {
|
||||
position: absolute;
|
||||
top: 84px;
|
||||
bottom: 19px;
|
||||
left: 0px;
|
||||
width: 229px;
|
||||
background-color: #E4EBF7;
|
||||
border-right: 1px solid #ACACAC;
|
||||
border-top: 1px solid #2B334F;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 0px 0px 0px 0px;
|
||||
font-size:11px;
|
||||
}
|
||||
|
||||
div.sphinxsidebarwrapper {
|
||||
padding: 10px 5px 0 10px;
|
||||
}
|
||||
|
||||
#sphinxsidebar li {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
font-weight: normal;
|
||||
margin: 0px 0px 7px 0px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
#sphinxsidebar ul {
|
||||
list-style: none;
|
||||
margin: 0px 0px 0px 0px;
|
||||
padding: 0px 5px 0px 5px;
|
||||
}
|
||||
|
||||
#sphinxsidebar ul ul,
|
||||
#sphinxsidebar ul.want-points {
|
||||
list-style: square;
|
||||
}
|
||||
|
||||
#sphinxsidebar ul ul {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#sphinxsidebar form {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#sphinxsidebar input {
|
||||
border: 1px solid #787878;
|
||||
font-family: sans-serif;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
#sphinxsidebar li.toctree-l1 a {
|
||||
font-weight: bold;
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#sphinxsidebar li.toctree-l2 a {
|
||||
font-weight: bold;
|
||||
color: #4f4f4f;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* -- search page ----------------------------------------------------------- */
|
||||
|
||||
ul.search {
|
||||
margin: 10px 0 0 20px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul.search li {
|
||||
padding: 5px 0 5px 20px;
|
||||
background-image: url(file.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 7px;
|
||||
}
|
||||
|
||||
ul.search li a {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
ul.search li div.context {
|
||||
color: #888;
|
||||
margin: 2px 0 0 30px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
ul.keywordmatches li.goodmatch a {
|
||||
font-weight: bold;
|
||||
}
|
||||
#sphinxsidebar input.prettysearch {border:none;}
|
||||
input.searchbutton {
|
||||
float: right;
|
||||
}
|
||||
.search-wrapper {width: 100%; height: 25px;}
|
||||
.search-wrapper input.prettysearch { border: none; width:200px; height: 16px; background: url(searchfield_repeat.png) center top repeat-x; border: 0px; margin: 0; padding: 3px 0 0 0; font: 11px "Lucida Grande", "Lucida Sans Unicode", Arial, sans-serif; }
|
||||
.search-wrapper input.prettysearch { width: 184px; margin-left: 20px; *margin-top:-1px; *margin-right:-2px; *margin-left:10px; }
|
||||
.search-wrapper .search-left { display: block; position: absolute; width: 20px; height: 19px; background: url(searchfield_leftcap.png) left top no-repeat; }
|
||||
.search-wrapper .search-right { display: block; position: relative; left: 204px; top: -19px; width: 10px; height: 19px; background: url(searchfield_rightcap.png) right top no-repeat; }
|
||||
|
||||
/* -- index page ------------------------------------------------------------ */
|
||||
|
||||
table.contentstable {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
table.contentstable p.biglink {
|
||||
line-height: 150%;
|
||||
}
|
||||
|
||||
a.biglink {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
span.linkdescr {
|
||||
font-style: italic;
|
||||
padding-top: 5px;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
/* -- general index --------------------------------------------------------- */
|
||||
|
||||
table.indextable td {
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
table.indextable dl, table.indextable dd {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
table.indextable tr.pcap {
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
table.indextable tr.cap {
|
||||
margin-top: 10px;
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
img.toggler {
|
||||
margin-right: 3px;
|
||||
margin-top: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* -- general body styles --------------------------------------------------- */
|
||||
.document {
|
||||
border-top:1px solid #2B334F;
|
||||
overflow:auto;
|
||||
padding-left:2em;
|
||||
padding-right:2em;
|
||||
position:absolute;
|
||||
z-index:1;
|
||||
top:84px;
|
||||
bottom:19px;
|
||||
right:0;
|
||||
left:230px;
|
||||
}
|
||||
|
||||
a.headerlink {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
h1:hover > a.headerlink,
|
||||
h2:hover > a.headerlink,
|
||||
h3:hover > a.headerlink,
|
||||
h4:hover > a.headerlink,
|
||||
h5:hover > a.headerlink,
|
||||
h6:hover > a.headerlink,
|
||||
dt:hover > a.headerlink {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
div.body p.caption {
|
||||
text-align: inherit;
|
||||
}
|
||||
|
||||
div.body td {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.field-list ul {
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.first {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
p.rubric {
|
||||
margin-top: 30px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* -- sidebars -------------------------------------------------------------- */
|
||||
|
||||
/*div.sidebar {
|
||||
margin: 0 0 0.5em 1em;
|
||||
border: 1px solid #ddb;
|
||||
padding: 7px 7px 0 7px;
|
||||
background-color: #ffe;
|
||||
width: 40%;
|
||||
float: right;
|
||||
}
|
||||
|
||||
p.sidebar-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
*/
|
||||
/* -- topics ---------------------------------------------------------------- */
|
||||
|
||||
div.topic {
|
||||
border: 1px solid #ccc;
|
||||
padding: 7px 7px 0 7px;
|
||||
margin: 10px 0 10px 0;
|
||||
}
|
||||
|
||||
p.topic-title {
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* -- admonitions ----------------------------------------------------------- */
|
||||
.admonition {
|
||||
border: 1px solid #a1a5a9;
|
||||
background-color: #f7f7f7;
|
||||
margin: 20px;
|
||||
padding: 0px 8px 7px 9px;
|
||||
text-align: left;
|
||||
}
|
||||
.warning {
|
||||
background-color:#E8E8E8;
|
||||
border:1px solid #111111;
|
||||
margin:30px;
|
||||
}
|
||||
.admonition p {
|
||||
font: 12px 'Lucida Grande', Geneva, Helvetica, Arial, sans-serif;
|
||||
margin-top: 7px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
div.admonition dt {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div.admonition dl {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
p.admonition-title {
|
||||
margin: 0px 10px 5px 0px;
|
||||
font-weight: bold;
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
div.body p.centered {
|
||||
text-align: center;
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
/* -- tables ---------------------------------------------------------------- */
|
||||
|
||||
table.docutils {
|
||||
border-collapse: collapse;
|
||||
border-top: 1px solid #919699;
|
||||
border-left: 1px solid #919699;
|
||||
border-right: 1px solid #919699;
|
||||
font-size:12px;
|
||||
padding:8px;
|
||||
text-align:left;
|
||||
vertical-align:top;
|
||||
}
|
||||
|
||||
table.docutils td, table.docutils th {
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
border-bottom: 1px solid #919699;
|
||||
}
|
||||
|
||||
table.docutils th {
|
||||
font-weight: bold;
|
||||
}
|
||||
/* This alternates colors in up to six table rows (light blue for odd, white for even)*/
|
||||
.docutils tr {
|
||||
background: #F0F5F9;
|
||||
}
|
||||
|
||||
.docutils tr + tr {
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.docutils tr + tr + tr {
|
||||
background: #F0F5F9;
|
||||
}
|
||||
|
||||
.docutils tr + tr + tr + tr {
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.docutils tr + tr + tr +tr + tr {
|
||||
background: #F0F5F9;
|
||||
}
|
||||
|
||||
.docutils tr + tr + tr + tr + tr + tr {
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.docutils tr + tr + tr + tr + tr + tr + tr {
|
||||
background: #F0F5F9;
|
||||
}
|
||||
|
||||
table.footnote td, table.footnote th {
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
/* -- other body styles ----------------------------------------------------- */
|
||||
|
||||
dl {
|
||||
margin-bottom: 15px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
dd p {
|
||||
margin-top: 0px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
dd ul, dd table {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-top: 3px;
|
||||
margin-bottom: 10px;
|
||||
margin-left: 30px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
dt:target, .highlight {
|
||||
background-color: #fbe54e;
|
||||
}
|
||||
|
||||
dl.glossary dt {
|
||||
font-weight: bold;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
dl.glossary dd {
|
||||
font-size:12px;
|
||||
}
|
||||
.field-list ul {
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
padding-bottom: 0;
|
||||
list-style: none inside;
|
||||
}
|
||||
|
||||
.field-list ul li {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.field-list p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.refcount {
|
||||
color: #060;
|
||||
}
|
||||
|
||||
.optional {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.versionmodified {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.system-message {
|
||||
background-color: #fda;
|
||||
padding: 5px;
|
||||
border: 3px solid red;
|
||||
}
|
||||
|
||||
.footnote:target {
|
||||
background-color: #ffa
|
||||
}
|
||||
|
||||
/* -- code displays --------------------------------------------------------- */
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
background-color:#F1F5F9;
|
||||
border:1px solid #C9D1D7;
|
||||
border-spacing:0;
|
||||
font-family:"Bitstream Vera Sans Mono",Monaco,"Lucida Console",Courier,Consolas,monospace;
|
||||
font-size:11px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
td.linenos {
|
||||
width: 2em;
|
||||
}
|
||||
|
||||
td.linenos pre {
|
||||
padding: 5px 0px;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
td.code {
|
||||
|
||||
}
|
||||
|
||||
table.highlighttable {
|
||||
margin-left: 0.5em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table.highlighttable td {
|
||||
padding: 0 0.5em 0 0.5em;
|
||||
}
|
||||
table.highlighttable td.linenos {
|
||||
text-align: right;
|
||||
width: 1.5em;
|
||||
padding-right: 0;
|
||||
}
|
||||
tt {
|
||||
font-family:"Bitstream Vera Sans Mono",Monaco,"Lucida Console",Courier,Consolas,monospace;
|
||||
}
|
||||
|
||||
tt.descname {
|
||||
background-color: transparent;
|
||||
font-weight: bold;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
tt.descclassname {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
tt.xref, a tt {
|
||||
background-color: transparent;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* -- math display ---------------------------------------------------------- */
|
||||
|
||||
img.math {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
div.body div.math p {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
span.eqno {
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* -- printout stylesheet --------------------------------------------------- */
|
||||
|
||||
@media print {
|
||||
div.document,
|
||||
div.documentwrapper,
|
||||
div.bodywrapper {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.sphinxsidebar,
|
||||
div.related,
|
||||
div.footer,
|
||||
#top-link {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family:'Lucida Grande',Geneva,Helvetica,Arial,sans-serif;
|
||||
}
|
||||
|
||||
dl.class dt {
|
||||
padding: 3px;
|
||||
/* border-top: 2px solid #999;*/
|
||||
}
|
||||
|
||||
em.property {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
dl.class dd p {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
dl.class dd dl.exception dt {
|
||||
padding: 3px;
|
||||
background-color: #FFD6D6;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
dl.class dd dl.method dt {
|
||||
padding: 3px;
|
||||
background-color: #e9e9e9;
|
||||
border-top: none;
|
||||
|
||||
}
|
||||
|
||||
dl.function dt {
|
||||
padding: 3px;
|
||||
border-top: 2px solid #999;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-image:none;
|
||||
list-style-position:outside;
|
||||
list-style-type:square;
|
||||
margin:0 0 0 30px;
|
||||
padding:0 0 12px 6px;
|
||||
}
|
||||
#docstitle {
|
||||
height: 36px;
|
||||
background-image: url(header_sm_mid.png);
|
||||
left: 0;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
#docstitle p {
|
||||
padding:7px 0 0 45px;
|
||||
margin: 0;
|
||||
color: white;
|
||||
text-shadow:0 1px 0 #787878;
|
||||
background: transparent url(documentation.png) no-repeat scroll 10px 3px;
|
||||
height: 36px;
|
||||
font-size: 15px;
|
||||
}
|
||||
#header {
|
||||
height:45px;
|
||||
left:0;
|
||||
position:absolute;
|
||||
right:0;
|
||||
top:36px;
|
||||
z-index:900;
|
||||
}
|
||||
|
||||
#header h1 {
|
||||
font-size:10pt;
|
||||
margin:0;
|
||||
padding:5px 0 0 10px;
|
||||
text-shadow:0 1px 0 #D5D5D5;
|
||||
white-space:nowrap;
|
||||
}
|
||||
|
||||
h1 {
|
||||
-x-system-font:none;
|
||||
color:#000000;
|
||||
font-family:'Lucida Grande',Geneva,Helvetica,Arial,sans-serif;
|
||||
font-size:30px;
|
||||
font-size-adjust:none;
|
||||
font-stretch:normal;
|
||||
font-style:normal;
|
||||
font-variant:normal;
|
||||
font-weight:bold;
|
||||
line-height:normal;
|
||||
margin-bottom:25px;
|
||||
margin-top:1em;
|
||||
}
|
||||
|
||||
.footer {
|
||||
border-top:1px solid #DDDDDD;
|
||||
clear:both;
|
||||
padding-top:9px;
|
||||
width:100%;
|
||||
font-size:10px;
|
||||
}
|
||||
|
||||
p {
|
||||
-x-system-font:none;
|
||||
font-family:'Lucida Grande',Geneva,Helvetica,Arial,sans-serif;
|
||||
font-size:12px;
|
||||
font-size-adjust:none;
|
||||
font-stretch:normal;
|
||||
font-style:normal;
|
||||
font-variant:normal;
|
||||
font-weight:normal;
|
||||
line-height:normal;
|
||||
margin-bottom:10px;
|
||||
margin-top:0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
border-bottom:1px solid #919699;
|
||||
color:#000000;
|
||||
font-size:24px;
|
||||
margin-top:2.5em;
|
||||
padding-bottom:2px;
|
||||
}
|
||||
|
||||
a:link:hover {
|
||||
color:#093D92;
|
||||
text-decoration:underline;
|
||||
}
|
||||
|
||||
a:link {
|
||||
color:#093D92;
|
||||
text-decoration:none;
|
||||
}
|
||||
|
||||
|
||||
ol {
|
||||
list-style-position:outside;
|
||||
list-style-type:decimal;
|
||||
margin:0 0 0 30px;
|
||||
padding:0 0 12px 6px;
|
||||
}
|
||||
li {
|
||||
margin-top:7px;
|
||||
font-family:'Lucida Grande',Geneva,Helvetica,Arial,sans-serif;
|
||||
font-size:12px;
|
||||
font-size-adjust:none;
|
||||
font-stretch:normal;
|
||||
font-style:normal;
|
||||
font-variant:normal;
|
||||
font-weight:normal;
|
||||
line-height:normal;
|
||||
}
|
||||
li > p {
|
||||
display:inline;
|
||||
}
|
||||
li p {
|
||||
margin-top:8px;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 412 B |
|
Before Width: | Height: | Size: 159 B |
|
Before Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 855 B |
|
Before Width: | Height: | Size: 158 B |
|
Before Width: | Height: | Size: 530 B |
|
Before Width: | Height: | Size: 132 B |
|
|
@ -1,20 +0,0 @@
|
|||
var TOC = {
|
||||
load: function () {
|
||||
$('#toc_button').click(TOC.toggle);
|
||||
},
|
||||
|
||||
toggle: function () {
|
||||
if ($('#sphinxsidebar').toggle().is(':hidden')) {
|
||||
$('div.document').css('left', "0px");
|
||||
$('toc_button').removeClass("open");
|
||||
} else {
|
||||
$('div.document').css('left', "230px");
|
||||
$('#toc_button').addClass("open");
|
||||
}
|
||||
return $('#sphinxsidebar');
|
||||
}
|
||||
};
|
||||
|
||||
$(document).ready(function () {
|
||||
TOC.load();
|
||||
});
|
||||
|
Before Width: | Height: | Size: 181 B |
|
Before Width: | Height: | Size: 195 B |
|
Before Width: | Height: | Size: 191 B |
10
doc_src/_templates/autosummary/base.rst
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
.. rst-class:: h4 text-secondary
|
||||
|
||||
{{ fullname }}
|
||||
|
||||
{{ objname | escape | underline}}
|
||||
|
||||
.. currentmodule:: {{ module }}
|
||||
{%- if not objname.startswith("test") %}
|
||||
.. auto{{ objtype }}:: {{ objname }}
|
||||
{%- endif %}
|
||||
34
doc_src/_templates/autosummary/class.rst
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
.. rst-class:: h4 text-secondary
|
||||
|
||||
{{ fullname }}
|
||||
|
||||
{{ objname | escape | underline}}
|
||||
|
||||
.. currentmodule:: {{ module }}
|
||||
|
||||
.. autoclass:: {{ objname }}
|
||||
|
||||
{% block methods %}
|
||||
{% if methods %}
|
||||
.. rubric:: {{ _('Methods') }}
|
||||
|
||||
.. autosummary::
|
||||
{% for item in methods %}
|
||||
{%- if "__" not in item %}
|
||||
~{{ name }}.{{ item }}
|
||||
{% endif -%}
|
||||
{%- endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block attributes %}
|
||||
{% if attributes %}
|
||||
.. rubric:: {{ _('Attributes') }}
|
||||
|
||||
.. autosummary::
|
||||
{% for item in attributes %}
|
||||
{%- if "__" not in item %}
|
||||
~{{ name }}.{{ item }}{% endif %}
|
||||
{% endfor -%}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
70
doc_src/_templates/autosummary/module.rst
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
.. rst-class:: h4 text-secondary
|
||||
|
||||
{{ fullname }}
|
||||
|
||||
{{ objname | escape | underline}}
|
||||
.. currentmodule:: {{ fullname }}
|
||||
|
||||
|
||||
.. automodule:: {{ fullname }}
|
||||
|
||||
{% block modules -%}
|
||||
{% if modules %}
|
||||
|
||||
.. rubric:: Submodules
|
||||
|
||||
.. autosummary::
|
||||
:toctree:
|
||||
:recursive:
|
||||
{% for item in modules %}
|
||||
{% if "migrations" not in item and "tests" not in item%}{{ item }}{% endif %}
|
||||
{%- endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block attributes -%}
|
||||
{%- if attributes -%}
|
||||
.. rubric:: {{ _('Module Attributes') }}
|
||||
|
||||
.. autosummary::
|
||||
:toctree:
|
||||
{% for item in attributes %}
|
||||
{{ item }}
|
||||
{%- endfor -%}
|
||||
{%- endif -%}
|
||||
{% endblock attributes -%}
|
||||
{%- block functions -%}
|
||||
{%- if functions %}
|
||||
|
||||
.. rubric:: {{ _('Functions') }}
|
||||
|
||||
.. autosummary::
|
||||
:toctree:
|
||||
{% for item in functions %}
|
||||
{{ item }}
|
||||
{%- endfor -%}
|
||||
{%- endif -%}
|
||||
{% endblock functions -%}
|
||||
{% block classes -%}
|
||||
{% if classes %}
|
||||
|
||||
.. rubric:: {{ _('Classes') }}
|
||||
|
||||
.. autosummary::
|
||||
:toctree:
|
||||
{% for item in classes %}
|
||||
{{ item }}
|
||||
{%- endfor -%}
|
||||
{%- endif -%}
|
||||
{% endblock classes -%}
|
||||
{% block exceptions -%}
|
||||
{% if exceptions %}
|
||||
|
||||
.. rubric:: {{ _('Exceptions') }}
|
||||
|
||||
.. autosummary::
|
||||
:toctree:
|
||||
{% for item in exceptions %}
|
||||
{{ item }}
|
||||
{%- endfor -%}
|
||||
{%- endif -%}
|
||||
{% endblock exceptions -%}
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
{% extends "basic/layout.html" %}
|
||||
{%- block doctype -%}
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
{%- endblock %}
|
||||
{%- set reldelim1 = reldelim1 is not defined and ' »' or reldelim1 %}
|
||||
{%- set reldelim2 = reldelim2 is not defined and ' |' or reldelim2 %}
|
||||
{%- block linktags %}
|
||||
{%- if hasdoc('about') %}
|
||||
<link rel="author" title="{{ _('About these documents') }}" href="{{ pathto('about') }}" />
|
||||
{%- endif %}
|
||||
{%- if hasdoc('genindex') %}
|
||||
<link rel="index" title="{{ _('Index') }}" href="{{ pathto('genindex') }}" />
|
||||
{%- endif %}
|
||||
{%- if hasdoc('search') %}
|
||||
<link rel="search" title="{{ _('Search') }}" href="{{ pathto('search') }}" />
|
||||
{%- endif %}
|
||||
{%- if hasdoc('copyright') %}
|
||||
<link rel="copyright" title="{{ _('Copyright') }}" href="{{ pathto('copyright') }}" />
|
||||
{%- endif %}
|
||||
<link rel="top" title="{{ docstitle|e }}" href="{{ pathto('index') }}" />
|
||||
{%- if parents %}
|
||||
<link rel="up" title="{{ parents[-1].title|striptags }}" href="{{ parents[-1].link|e }}" />
|
||||
{%- endif %}
|
||||
{%- if next %}
|
||||
<link rel="next" title="{{ next.title|striptags }}" href="{{ next.link|e }}" />
|
||||
{%- endif %}
|
||||
{%- if prev %}
|
||||
<link rel="prev" title="{{ prev.title|striptags }}" href="{{ prev.link|e }}" />
|
||||
{%- endif %}
|
||||
{%- endblock %}
|
||||
{%- block extrahead %} {% endblock %}
|
||||
{%- block header %}{% endblock %}
|
||||
{%- block relbar1 %}
|
||||
<div id="docstitle">
|
||||
<p>{{docstitle}}</p>
|
||||
</div>
|
||||
<div id="header">
|
||||
<div id="title"><h1>{{ title|striptags }}</h1></div>
|
||||
<ul id="headerButtons">
|
||||
<li id="toc_button"><div class="headerButton"><a href="#">Table of Contents</a></div></li>
|
||||
<li id="page_buttons">
|
||||
{%- for rellink in rellinks %}
|
||||
<div class="headerButton"><a href="{{ pathto(rellink[0]) }}" title="{{ rellink[1]|striptags }}" {{ accesskey(rellink[2]) }}>{{ rellink[3] }}</a></div>
|
||||
{%- endfor %}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{%- block sidebar1 %}
|
||||
{%- if not embedded %}{% if not theme_nosidebar|tobool %}
|
||||
<div id="sphinxsidebar">
|
||||
<div class="sphinxsidebarwrapper">
|
||||
{%- block sidebarlogo %}
|
||||
{%- if logo %}
|
||||
<p class="logo"><a href="{{ pathto(master_doc) }}">
|
||||
<img class="logo" src="{{ pathto('_static/' + logo, 1) }}" alt="Logo"/>
|
||||
</a></p>
|
||||
{%- endif %}
|
||||
{%- endblock %}
|
||||
{%- block sidebartoc %}
|
||||
<ul><li class="toctree-l1"><a href="{{ pathto(master_doc) }}">{{ _('Main Page') }}</a></li></ul>
|
||||
{{ toctree() }}
|
||||
{%- endblock %}
|
||||
{%- block sidebarrel %}
|
||||
{%- endblock %}
|
||||
{%- block sidebarsourcelink %}
|
||||
{%- if show_source and has_source and sourcename %}
|
||||
<h3>{{ _('This Page') }}</h3>
|
||||
<ul class="this-page-menu">
|
||||
<li><a href="{{ pathto('_sources/' + sourcename, true)|e }}"
|
||||
rel="nofollow">{{ _('Show Source') }}</a></li>
|
||||
</ul>
|
||||
{%- endif %}
|
||||
{%- endblock %}
|
||||
{%- if customsidebar %}
|
||||
{% include customsidebar %}
|
||||
{%- endif %}
|
||||
{%- block sidebarsearch %}
|
||||
{%- if pagename != "search" %}
|
||||
<div id="searchbox" style="display: none">
|
||||
{# <h3>{{ _('Quick search') }}</h3> #}
|
||||
<form class="search" action="{{ pathto('search') }}" method="get">
|
||||
<div class="search-wrapper">
|
||||
<span class="search-left"></span>
|
||||
<input class="prettysearch" type="text" name="q" size="18" />
|
||||
<span class="search-right"> </span>
|
||||
</div>
|
||||
<input type="submit" value="{{ _('Search') }}" class="searchbutton" />
|
||||
<input type="hidden" name="check_keywords" value="yes" />
|
||||
<input type="hidden" name="area" value="default" />
|
||||
</form>
|
||||
<p class="searchtip" style="font-size: 90%">
|
||||
{{ _('Enter search terms or a module, class or function name.') }}
|
||||
</p>
|
||||
</div>
|
||||
<script type="text/javascript">$('#searchbox').show(0);</script>
|
||||
{%- endif %}
|
||||
{%- endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{%- endif %}{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
{%- block document %}
|
||||
<div class="documentwrapper">
|
||||
{%- if not embedded %}{% if not theme_nosidebar|tobool %}
|
||||
<div class="bodywrapper">
|
||||
{%- endif %}{% endif %}
|
||||
<div class="body">
|
||||
{% block body %} {% endblock %}
|
||||
</div>
|
||||
{%- if not embedded %}{% if not theme_nosidebar|tobool %}
|
||||
</div>
|
||||
{%- endif %}{% endif %}
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>{%- if hasdoc('copyright') %}
|
||||
{% trans path=pathto('copyright'), copyright=copyright|e %}© <a href="{{ path }}">Copyright</a> {{ copyright }}.{% endtrans %}
|
||||
{%- else %}
|
||||
{% trans copyright=copyright|e %}© Copyright {{ copyright }}.{% endtrans %}
|
||||
{%- endif %}
|
||||
{%- if last_updated %}
|
||||
{% trans last_updated=last_updated|e %}Last updated on {{ last_updated }}.{% endtrans %}
|
||||
{%- endif %}
|
||||
{%- if show_sphinx %}
|
||||
{% trans sphinx_version=sphinx_version|e %}Created using <a href="http://sphinx.pocoo.org/">Sphinx</a> {{ sphinx_version }}.{% endtrans %}
|
||||
{%- endif %}
|
||||
</p>
|
||||
</div>
|
||||
{%- endblock %}
|
||||
{%- block sidebar2 %}{% endblock %}
|
||||
{%- block relbar2 %}{% endblock %}
|
||||
{%- block footer %}
|
||||
<div id="breadcrumbs">
|
||||
{%- for parent in parents %}
|
||||
<a href="{{ parent.link|e }}" {{ accesskey("U") }}>{{ parent.title|safe }}</a><img src="{{ pathto('_static/triangle_closed.png', 1) }}" height="9" width="9" alt=">">
|
||||
{%- endfor %}
|
||||
{{ title|safe|e }}
|
||||
</ul>
|
||||
</div>
|
||||
<script type="text/javascript" charset="utf-8" src="{{ pathto('_static/toc.js', 1) }}"></script>
|
||||
{%- endblock %}
|
||||
8
doc_src/api/index.rst
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
API
|
||||
===
|
||||
|
||||
.. autosummary::
|
||||
:toctree:
|
||||
:recursive:
|
||||
|
||||
categories
|
||||
2
doc_src/changelog.md
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
```{include} ../CHANGELOG.md
|
||||
```
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
from categories.admin import CategoryAdminForm
|
||||
from categories.base import CategoryBaseAdmin
|
||||
|
||||
|
||||
class CategoryAdmin(CategoryBaseAdmin):
|
||||
form = CategoryAdminForm
|
||||
list_display = ('name', 'alternate_title', 'active')
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': ('parent', 'name', 'thumbnail', 'active')
|
||||
}),
|
||||
('Meta Data', {
|
||||
'fields': ('alternate_title', 'alternate_url', 'description',
|
||||
'meta_keywords', 'meta_extra'),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
('Advanced', {
|
||||
'fields': ('order', 'slug'),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
)
|
||||
252
doc_src/conf.py
|
|
@ -1,197 +1,85 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Django Categories documentation build configuration file, created by
|
||||
# sphinx-quickstart on Tue Oct 6 07:53:33 2009.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
"""
|
||||
Sphinx configuration.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import sys
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
sys.path.append(os.path.abspath('..'))
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'example.settings'
|
||||
project_root = Path("..").resolve()
|
||||
sys.path.insert(0, str(project_root / "example"))
|
||||
sys.path.insert(0, str(project_root))
|
||||
os.environ["DJANGO_SETTINGS_MODULE"] = "example.settings"
|
||||
|
||||
# Setup Django
|
||||
import django # NOQA
|
||||
|
||||
django.setup()
|
||||
|
||||
import categories # noqa
|
||||
import categories.urls # noqa
|
||||
|
||||
project = "Django Categories"
|
||||
copyright = f"2010-{date.today():%Y}, Corey Oordt"
|
||||
|
||||
version = categories.__version__
|
||||
release = categories.__version__
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = []
|
||||
extensions = [
|
||||
"myst_parser",
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.viewcode",
|
||||
"sphinx.ext.autosummary",
|
||||
"sphinx.ext.intersphinx",
|
||||
"sphinx.ext.autosectionlabel",
|
||||
"sphinx.ext.napoleon",
|
||||
"sphinx_autodoc_typehints",
|
||||
"sphinx.ext.coverage",
|
||||
"sphinx.ext.githubpages",
|
||||
"sphinxcontrib_django2",
|
||||
]
|
||||
autosectionlabel_prefix_document = True
|
||||
autosectionlabel_maxdepth = 2
|
||||
autosummary_generate = True
|
||||
napoleon_attr_annotations = True
|
||||
napoleon_include_special_with_doc = False
|
||||
napoleon_include_private_with_doc = True
|
||||
napoleon_include_init_with_doc = True
|
||||
myst_enable_extensions = [
|
||||
"amsmath",
|
||||
"colon_fence",
|
||||
"deflist",
|
||||
"dollarmath",
|
||||
"linkify",
|
||||
"replacements",
|
||||
"smartquotes",
|
||||
"substitution",
|
||||
"tasklist",
|
||||
]
|
||||
intersphinx_mapping = {
|
||||
"python": ("https://docs.python.org/3", None),
|
||||
"django": (
|
||||
"https://docs.djangoproject.com/en/stable",
|
||||
"https://docs.djangoproject.com/en/stable/_objects",
|
||||
),
|
||||
}
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
# templates_path = ['_templates']
|
||||
templates_path = ["_templates"]
|
||||
source_suffix = [".rst", ".md"]
|
||||
master_doc = "index"
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
# ource_encoding = 'utf-8'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = 'Django Categories'
|
||||
copyright = '2010-2012, Corey Oordt'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = categories.get_version(short=True)
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = categories.get_version()
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
# anguage = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
# oday = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
# oday_fmt = '%B %d, %Y'
|
||||
|
||||
# List of documents that shouldn't be included in the build.
|
||||
# nused_docs = []
|
||||
|
||||
# List of directories, relative to source directory, that shouldn't be searched
|
||||
# for source files.
|
||||
exclude_trees = ['_build']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all documents.
|
||||
# efault_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
# dd_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
# dd_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
# how_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
# odindex_common_prefix = []
|
||||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||
pygments_style = "sphinx"
|
||||
todo_include_todos = False
|
||||
|
||||
|
||||
# -- Options for HTML output ---------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. Major themes that come with
|
||||
# Sphinx are currently 'default' and 'sphinxdoc'.
|
||||
# html_theme = 'alabaster'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
# tml_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
# tml_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
# tml_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
# tml_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
# tml_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
# tml_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
# tml_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
# tml_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
# tml_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
# tml_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
# tml_use_modindex = True
|
||||
|
||||
# If false, no index is generated.
|
||||
# tml_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
# tml_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
# tml_show_sourcelink = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
# tml_use_opensearch = ''
|
||||
|
||||
# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
# tml_file_suffix = ''
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'DjangoCategoriesdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output --------------------------------------------------
|
||||
|
||||
# The paper size ('letter' or 'a4').
|
||||
# atex_paper_size = 'letter'
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
# atex_font_size = '10pt'
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'DjangoCategories.tex', 'Django Categories Documentation', 'CoreyOordt', 'manual'),
|
||||
html_theme = "pydata_sphinx_theme"
|
||||
html_static_path = ["_static"]
|
||||
html_css_files = [
|
||||
"css/custom.css",
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
# atex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
# atex_use_parts = False
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
# atex_preamble = ''
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
# atex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
# atex_use_modindex = True
|
||||
|
|
|
|||
|
|
@ -6,18 +6,6 @@ Django Categories grew out of our need to provide a basic hierarchical taxonomy
|
|||
|
||||
As a news site, our stories, photos, and other content get divided into "sections" and we wanted all the apps to use the same set of sections. As our needs grew, the Django Categories grew in the functionality it gave to category handling within web pages.
|
||||
|
||||
New in 1.1
|
||||
==========
|
||||
|
||||
* Fixed a cosmetic bug in the Django 1.4 admin. Action checkboxes now only appear once.
|
||||
|
||||
* Template tags are refactored to allow easy use of any model derived from ``CategoryBase``.
|
||||
|
||||
* Improved test suite.
|
||||
|
||||
* Improved some of the documentation.
|
||||
|
||||
|
||||
|
||||
Contents
|
||||
========
|
||||
|
|
@ -28,12 +16,10 @@ Contents
|
|||
|
||||
installation
|
||||
getting_started
|
||||
usage
|
||||
registering_models
|
||||
adding_the_fields
|
||||
admin_settings
|
||||
custom_categories
|
||||
user_guide/index
|
||||
reference/index
|
||||
api/index
|
||||
changelog
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
|
@ -41,4 +27,3 @@ Indices and tables
|
|||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
||||
|
|
|
|||
|
|
@ -35,4 +35,4 @@ drop_category_field
|
|||
|
||||
Drop the ``field_name`` field from the ``app_name_model_name`` table, if the field is currently registered in ``CATEGORIES_SETTINGS``\ .
|
||||
|
||||
Requires Django South.
|
||||
Requires Django South.
|
||||
|
|
|
|||
|
|
@ -128,4 +128,4 @@ ADMIN_FIELDSETS
|
|||
|
||||
**Default:** ``{}``
|
||||
|
||||
**Description:** Allows for selective customization of the default behavior of adding the fields to the admin class. See :ref:`admin_settings` for more information.
|
||||
**Description:** Allows for selective customization of the default behavior of adding the fields to the admin class. See :ref:`admin_settings` for more information.
|
||||
|
|
|
|||
|
|
@ -20,4 +20,4 @@ Reconfiguring Fields
|
|||
|
||||
You can make changes to the field configurations as long as they do not change the underlying database structure. For example, adding a ``related_name`` (see :ref:`registering_a_m2one_relationship`\ ) because it only affects Django code. Changing the name of the field, however, is a different matter.
|
||||
|
||||
Django Categories provides a complementary management command to drop a field from the database (the field must still be in the configuration to do so): ``python manage.py drop_category_field app_name model_name field_name``
|
||||
Django Categories provides a complementary management command to drop a field from the database (the field must still be in the configuration to do so): ``python manage.py drop_category_field app_name model_name field_name``
|
||||
|
|
@ -7,4 +7,4 @@ class SimpleCategory(CategoryBase):
|
|||
"""
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = 'simple categories'
|
||||
verbose_name_plural = "simple categories"
|
||||
|
|
@ -5,29 +5,25 @@ from categories.base import CategoryBase
|
|||
class Category(CategoryBase):
|
||||
thumbnail = models.FileField(
|
||||
upload_to=settings.THUMBNAIL_UPLOAD_PATH,
|
||||
null=True, blank=True,
|
||||
storage=settings.THUMBNAIL_STORAGE,)
|
||||
null=True,
|
||||
blank=True,
|
||||
storage=settings.THUMBNAIL_STORAGE,
|
||||
)
|
||||
thumbnail_width = models.IntegerField(blank=True, null=True)
|
||||
thumbnail_height = models.IntegerField(blank=True, null=True)
|
||||
order = models.IntegerField(default=0)
|
||||
alternate_title = models.CharField(
|
||||
blank=True,
|
||||
default="",
|
||||
max_length=100,
|
||||
help_text="An alternative title to use on pages with this category.")
|
||||
blank=True, default="", max_length=100, help_text="An alternative title to use on pages with this category."
|
||||
)
|
||||
alternate_url = models.CharField(
|
||||
blank=True,
|
||||
max_length=200,
|
||||
help_text="An alternative URL to use instead of the one derived from "
|
||||
"the category hierarchy.")
|
||||
help_text="An alternative URL to use instead of the one derived from " "the category hierarchy.",
|
||||
)
|
||||
description = models.TextField(blank=True, null=True)
|
||||
meta_keywords = models.CharField(
|
||||
blank=True,
|
||||
default="",
|
||||
max_length=255,
|
||||
help_text="Comma-separated keywords for search engines.")
|
||||
blank=True, default="", max_length=255, help_text="Comma-separated keywords for search engines."
|
||||
)
|
||||
meta_extra = models.TextField(
|
||||
blank=True,
|
||||
default="",
|
||||
help_text="(Advanced) Any additional HTML to be placed verbatim "
|
||||
"in the <head>")
|
||||
blank=True, default="", help_text="(Advanced) Any additional HTML to be placed verbatim " "in the <head>"
|
||||
)
|
||||
|
|
@ -3,8 +3,9 @@ from categories.models import Category
|
|||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.thumbnail:
|
||||
from django.core.files.images import get_image_dimensions
|
||||
import django
|
||||
from django.core.files.images import get_image_dimensions
|
||||
|
||||
if django.VERSION[1] < 2:
|
||||
width, height = get_image_dimensions(self.thumbnail.file)
|
||||
else:
|
||||
|
|
@ -2,8 +2,8 @@ from categories.base import CategoryBase
|
|||
|
||||
|
||||
class Meta(CategoryBase.Meta):
|
||||
verbose_name_plural = 'categories'
|
||||
verbose_name_plural = "categories"
|
||||
|
||||
|
||||
class MPTTMeta:
|
||||
order_insertion_by = ('order', 'name')
|
||||
order_insertion_by = ("order", "name")
|
||||
|
|
@ -7,7 +7,7 @@ class CategoryAdminForm(CategoryBaseAdminForm):
|
|||
model = Category
|
||||
|
||||
def clean_alternate_title(self):
|
||||
if self.instance is None or not self.cleaned_data['alternate_title']:
|
||||
return self.cleaned_data['name']
|
||||
if self.instance is None or not self.cleaned_data["alternate_title"]:
|
||||
return self.cleaned_data["name"]
|
||||
else:
|
||||
return self.cleaned_data['alternate_title']
|
||||
return self.cleaned_data["alternate_title"]
|
||||
24
doc_src/user_guide/code_examples/custom_categories7.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
from categories.admin import CategoryAdminForm
|
||||
from categories.base import CategoryBaseAdmin
|
||||
|
||||
|
||||
class CategoryAdmin(CategoryBaseAdmin):
|
||||
form = CategoryAdminForm
|
||||
list_display = ("name", "alternate_title", "active")
|
||||
fieldsets = (
|
||||
(None, {"fields": ("parent", "name", "thumbnail", "active")}),
|
||||
(
|
||||
"Meta Data",
|
||||
{
|
||||
"fields": ("alternate_title", "alternate_url", "description", "meta_keywords", "meta_extra"),
|
||||
"classes": ("collapse",),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Advanced",
|
||||
{
|
||||
"fields": ("order", "slug"),
|
||||
"classes": ("collapse",),
|
||||
},
|
||||
),
|
||||
)
|
||||