diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..585e2ab --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.gitchangelog.rc b/.gitchangelog.rc new file mode 100644 index 0000000..47ebf32 --- /dev/null +++ b/.gitchangelog.rc @@ -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 could be any of the available templates in +# ``templates/mustache/*.tpl``. +# Requires python package ``pystache``. +# Examples: +# - mustache("markdown") +# - mustache("restructuredtext") +# +# - makotemplate() +# +# 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\#\#\s+(?P%(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") + + +# ``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" +] diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml new file mode 100644 index 0000000..778d218 --- /dev/null +++ b/.github/workflows/publish-docs.yml @@ -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 diff --git a/.github/workflows/publish-package.yml b/.github/workflows/publish-package.yml new file mode 100644 index 0000000..bc6333c --- /dev/null +++ b/.github/workflows/publish-package.yml @@ -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 }} diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml deleted file mode 100644 index c4d1b54..0000000 --- a/.github/workflows/python-package.yml +++ /dev/null @@ -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" diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml new file mode 100644 index 0000000..5d979a8 --- /dev/null +++ b/.github/workflows/run-tests.yaml @@ -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 }} diff --git a/.gitignore b/.gitignore index 74065d1..2948063 100644 --- a/.gitignore +++ b/.gitignore @@ -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/* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..5da5536 --- /dev/null +++ b/.pre-commit-config.yaml @@ -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 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8ac2512..0000000 --- a/.travis.yml +++ /dev/null @@ -1,12 +0,0 @@ -sudo: false - -dist: xenial - -language: python - -python: - - "3.6" - -install: pip install tox-travis - -script: tox diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6159a28 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,1228 @@ +# Changelog + + +## Unreleased (2021-12-05) + +### New + +* Added pre-commit configuration and configuration for the tools. [Corey Oordt] + +* Added XML reporting for code coverage. #164. [Corey Oordt] + +* Added codecov uploading to Tox. #164. [Corey Oordt] + +* Adds GitHub Actions to run Tox. #164. [Corey Oordt] + +* Added contributing documentation. #164. [Corey Oordt] + +* Added Jazzband badge to README. [Corey Oordt] + + Also changed it to Markdown. + +### Updates + +* Changed codecov uploading pattern. [Corey Oordt] + +* Updated tox to run coverage commands together. [Corey Oordt] + +* Updated references to Python 3.10. #164. [Corey Oordt] + +* Updated gitignore to be more ignorative. [Corey Oordt] + +* Updates URL of the project to Jazzband. #164. [Corey Oordt] + +### Other + +* Removed Python 3.10 testing until compatibility established. [Corey Oordt] + + +## 1.8.0 (2020-08-31) + +### New + +* Add support for Django 3.1. [gantonayde] + + In Django 3.1 the compatibility import of django.core.exceptions.FieldDoesNotExist in django.db.models.fields is removed. + + So we'd have to update the package by replacing: + from django.db.models.fields import FieldDoesNotExist + with + from django.core.exceptions import FieldDoesNotExist + +### Updates + +* Update the version to 1.8. [Brent O'Connor] + +* Update tox tests to run Django 3.1 and removed support for Python 2.7. [Brent O'Connor] + +### Other + +* Remove Python 2.7 from the Travis config. [Brent O'Connor] + +* Django-mptt 0.11 needed for Django 3.1. [gantonayde] + + In Django 3.1 the compatibility import of django.core.exceptions.FieldDoesNotExist in django.db.models.fields is removed. + django-mptt should be the latest version (0.11 as of now) + + +## 1.7.2 (2020-05-18) + +### Updates + +* Update publish make task. [Brent O'Connor] + +### Fix + +* Fix #152. [Petr Dlouhý] + +### Other + +* Include missing migration. [Carlos Cesar Caballero Díaz] + +* Ignore .python-version. [Brent O'Connor] + + +## 1.7.1 (2020-03-06) + +### New + +* Add missing migrations. [Brent O'Connor] + + +## 1.7.0 (2020-02-04) + +### New + +* Add newer Django versions to tox.ini. [Petr Dlouhý] + +### Updates + +* Update django-mptt. [Petr Dlouhý] + +* Update `make publish` [Brent O'Connor] + +### Fix + +* Fixes to Django 3.0. [Petr Dlouhý] + + +## 1.6.1 (2019-06-26) + +### New + +* Adding opts to context for Django version 2 and above. [Gagandeep Singh] + +* Django 2.0 support in Admin. [Gagandeep Singh] + + TypeError at /admin/categories/category/ + __init__() missing 1 required positional argument: 'sortable_by' + +### Updates + +* Update Travis. [Brent O'Connor] + + * travis.yml + * build badge + * Remove 3.7 from Travis config since it doesn't look like it's supported + * Switch to tox-travis. + +* Updated tree editor for typo. [Gagandeep Singh] + +* Upgrade build environment to Xenial. [Brent O'Connor] + + This makes it so Django 2.2 tests should pass + +### Fix + +* Fix Travis so it uses the correct python versions. [Brent O'Connor] + +* Fix Travis so it works with Python 3.7. [Brent O'Connor] + +* Fix 'Models aren't loaded yet' warning on import. [Frankie Dintino] + + categories.registration._process_registry was being called in + categories/__init__.py, but since Django 1.9 it hasn't been possible to + perform operations with models until the app registry is fully + loaded. Currently the `AppRegistryNotReady` exception is being caught + and printed, which means it is never actually executed on load. + + Since this code isn't currently doing anything (other than emitting a + print() of a warning), I've removed it. + +* Fix tests. [Brent O'Connor] + + Also dropped testing Django 1.10 since django-mptt requires Django>=1.11. + +* Fix for TOXENV=py27-lint. [Gagandeep Singh] + +* Fixing model for TOXENV=py36-django110. [Gagandeep Singh] + +* Py27-lint test fix. [Gagandeep Singh] + +* Test Cases fix. [Gagandeep Singh] + +* Bug Fix : sortable was last argument. [Gagandeep Singh] + + + +## 1.6.0 (2018-02-27) + +### Updates + +* Updated the Travis CI config. [Brent O'Connor] + +* Changed from using a string to importing the actual CASCADE function. [Brent O'Connor] + +### Other + +* Proposes changes based on 366ff74619811505ac73ac5ea2c0096ddab0ac51 and pull request #140 for Django 2.0 to pass CI tests. [goetzb] + +* Made updates to get everything working with Django 2. [Brent O'Connor] + + +## 1.5.4 (2017-10-13) + +### New + +* Django 1.11 compatibility. [Hodossy Szabolcs] + +* Support Django 1.11 testing environment. [Egor] + +* Add migrations for simpletext example app. [Corey Oordt] + +### Fix + +* Fix changlist TypeError. Return RequestContext as dict on changelist_view. [Egor] + + * Based on [changes in Django 1.11](https://docs.djangoproject.com/en/1.11/releases/1.11/#django-template-backends-django-template-render-prohibits-non-dict-context) + +* Get management commands compatible with Django 1.10+ [Corey Oordt] + +### Updates + +* Updated test settings to test generic relations. [Corey Oordt] + +* Updated tox and travis configurations to check py2.7 and 3.6 and django 1.8-1.11. [Corey Oordt] + + +### Other + +* Made sure example was excluded from packaging. [Corey Oordt] + +* Remove old django-cbv reference and adds better error checking in views. [Corey Oordt] + +* Retrieve content types lazily in Generic Relations admin. [Corey Oordt] + +* Check for a valid session id before trying to save or rollback a transaction. [Corey Oordt] + +* Added additional test coverage for management commands and views. [Corey Oordt] + +* Remove south migrations. [Corey Oordt] + +* Set decendent_ids to empty list if not saved. [Corey Oordt] + +* Removing every occurrence of Requestcontext and Context. [Hodossy Szabolcs] + + +## 1.5.3 (2017-03-31) + +### Fix + +* Fixed a ValueError that happened when trying to save a Category that has a thumbnail. [Brent O'Connor] + +### Other + +* Version bump. [Brent O'Connor] + + +## 1.5.2 (2017-03-29) + +### Fix + +* Fixed a unicode error that happens with Python 2.7. [Brent O'Connor] + + +## 1.5.1 (2017-02-17) + +### New + +* Added a missing migration. [Brent O'Connor] + +### Updates + +* Updated README.rst with svg badge. [Sobolev Nikita] + +### Fix + +* Close table tag in templatetag result. [Dheeraj Sayala] + + In items_for_tree_result, there's a format_html call which builds HTML via string interpolation. It missed back slash in the closing tag. This commit adds that. + +### Other + +* Just to be safe - pin it down. [Primož Verdnik] + + +## 1.5 (2016-11-14) + +### Updates + +* Updated the Travis config to test for Django 1.10. [Brent O'Connor] + +* Updated django-categories to work with Django 1.10. [Brent O'Connor] + + +## 1.4.3 (2016-10-21) + +### Fix + +* Fixes popup raw_id fields for django versions 8 or greater. [Jordan Roth] + + +## 1.4.2 (2016-04-19) + +### Fix + +* Fixed it so display_for_field works with Django 1.8 and 1.9. [Brent O'Connor] + + +## 1.4.1 (2016-03-31) + +### New + +* Added setup.cfg file for creating universal wheel distribution. [Brent O'Connor] + +* Added coverage to tox. [Brent O'Connor] + +* Added some tests to test the admin. [Brent O'Connor] + +* Added a makefile for common tasks. [Brent O'Connor] + +### Updates + +* Updated the new in 1.4 information. [Brent O'Connor] + +### Fix + +* Fixed an exception error that happens when saving a category in the admin. [msaelices] + +* Removed some RemovedInDjango110Warning warnings. [Brent O'Connor] + +### Other + +* Moved all template settings for the example app into the TEMPLATES Django setting. [Brent O'Connor] + +* Avoid the "Cannot call get_descendants on unsaved Category instances" ValueError when adding categories in admin interface. [msaelices] + +* Removed contributors from the README since that information is in CREDITS.md. No sense maintaining it two places. [Brent O'Connor] + + +## 1.4 (2016-02-15) + +### New + +* Added a tox.ini and updated the travis config to work with tox. [Brent O'Connor] + +### Updates + +* Updated admin_tree_list_tags so that EMPTY_CHANGELIST_VALUE has a compatible way of working with Django 1.9 and older versions. [Brent O'Connor] + +* Updated urls to work without patterns since patterns is being deprecated. [Brent O'Connor] + +* Updated settings to remove all the TEMPLATE_* settings and put them into the TEMPLATES dict for Django 1.9 compatibility. [Brent O'Connor] + +* Changed __unicode__ to __str__ on the CategoryBase class for Python 3 compatibility. [Brent O'Connor] + +* Upgraded to django-mptt 0.8. [Brent O'Connor] + +* Switched to using _meta.get_fields() instead of ._meta.get_all_field_names() for compatibility with Django 1.9. [Brent O'Connor] + +* Replaced django.db.models.get_model with django.apps.apps.get_model for future compatibility with Django. [Brent O'Connor] + +* Switched to importing the correct templatetags that got renamed. [Brent O'Connor] + +* Switched form using smart_unicode to smart_text and force_unicode to force_text. [Brent O'Connor] + +* Switched from using django.db.models.loading.get_model to using django.apps.apps.get_model. [Brent O'Connor] + +* Switched form using force_unicode to force_text. [Brent O'Connor] + +* Use singleton `registry` to import `register_fk` and `register_m2m` since they are members on `Registry` class. [Orestes Sanchez] + +### Fix + +* Fixed the max_length setting to use a int instead of a string. [Brent O'Connor] + +* Fixed a test: file() doesn't work in Python 3, used open() instead. [Brent O'Connor] + +* Made a bunch of flake8 fixes and also added flake8 testing to Travis and Tox. [Brent O'Connor] + +* Made a fix for backwards compatibility with Python 2. [Brent O'Connor] + +* B'' doesn't work under Python 3 in a migration file. [Brent O'Connor] + +### Other + +* Ran the 2to3 script `2to3 -w .` [Brent O'Connor] + +* Ugettext may cause circular import. [Basile LEGAL] + +* Run the test with a different configuration. [Orestes Sanchez] + + +## 1.3 (2015-06-09) + +### New + +* Added the fields property with it set to '__all__' in order to not get the RemovedInDjango18Warning. [Brent O'Connor] + +* Defaulting the url prefix to / if it can't find the category tree. [Corey Oordt] + +* I18n: add french translation. [Olivier Le Brouster] + +### Updates + +* Updates the existing migration to south_migrations. [Corey Oordt] + +* Renamed get_query_set to get_queryset to get Django categories to work in Django 1.7. I'm not sure of a good way to make this work in Django 1.6. [Brent O'Connor] + +* Migrations + + * Dramatically refactored how migrations are performed to work with Django 1.7. [Corey Oordt] + + * Missed some migrations. [Jose Soares] + + * Changing migration dependency of contenttypes to 0001_initial for support for Django 1.7. [Corey Oordt] + +### Fix + +* Fixes potential double imports in dev and test servers. [Corey Oordt] + +* Fixed a potential issue with double-loading of the dev server. [Corey Oordt] + +* Fixes a conflict with treebeard. They stole the name admin_tree_list. [Corey Oordt] + +* Fixed the RemovedInDjango19Warning deprecation warning. [Brent O'Connor] + +* Fixed tests so they run under Django 1.7. [Brent O'Connor] + +* fixes registration when there is no app config. [Corey Oordt] + +* [-] Fixed some tree editor and generic collection issues. [Jose Soares] + +### Other + +* Removing outdated settings and updating outdated files. [Corey Oordt] + +* [-] 1.6/1.7/1.8 compatiable changes (WIP) [Jose Soares] + + +## 1.2.3 (2015-05-05) + +### New + +* Added a new way to register models manually. [Corey Oordt] + +* Bootstrap class on table (important for django-suit) [Mirza Delic] + +### Updates + +* Update requirements. [Sina Samavati] + +* Using custom model in CategoryDetailView. [Enver Bisevac] + +### Fix + +* Fix unicode slug issue. [Sina Samavati] + + +## 1.2.2 (2013-07-07) + +### New + +* Italian localization. [Iacopo Spalletti] + +### Fix + +* Fixing migration script for adding fields. [Corey Oordt] + +* Fixed i18n and failing test in Django 1.4. [Corey Oordt] + +### Other + +* Load I18N templatetags. [Eugene] + + +## 1.2.1 (2013-03-22) + +### Fix + +* Fixed i18n and failing test in Django 1.4. [Corey Oordt] + + +## 1.2 (2013-03-20) + +### New + +* Added admin settings documentation. [Corey Oordt] + +* Added customization of admin fieldsets. [Corey Oordt] + +### Updates + +* Update categories/templatetags/category_tags.py. [Glen] + + * Added NoneType check to display_drilldown_as_ul on line 188 to fix NoneType error. + + * Added str() to line 49 to fix an error where .strip("'\"") in get_category is getting called on a non-string category_string. + +* Made updates so django-categories works with django-grappelli. [Brent O'Connor] + +* Updated the code so it will work with or without Grappelli installed. [Brent O'Connor] + +### Fix + +* Fixing a few minor Django 1.5 incompatibilities. [Corey Oordt] + +* Fix for Django 1.5: {% url %} parameter needs to be quoted. [Corey Oordt] + +* Fixed an exception error. [Brent O'Connor] + + Fixed an exception error that occurs when an empty form is submitted for apps that are created using categories.base.CategoryBase. + +### Other + +* Version bump 1.2. [Corey Oordt] + +* Updating the admin template to support the latest django admin code. [Corey Oordt] + +* I18n. [winniehell] + +* German translation. [winniehell] + +* 1.5 compat: remove adminmedia templatetag calls. [Yohan Boniface] + + See https://docs.djangoproject.com/en/1.5/releases/1.5/#miscellaneous + +* Made it so django-categories works with Django 1.5 and Grappelli 2.4.4. [Brent O'Connor] + +* Simplified the assignment of the IS_GRAPPELLI_INSTALLED variable. [Brent O'Connor] + + + +## 1.1.3 (2012-08-29) + +### Other + +* To satisfy a very demanding and owly jsoa, I removed an unused variable. :P. [Corey Oordt] + +* Updating the signal registration to check for south first and fail silently. [Corey Oordt] + +* Moved the registration of the signal to models.py where it will get executed. [Corey Oordt] + +* Refactored the migration script to use the syncdb signal. The post_migrate signal only fires for south-managed apps, so it isn't as useful. [Corey Oordt] + + +## 1.1.2 (2012-08-18) + +### New + +* Added travisci. [Jose Soares] + +### Fix + +* Fixed a bug in the compatibility layer. [Corey Oordt] + +* Minor tweak to tempatetag tests. [Jose Soares] + +### Other + +* Can't use the m2m tests because it conflicts with the fk tests. [Corey Oordt] + +* Placing some south imports into try blocks. [Corey Oordt] + +* Capitalizing the various REGISTRY settings. [Corey Oordt] + +* Refactored the registration of fields from __init__ to a new module. [Corey Oordt] + + It also makes it easier to test. + + +## 1.1 (2012-07-12) + +### New + +* Added Brad Jasper to the credits and updated Jonathan's github account. [Corey Oordt] + +* Added queryset parameter to ChangeList.get_ordering() [Brad Jasper] + +### Updates + +* Updated read me and version bump to 1.1. [Corey Oordt] + +* Updated and rendered docs. [Corey Oordt] + +* Update to template tags to include ways to retrieve an object from a model other than Category. [Corey Oordt] + +* Updated the credits to add Iacopo Spalletti. [Corey Oordt] + +* Updated CREDITS, docs. [Jose Soares] + +### Fix + +* Fixed an incorrect include in the example. [Corey Oordt] + +* Fixed some Django 1.4 cosmetic issues. [Corey Oordt] + +* Fixes Pull Request #37 Adds notification in the readme regarding issue with South version 0.7.4. [Corey Oordt] + +* Fixed format error. [Iacopo Spalletti] + +* Fixes issue #40 Checks for instance of CategoryBase instead of Category. [Corey Oordt] + + There are still some template tags that won't work with subclasses. Need a better solution for those tags. + +### Other + +* Template tags now work with any derivative of CategoryBase. Recognizes the "using" param to specify the model to use. [Corey Oordt] + +* Sorry, typo in documentation. [Iacopo Spalletti] + +* Documented the upgrade path from 1.0.2 and 1.0.3 plus a small migration to keep things in sync. [Iacopo Spalletti] + +* Stylistic fixes and docs. [Martin Matusiak] + +* Make it optional to register in admin. [Martin Matusiak] + +* Use ugettext_lazy. [Martin Matusiak] + +* Minor fix to example app. [Jose Soares] + + +## 1.0.3 (2012-03-28) + +### New + +* Adding additional migrations to fix potential data corruption when renaming the foreign key. [Corey Oordt] + +### Fix + +* Fixed another migration. [Corey Oordt] + +* Altering the #10 migration as it caused strange behavior with data. [Corey Oordt] + + +## 1.0.1 (2012-03-09) + +### Other + +* Importing get_model directly from the loading module appears to fix certain edge cases. [Corey Oordt] + + +## 1.0.2 (2012-03-06) + +### Fix + +* Fixed how the activate/deactivate methods in the admin fetched their models. [Corey Oordt] + +* Fix for django 1.4 compatibility. [Corey Oordt] + +### Other + +* Removed an errant print statement. [Corey Oordt] + + +## 1.0 (2012-02-15) + +### New + +* Added compatibility with Django 1.4. [Corey Oordt] + +* Allow the setting of a SLUG_TRANSLITERATOR to convert non-ASCII characters to ASCII characters. [Corey Oordt] + +### Updates + +* Updated documentation for 1.0b1. [Corey Oordt] + +* Updated migrations to include a data migration. [Corey Oordt] + +* Updated the default view caching to 600, which is the django default instead of forcing the views to NEVER cache at all. [Corey Oordt] + +* Updating docs to correct and simplify the simple custom categories instructions. [Corey Oordt] + +### Fix + +* Also fixes #30 by including the editor's media. [Corey Oordt] + +* Formally fixes #1 by adding the ability to specify a transliteration function. [Corey Oordt] + +* Addresses issue #27; updated musicgenres.json. [Jose Soares] + +* The admin prior to 1.4 requires a different result from get_ordering. [Corey Oordt] + +* This fixes #31. [Corey Oordt] + + * Uses the incorrect version segment. Although it works in 1.4a1, it is not perfect. + +### Other + +* Removed the __init__ method for the treechange list. Don't need it and it varies too much by django version. [Corey Oordt] + + +* Test of the CategoryBase class subclassed without extras. [Corey Oordt] + +* Moved the base models to base.py and did a few PEP8 cleanups. [Corey Oordt] + +* Moved the base classes to a new file to isolate them. [Corey Oordt] + +* Refactored the admin into a base class for subclasses. [Corey Oordt] + +* Extracted a base class for categories to allow other apps to make their own independent category-style models. [Corey Oordt] + + * Updated for django-mptt 0.5.2 + * Fixed typo in the CategoryRelation field in that the foreign key is called 'story' + * Made the order field non-null and default to 0 + * Changed the parent foreign key a TreeForeignKey (for 0.5.2) + * Changed requirements to mptt>=0.5.2 + * Added a migration for model changes. + + +## 0.8.9 (2012-02-06) + +### Updates + +* Updated the docs. [Jose Soares] + +### Fix + +* Fixes issue #30; includes static directory when packaged. [Jose Soares] + +### Other + +* Moved the editor app so it's inside the categories app. [Jose Soares] + + +## 0.8.7 (2012-01-05) + +### Updates + +* Changed behavior of (de)activating an item within the change form: [Corey Oordt] + + Instead of changing all descendants' active status to the current item's, it will only change the descendants' active status if the item is False. + + As it makes sense to have an item active, but its children inactive, it doesn't make sense that an item is inactive, but its descendants are active. + + This doesn't change the activate/deactivate admin actions. They will always affect an item and its descendants. + + +## 0.8.6 (2012-01-03) + +### New + +* Added a django/jQuery stub for previous versions of Django. [Corey Oordt] + +* Added David Charbonnier to the credits. [Corey Oordt] + +### Fix + +* Fixes #13 : Documented installation and re-rendered the docs. [Corey Oordt] + +* Fix missing imports. [David Charbonnier] + +### Other + +* Altered the field type of the alternate url field from URL to Char. This allows relative urls, instead of full urls. [Corey Oordt] + + Added a migration in case the database complains. Really doesn't do anything on that level + + +## 0.8.5 (2011-11-03) + +### Fix + +* Fixes issue #26 by limiting the slug to the first 50 characters. [Corey Oordt] + + +## 0.8.4 (2011-10-14) + +### New + +* Added a version check to support Django 1.1 in a core Django function. [Corey Oordt] + + +## 0.8.3 (2011-10-13) + +### Other + +* Activate and Deactivate of a child no longer (de)activates their parent. [Corey Oordt] + + The query set includes the entire hierarchy. So manually get the categories based on the selected items. Then do them and their children + +* Remove the delete action from the available actions. [Corey Oordt] + + +## 0.8.2 (2011-09-04) + +### Updates + +* Updated docs adding usage in templates and rendered. [Corey Oordt] + +### Fix + +* Fix Issue #25 : The override of __getitem__ was causing issues with analysis of query sets, [Corey Oordt] + + +## 0.8.1 (2011-08-29) + +### Fix + +* Fixes a bug trying to set active on decendants before object is saved. [Corey Oordt] + + +## 0.8 (2011-08-22) + +### New + +* Added to the README. [Corey Oordt] + +* Added an active flag for models. [Corey Oordt] + +### Other + +* Improved Category import. [Corey Oordt] + + +## 0.7.2 (2011-08-19) + +### New + +* Added a check in migrate_app to see if the app is a string or not. [Corey Oordt] + +### Updates + +* Updated the get_version function to be PEP 386 compliant. [Corey Oordt] + +* Changed the DatabaseError import to be more compatible. [Corey Oordt] + +* Updated the readme. [Corey Oordt] + +### Other + +* Pruning the example project. [Corey Oordt] + +* Refactored the editor to become Django 1.1.1 compatible and some PEP8 formatting. [Corey Oordt] + +* Ensure that the slug is always within the 50 characters it needs to be. [Corey Oordt] + + +## 0.7.1 (2011-08-03) + +### Other + +* Due to settings, the migration for the category relations table never would be created. This fixes it. [Corey Oordt] + + +## 0.7 (2011-08-02) + +### New + +* Added a setting for the JAVASCRIPT_URL to make placement of the genericcollections.js file easier. [Corey Oordt] + +* Added compatibility with Django 1.1 by adding missing methods for editor and bumped version to 0.7beta2. [Corey Oordt] + +* Added a get_latest_objects_by_category template tag. Might be useful. [Corey Oordt] + +* Added the ability to add the appropriate fields to a table if configured after an initial syncdb. [Corey Oordt] + +* Added an alternate url field to the model. [Corey Oordt] + +* Added the alternate_url to the admin. [Corey Oordt] + +### Updates + +* Updated and rendered docs. [Corey Oordt] + +* Updated the gitignore for venv file. [Corey Oordt] + +* Altered the inline template to display the content_object instead of the __unicode__ of the middle table. [Corey Oordt] + +* Updating the documentation. [Corey Oordt] + +### Fix + +* Fixed a typo in the docs. [Corey Oordt] + +* [Fixes issue #23] Changes the way the tree shows items when searched. Doesn't hide them in the template. [Corey Oordt] + +* Fixed a bug in the javascript. [Corey Oordt] + +### Other + +* Refactored the registry into a registry of models and fields. This will make it easier for migrations. [Corey Oordt] + +* Deleted old migration scripts since they were migrated to south. [Corey Oordt] + + +## 0.6 (2011-05-18) + +### New + +* Added a Deprecation warning for CATEGORIES_RELATION_MODELS. [Corey Oordt] + +* Adding South migrations. [Corey Oordt] + +* Added some specialized functions for relations. [Corey Oordt] + +* Added a class based view for the detail page of a model related to a category. [Erik Simmler] + +* Added a view that list items of specific model that are related to the current category. [Erik Simmler] + +* Added a class based CategoryDetailView that should be functionally identical to the original function based view. [Erik Simmler] + +* Add optional thumbnail model field. [Evan Culver] + +### Updates + +* Updated docs. [Corey Oordt] + +* Updated README. [Corey Oordt] + +* Updated some of the setup info. [Corey Oordt] + +### Fix + +* Fixed a problem in the new admin creation where it wouldn't properly filter out the category fields by model. [Corey Oordt] + +* [FIXED Issue #17] Refactored how the HTML is rendered, removing the checkbox from the tag and pulling the parent checkbox from the row class. [Corey Oordt] + +* Fixed the deprecated settings in the example app. [Corey Oordt] + +* Fixed small errors in templatetags documentation and docstrings. [Ramiro Morales] + +* Fixed wrong var name in import_categories command. [Andrzej Herok] + +* Fixed the homepage in the setup.py. [Corey Oordt] + +### Other + +* Final doc rendering. [Corey Oordt] + +* Enabled new registry in the example app for testing. [Corey Oordt] + +* The registry default settings needs to be an empty dict, not list. [Corey Oordt] + +* Enable registering of models in settings. [Corey Oordt] + +* Putting registry outside of the try block. [Corey Oordt] + +* Updating settings for Django 1.3. [Corey Oordt] + +* Refactored the thumbnail from imagefield to filefield. [Corey Oordt] + + Why? ImageField causes hits to storage to fill out certain fields. Added a storage class and width/height fields so it is possible to scale the thumbnails and store them somewhere besides the filesystem. + +* Allow for using django-cbv in Django 1.2.x. [Corey Oordt] + +* Slight refactor of the default settings to clean it up. [Corey Oordt] + +* Filled out all contributors. [Corey Oordt] + +* Moved path to category code into its own function to make reuse easier. [Erik Simmler] + +* Remove 'to' from kwargs in CategoryM2MField and CategoryFKField. 'to' is already specified, and causes errors when running unit tests. [Martin Ogden] + +* Make admin js relative to MEDIA_URL. [Evan Culver] + +* Make the initial state of the editor tree an app setting with collapsed as the default. [Erik Simmler] + + +## 0.5.2 (2011-02-14) + +### Other + +* Removed the raising of an exception when it finds a model that is already registered. [Corey Oordt] + + +## 0.5.1 (2011-02-14) + +### Updates + +* Updated the test to test a new template tag, not the old one. [Corey Oordt] + +* Changed the import to import from category_import. [Corey Oordt] + +### Other + +* The test for importing checks the first child. With two children either could be 1st, so remove one. [Corey Oordt] + +* Need to delete all the objects before each test because the import checks its work. [Corey Oordt] + +* Checking for raising the correct exception and moved the strings used in the test to a list of strings. [Corey Oordt] + +* Got rid of the debugging print statements. [Corey Oordt] + + +## 0.5 (2011-01-20) + +### New + +* Added contributors to the readme for proper recognition. [Corey Oordt] + +* Added logic to skip adding categories that are already defined for a modeladmin. [Erik Simmler] + +* Added additional fields to the display_list. [Corey Oordt] + +* Adding a new import and alphabetizing them (OCD, I know) [Corey Oordt] + +* Added a new template tag to override the painting of the admin rows. [Corey Oordt] + +* New template and media. [Corey Oordt] + +* Added a placeholder for Django. [Corey Oordt] + +* Adding a new version of TreeTable with a few minor changes to support row repainting. [Corey Oordt] + +### Updates + +* Updated the documentation! [Corey Oordt] + +* Updated the docstrings of the template tags and added breadcrumbs. [Corey Oordt] + +### Other + +* STATIC_URL seems to be returning as None even when not defined. [Erik Simmler] + +* Renamed 'media' directories to 'static' to work with the django 1.3 staticfiles app. [Erik Simmler] + +* Removed duplicate slash from EDITOR_MEDIA_PATH setting. [Erik Simmler] + +* ModelAdmin re-register now skips modeladmins without fieldsets already defined. [Erik Simmler] + + Was causing a "TypeError at /current/url/: unsupported operand type(s) for +: 'NoneType' and 'tuple'" + +* Got rid of the with_stories keyword for the category detail view. [Corey Oordt] + +* Revised the README to get it up-to-date. [Corey Oordt] + +* Refactored the templates to extend a categories/base.html. [Corey Oordt] + +* Renamed the README to indicate it is a reST file. [Corey Oordt] + +* Long trees cause a performance hit if the initial state is expanded. Changing to "collapsed" [Corey Oordt] + +* Getting rid of unused code in the treeeditor. [Corey Oordt] + +* Ignoring a few more things. [Corey Oordt] + +* Made the media delivery work. [Corey Oordt] + +* Removed some unused cruft from the TreeEditor class. [Corey Oordt] + +* What's that doing there? [Corey Oordt] + +* Now that Django has a getchangelist function, we don't need to hack anymore. [Corey Oordt] + +* Don't need to set that EDITOR_MEDIA_PATH any more. [Corey Oordt] + +* Reworked the template to initialize the correct javascript and use the result_tree_list. [Corey Oordt] + +* Deleted an unused template. [Corey Oordt] + +* Got rid of hotlinking settings and changed the EDITOR_MEDIA_PATH. [Corey Oordt] + +* Removed unused code files. [Corey Oordt] + +* Removed all the old, unused templates. [Corey Oordt] + +* Removed all the old media. [Corey Oordt] + + + +## 0.4.8 (2010-12-10) + +### New + +* Added a Meta class for proper plural naming. [Corey Oordt] + +### Updates + +* Updated the requirements to django-mptt 0.4.2. [Corey Oordt] +* Modified Category model to work with django-mptt 0.4. [Josh Ourisman] + +### Fix + +* Fixing bug #6 per primski. Adds the correct fields into the admin instead of both. [Corey Oordt] + +### Other + +* PyPI didn't like the license metadata. [Corey Oordt] + + +## 0.4.6 (2010-10-07) + +### Other + +* Bumped version to 0.4.6. [Corey Oordt] + + +## 0.4.5 (2010-10-07) + +### Fix + +* Fix fieldsets assignment, issue 3. [Justin Quick] + +* Category string, fixes issue 2. [Justin Quick] + +### Other + +* Checks for parent if given enough path bits. [Justin Quick] + + +## 0.4.4 (2010-05-28) + +### New + +* Added the extra templates. [Corey Oordt] + +* Added extra context to view func. [Justin Quick] + +### Other + +* Redid docs with new template. [Corey Oordt] + +* Refactoring docs into doc_src and docs. [Corey Oordt] + +* Require a trailing slash at the end of urls. [Corey Oordt] + +* Safe mptt registration. [Justin Quick] + + +## 0.4.2 (2010-04-28) + +### Updates + +* Updated the version number. [Corey Oordt] + +### Fix + +* Fixing jquery issues. [Corey Oordt] + +### Other + +* Fied my typo for settings url. [Web Development] + + +## 0.4 (2010-04-23) + +### New + +* Added the necessary files to test the generic relations. [Corey Oordt] + +* Added generic relation stuff into categories. [Corey Oordt] + +### Other + +* Renamed sample to example because that is what every other one is called, damnit. [Corey Oordt] + + +## 0.3 (2010-04-23) + +### New + +* Added metadata to the model for seo stuff. [Corey Oordt] + +### Updates + +* Changed the requirements from mptt in our repository to mptt-2 in pypi. [Corey Oordt] + + +## 0.2.2 (2010-04-08) + +### New + +* Added better setup.py pieces. Getting ready to push to our PyPi. [Corey Oordt] + +### Updates + +* Changed the requirements to have mptt just greater than 0.2. [Corey Oordt] + +### Other + +* Switched to setuptools/distribute. [Corey Oordt] + +* Deleted code referencing something I deleted earlier. [Corey Oordt] + +* Removing docs for piece I deleted previously. [Corey Oordt] + + + +## 0.2.1 (2010-04-06) + +### New + +- Added some docs and testing apps. [Corey Oordt] +- Added a caching setting to vary the amount of time the view is cached. + [Corey Oordt] +- Added missing templates for category traversal. [Justin Quick] +- Added an app to test categories against. [Corey Oordt] +- Added some registration notes to start the docs. [Corey Oordt] +- Added registry, hacked admin w/ new templates for category editor. + [Justin Quick] +- Added ability to register fields to models. [Jose Soares] +- Added registry, hacked admin w/ new templates for category editor. + [Justin Quick] +- Added an optional setting to allow the slug to be changed. [Corey + Oordt] +- Added a new templatetag to retrieve the top level categories. [Jose + Soares] +- Added views. [Jonathan Hensley] +- Added new documentation. [Corey Oordt] +- Added a description field. [Corey Oordt] +- Added some sample config to see it work. [Corey Oordt] +- Added a template for the template tags. [Corey Oordt] +- Added a demo file of music genres. [Corey Oordt] +- Added tests for templatetags. [Corey Oordt] +- Upped the version and separated the editor. [Corey Oordt] +- Added some testing fixtures. [Corey Oordt] +- Added some tests for category importing. [Corey Oordt] +- Started the docs. [Corey Oordt] +- Added a command to import categories from a file. [Corey Oordt] +- Added the editor templates. [Corey Oordt] +- Added to the gitignore. [Corey Oordt] +- Added template for category detail. [Jose Soares] +- Added urls and views for category detail. [Jose Soares] +- Getting the admin interface working. [Corey Oordt] + +### Fix + +- Fixed a typo in the setup.py and wrapped the other django import in + __init__.py so you could call get_version without having django + installed. Also increased the version number to 0.2.1. [Corey Oordt] +- Fixed the get_absolute_url for the Categories model and fixed up the + view as well. [Corey Oordt] +- Fixing up and updating the usage. [Corey Oordt] +- Fixed up the readme to include some goals. [Corey Oordt] +- Tweaked the description and example of the template tag. [Corey Oordt] +- Fixed a wrong relative path with the jsi18n admin script. [Corey + Oordt] + +### Updates + +- Modified the setup.py to get the latest version from the code and the + long_description fro the README.txt file. [Corey Oordt] +- Altered the registration naming so more than one field could be + registered for a model. [Corey Oordt] +- Changed the disclosure triangle to be a unicode character instead of + the images. [Corey Oordt] +- Updated tree editor view. [jhensley] + +### Other + +- Tiered template heirarchy. [Justin Quick] +- Removed the special many2many models. The user interface was just too + odd to implement. [Corey Oordt] +- Removed the permalink decorator to make the absoluteurl work. [Corey + Oordt] +- Fixed most of the tests. [Corey Oordt] +- Moving media files around. [Corey Oordt] +- Split the editor into a separate app. [Corey Oordt] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ad78220 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,3 @@ +[![Jazzband](https://jazzband.co/static/img/jazzband.svg)](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). diff --git a/LICENSE.txt b/LICENSE.txt index 57bc88a..261eeb9 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -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. - diff --git a/MANIFEST.in b/MANIFEST.in index 26b3af9..e6535cf 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -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 diff --git a/Makefile b/Makefile index 90bd027..b630b5f 100644 --- a/Makefile +++ b/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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..0f8741b --- /dev/null +++ b/README.md @@ -0,0 +1,178 @@ +# Django Categories + +[![Jazzband](https://jazzband.co/static/img/badge.svg)](https://jazzband.co/) +[![codecov](https://codecov.io/gh/jazzband/django-categories/branch/master/graph/badge.svg?token=rW8mpdZqWQ)](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. diff --git a/README.rst b/README.rst deleted file mode 100644 index 8a46b34..0000000 --- a/README.rst +++ /dev/null @@ -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 `_ - -**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 `_. - -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 `_ - -**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 `_ 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 diff --git a/categories/__init__.py b/categories/__init__.py index 7808fc0..509879e 100644 --- a/categories/__init__.py +++ b/categories/__init__.py @@ -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" diff --git a/categories/admin.py b/categories/admin.py index 5c0eaba..abf3f72 100644 --- a/categories/admin.py +++ b/categories/admin.py @@ -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}),)}, + ), + ) diff --git a/categories/apps.py b/categories/apps.py index 02e70e0..5f58385 100644 --- a/categories/apps.py +++ b/categories/apps.py @@ -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) diff --git a/categories/base.py b/categories/base.py index 29319f4..f2e5521 100644 --- a/categories/base.py +++ b/categories/base.py @@ -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") diff --git a/categories/editor/models.py b/categories/editor/models.py index 3542496..8c35c95 100644 --- a/categories/editor/models.py +++ b/categories/editor/models.py @@ -1 +1 @@ -# Placeholder for Django +"""Placeholder for Django.""" diff --git a/categories/editor/settings.py b/categories/editor/settings.py index af293e5..164cf21 100644 --- a/categories/editor/settings.py +++ b/categories/editor/settings.py @@ -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 diff --git a/categories/editor/static/editor/jquery.treeTable.css b/categories/editor/static/editor/jquery.treeTable.css index 9d9a263..07421f1 100644 --- a/categories/editor/static/editor/jquery.treeTable.css +++ b/categories/editor/static/editor/jquery.treeTable.css @@ -66,4 +66,4 @@ .treeTable .ui-draggable-dragging { color: #000; z-index: 1; -} \ No newline at end of file +} diff --git a/categories/editor/static/editor/jquery.treeTable.js b/categories/editor/static/editor/jquery.treeTable.js index 38f7bcb..ea2a88b 100644 --- a/categories/editor/static/editor/jquery.treeTable.js +++ b/categories/editor/static/editor/jquery.treeTable.js @@ -460,4 +460,4 @@ function parentOf(node) { return $(node).parentOf(); } -})(django.jQuery); \ No newline at end of file +})(django.jQuery); diff --git a/categories/editor/templates/admin/editor/grappelli_tree_list_results.html b/categories/editor/templates/admin/editor/grappelli_tree_list_results.html index 379471e..f5b5e5b 100644 --- a/categories/editor/templates/admin/editor/grappelli_tree_list_results.html +++ b/categories/editor/templates/admin/editor/grappelli_tree_list_results.html @@ -25,4 +25,4 @@ -{% endif %} \ No newline at end of file +{% endif %} diff --git a/categories/editor/templates/admin/editor/tree_editor.html b/categories/editor/templates/admin/editor/tree_editor.html index 9179640..337da05 100644 --- a/categories/editor/templates/admin/editor/tree_editor.html +++ b/categories/editor/templates/admin/editor/tree_editor.html @@ -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 %} \ No newline at end of file +{% endblock %} diff --git a/categories/editor/templatetags/admin_tree_list_tags.py b/categories/editor/templatetags/admin_tree_list_tags.py index 6ef6e95..11088c4 100644 --- a/categories/editor/templatetags/admin_tree_list_tags.py +++ b/categories/editor/templatetags/admin_tree_list_tags.py @@ -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('%s' % (row_class, result_repr))) + yield mark_safe(smart_text("%s" % (row_class, result_repr))) if form and not form[cl.model._meta.pk.name].is_hidden: - yield mark_safe(smart_text('%s' % force_text(form[cl.model._meta.pk.name]))) + yield mark_safe(smart_text("%s" % 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 diff --git a/categories/editor/tree_editor.py b/categories/editor/tree_editor.py index f832df5..d528c95 100644 --- a/categories/editor/tree_editor.py +++ b/categories/editor/tree_editor.py @@ -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 diff --git a/categories/editor/utils.py b/categories/editor/utils.py index dab0511..3ce0165 100644 --- a/categories/editor/utils.py +++ b/categories/editor/utils.py @@ -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: diff --git a/categories/fields.py b/categories/fields.py index 1926de9..12f254e 100644 --- a/categories/fields.py +++ b/categories/fields.py @@ -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 diff --git a/categories/fixtures/musicgenres.json b/categories/fixtures/musicgenres.json index b832884..3817510 100644 --- a/categories/fixtures/musicgenres.json +++ b/categories/fixtures/musicgenres.json @@ -4414,4 +4414,4 @@ } } -] \ No newline at end of file +] diff --git a/categories/fixtures/test_category_spaces.txt b/categories/fixtures/test_category_spaces.txt index 025edbe..85852b5 100644 --- a/categories/fixtures/test_category_spaces.txt +++ b/categories/fixtures/test_category_spaces.txt @@ -5,4 +5,4 @@ Category 2 Category 2-1 Category 2-1-1 Category 3 - Category 3-1 \ No newline at end of file + Category 3-1 diff --git a/categories/fixtures/test_category_tabs.txt b/categories/fixtures/test_category_tabs.txt index 207a634..8ded421 100644 --- a/categories/fixtures/test_category_tabs.txt +++ b/categories/fixtures/test_category_tabs.txt @@ -5,4 +5,4 @@ Category 2 Category 2-1 Category 2-1-1 Category 3 - Category 3-1 \ No newline at end of file + Category 3-1 diff --git a/categories/genericcollection.py b/categories/genericcollection.py index ffa01a0..7eab863 100644 --- a/categories/genericcollection.py +++ b/categories/genericcollection.py @@ -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" diff --git a/categories/locale/it/LC_MESSAGES/django.po b/categories/locale/it/LC_MESSAGES/django.po index 1186eef..32f3f2d 100644 --- a/categories/locale/it/LC_MESSAGES/django.po +++ b/categories/locale/it/LC_MESSAGES/django.po @@ -181,4 +181,3 @@ msgstr "Cancella?" #: templates/admin/edit_inline/gen_coll_tabular.html:24 msgid "View on site" msgstr "Vedi sul sito" - diff --git a/categories/management/commands/add_category_fields.py b/categories/management/commands/add_category_fields.py index 4d44c4d..de315a4 100644 --- a/categories/management/commands/add_category_fields.py +++ b/categories/management/commands/add_category_fields.py @@ -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: diff --git a/categories/management/commands/drop_category_field.py b/categories/management/commands/drop_category_field.py index cc4850e..ca48b39 100644 --- a/categories/management/commands/drop_category_field.py +++ b/categories/management/commands/drop_category_field.py @@ -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"]) diff --git a/categories/management/commands/import_categories.py b/categories/management/commands/import_categories.py index a899e50..b41469e 100644 --- a/categories/management/commands/import_categories.py +++ b/categories/management/commands/import_categories.py @@ -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) diff --git a/categories/migration.py b/categories/migration.py index 407a796..13ff0c5 100644 --- a/categories/migration.py +++ b/categories/migration.py @@ -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) diff --git a/categories/migrations/0001_initial.py b/categories/migrations/0001_initial.py index b58fabc..23afb6a 100644 --- a/categories/migrations/0001_initial.py +++ b/categories/migrations/0001_initial.py @@ -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")]), ), ] diff --git a/categories/migrations/0002_auto_20170217_1111.py b/categories/migrations/0002_auto_20170217_1111.py index 27b77ed..38c1f4f 100644 --- a/categories/migrations/0002_auto_20170217_1111.py +++ b/categories/migrations/0002_auto_20170217_1111.py @@ -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", + ), ), ] diff --git a/categories/migrations/0003_auto_20200306_1050.py b/categories/migrations/0003_auto_20200306_1050.py index c725321..21d98a3 100644 --- a/categories/migrations/0003_auto_20200306_1050.py +++ b/categories/migrations/0003_auto_20200306_1050.py @@ -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", + ), ), ] diff --git a/categories/migrations/0004_auto_20200517_1832.py b/categories/migrations/0004_auto_20200517_1832.py index ec3f2a0..dcbde23 100644 --- a/categories/migrations/0004_auto_20200517_1832.py +++ b/categories/migrations/0004_auto_20200517_1832.py @@ -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" + ), ), ] diff --git a/categories/models.py b/categories/models.py index 15ddecb..0cba3fb 100644 --- a/categories/models.py +++ b/categories/models.py @@ -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() diff --git a/categories/registration.py b/categories/registration.py index e26bb12..793c908 100644 --- a/categories/registration.py +++ b/categories/registration.py @@ -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} + ) diff --git a/categories/settings.py b/categories/settings.py index bf77d66..03519fe 100644 --- a/categories/settings.py +++ b/categories/settings.py @@ -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)} diff --git a/categories/static/js/genericcollections.js b/categories/static/js/genericcollections.js index 584208e..8081eb8 100644 --- a/categories/static/js/genericcollections.js +++ b/categories/static/js/genericcollections.js @@ -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; -} \ No newline at end of file +} diff --git a/categories/templates/categories/ancestors_ul.html b/categories/templates/categories/ancestors_ul.html index eb07cb2..d9173e2 100644 --- a/categories/templates/categories/ancestors_ul.html +++ b/categories/templates/categories/ancestors_ul.html @@ -8,4 +8,4 @@ {% else %}{{ node.name }} {% endifequal %} {% for level in structure.closed_levels %}{% endfor %} -{% endfor %} \ No newline at end of file +{% endfor %} diff --git a/categories/templates/categories/base.html b/categories/templates/categories/base.html index 385820f..245102a 100644 --- a/categories/templates/categories/base.html +++ b/categories/templates/categories/base.html @@ -1,3 +1,3 @@ {% extends 'base.html' %} {% block content %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/categories/templates/categories/breadcrumbs.html b/categories/templates/categories/breadcrumbs.html index d6ec117..9fac9e2 100644 --- a/categories/templates/categories/breadcrumbs.html +++ b/categories/templates/categories/breadcrumbs.html @@ -1,3 +1,3 @@ {% spaceless %}{% for item in category.get_ancestors %} {{ item.name }}{{ separator }}{% endfor %}{{ category.name }} -{% endspaceless %} \ No newline at end of file +{% endspaceless %} diff --git a/categories/templates/categories/category_detail.html b/categories/templates/categories/category_detail.html index 3865fa1..c43e371 100644 --- a/categories/templates/categories/category_detail.html +++ b/categories/templates/categories/category_detail.html @@ -14,4 +14,4 @@ {% if category.parent %}

Go up to {{ category.parent }}

{% endif %} {% if category.description %}

{{ category.description }}

{% endif %} {% if category.children.count %}

Subcategories

    {% for child in category.children.all %}
  • {{ child }}
  • {% endfor %}
{% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/categories/templates/categories/category_list.html b/categories/templates/categories/category_list.html index ff87511..f9a2c26 100644 --- a/categories/templates/categories/category_list.html +++ b/categories/templates/categories/category_list.html @@ -2,4 +2,4 @@ {% block content %}

Categories

-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/categories/templates/categories/ul_tree.html b/categories/templates/categories/ul_tree.html index b6013de..891e298 100644 --- a/categories/templates/categories/ul_tree.html +++ b/categories/templates/categories/ul_tree.html @@ -8,4 +8,4 @@ {% else %}{{ node.name }} {% endifequal %} {% for level in structure.closed_levels %}{% endfor %} -{% endfor %}{% endspaceless %} \ No newline at end of file +{% endfor %}{% endspaceless %} diff --git a/categories/templatetags/category_tags.py b/categories/templatetags/category_tags.py index 69d2aad..cc4a579 100644 --- a/categories/templatetags/category_tags.py +++ b/categories/templatetags/category_tags.py @@ -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:: - + + 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:: - + + 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 [, , , , {% recursetree nodes %}
  • @@ -397,13 +494,23 @@ def recursetree(parser, token):
  • {% endrecursetree %} + + 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) diff --git a/categories/tests/test_admin.py b/categories/tests/test_admin.py index 20759c1..4875889 100644 --- a/categories/tests/test_admin.py +++ b/categories/tests/test_admin.py @@ -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) diff --git a/categories/tests/test_category_import.py b/categories/tests/test_category_import.py index 3db1be4..acd366f 100644 --- a/categories/tests/test_category_import.py +++ b/categories/tests/test_category_import.py @@ -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): """ diff --git a/categories/tests/test_manager.py b/categories/tests/test_manager.py index b2fb6c6..a9f2718 100644 --- a/categories/tests/test_manager.py +++ b/categories/tests/test_manager.py @@ -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() diff --git a/categories/tests/test_mgmt_commands.py b/categories/tests/test_mgmt_commands.py index 7466910..b94c842 100644 --- a/categories/tests/test_mgmt_commands.py +++ b/categories/tests/test_mgmt_commands.py @@ -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) diff --git a/categories/tests/test_models.py b/categories/tests/test_models.py index 5649bee..5208baf 100644 --- a/categories/tests/test_models.py +++ b/categories/tests/test_models.py @@ -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) diff --git a/categories/tests/test_registration.py b/categories/tests/test_registration.py index 842baec..918d8c9 100644 --- a/categories/tests/test_registration.py +++ b/categories/tests/test_registration.py @@ -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()]) diff --git a/categories/tests/test_templatetags.py b/categories/tests/test_templatetags.py index f099715..2d419a3 100644 --- a/categories/tests/test_templatetags.py +++ b/categories/tests/test_templatetags.py @@ -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 = '' 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 = '' 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 = 'World > Worldbeat' + expected_resp = 'World > 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 = '
    • Country
      • Country pop
        • Urban Cowboy
    • World
      • Worldbeat
      ' - ctxt = {'nodes': Category.objects.filter(name__in=("Worldbeat", "Urban Cowboy"))} + expected_resp = "
      • Country
        • Country pop
          • Urban Cowboy
      • World
        • Worldbeat
        " + ctxt = {"nodes": Category.objects.filter(name__in=("Worldbeat", "Urban Cowboy"))} resp = self.render_template( - '{% load category_tags %}' - '
          {% recursetree nodes|tree_queryset %}
        • {{ node.name }}' - '{% if not node.is_leaf_node %}
            {{ children }}' - '
          {% endif %}
        • {% endrecursetree %}
        ', ctxt) + "{% load category_tags %}" + "
          {% recursetree nodes|tree_queryset %}
        • {{ node.name }}" + "{% if not node.is_leaf_node %}
            {{ children }}" + "
          {% endif %}
        • {% endrecursetree %}
        ", + ctxt, + ) self.assertEqual(resp, expected_resp) diff --git a/categories/tests/test_views.py b/categories/tests/test_views.py index d64435b..dac65f9 100644 --- a/categories/tests/test_views.py +++ b/categories/tests/test_views.py @@ -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/" + ) diff --git a/categories/urls.py b/categories/urls.py index 986df79..ecb9649 100644 --- a/categories/urls.py +++ b/categories/urls.py @@ -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.+)/$', views.category_detail, name='categories_category'), -) +urlpatterns += (url(r"^(?P.+)/$", views.category_detail, name="categories_category"),) diff --git a/categories/views.py b/categories/views.py index 65584f6..cb81054 100644 --- a/categories/views.py +++ b/categories/views.py @@ -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 diff --git a/doc_src/Makefile b/doc_src/Makefile index d34da38..7c75b9a 100644 --- a/doc_src/Makefile +++ b/doc_src/Makefile @@ -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 diff --git a/doc_src/_static/breadcrumb_background.png b/doc_src/_static/breadcrumb_background.png deleted file mode 100644 index 9b45910..0000000 Binary files a/doc_src/_static/breadcrumb_background.png and /dev/null differ diff --git a/doc_src/_static/css/custom.css b/doc_src/_static/css/custom.css new file mode 100644 index 0000000..59a7390 --- /dev/null +++ b/doc_src/_static/css/custom.css @@ -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; +} diff --git a/doc_src/_static/default.css b/doc_src/_static/default.css deleted file mode 100644 index c719235..0000000 --- a/doc_src/_static/default.css +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/doc_src/_static/documentation.png b/doc_src/_static/documentation.png deleted file mode 100644 index f0d334b..0000000 Binary files a/doc_src/_static/documentation.png and /dev/null differ diff --git a/doc_src/_static/header_sm_mid.png b/doc_src/_static/header_sm_mid.png deleted file mode 100644 index dce5a40..0000000 Binary files a/doc_src/_static/header_sm_mid.png and /dev/null differ diff --git a/doc_src/_static/scrn1.png b/doc_src/_static/scrn1.png deleted file mode 100644 index 6499b3c..0000000 Binary files a/doc_src/_static/scrn1.png and /dev/null differ diff --git a/doc_src/_static/scrn2.png b/doc_src/_static/scrn2.png deleted file mode 100644 index 2a60215..0000000 Binary files a/doc_src/_static/scrn2.png and /dev/null differ diff --git a/doc_src/_static/searchfield_leftcap.png b/doc_src/_static/searchfield_leftcap.png deleted file mode 100644 index cc00c22..0000000 Binary files a/doc_src/_static/searchfield_leftcap.png and /dev/null differ diff --git a/doc_src/_static/searchfield_repeat.png b/doc_src/_static/searchfield_repeat.png deleted file mode 100644 index b429a16..0000000 Binary files a/doc_src/_static/searchfield_repeat.png and /dev/null differ diff --git a/doc_src/_static/searchfield_rightcap.png b/doc_src/_static/searchfield_rightcap.png deleted file mode 100644 index 8e13620..0000000 Binary files a/doc_src/_static/searchfield_rightcap.png and /dev/null differ diff --git a/doc_src/_static/title_background.png b/doc_src/_static/title_background.png deleted file mode 100644 index 6fcd1cd..0000000 Binary files a/doc_src/_static/title_background.png and /dev/null differ diff --git a/doc_src/_static/toc.js b/doc_src/_static/toc.js deleted file mode 100644 index 7b70978..0000000 --- a/doc_src/_static/toc.js +++ /dev/null @@ -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(); -}); \ No newline at end of file diff --git a/doc_src/_static/triangle_closed.png b/doc_src/_static/triangle_closed.png deleted file mode 100644 index 1e7f7bb..0000000 Binary files a/doc_src/_static/triangle_closed.png and /dev/null differ diff --git a/doc_src/_static/triangle_left.png b/doc_src/_static/triangle_left.png deleted file mode 100644 index 2d86be7..0000000 Binary files a/doc_src/_static/triangle_left.png and /dev/null differ diff --git a/doc_src/_static/triangle_open.png b/doc_src/_static/triangle_open.png deleted file mode 100644 index e5d3bfd..0000000 Binary files a/doc_src/_static/triangle_open.png and /dev/null differ diff --git a/doc_src/_templates/autosummary/base.rst b/doc_src/_templates/autosummary/base.rst new file mode 100644 index 0000000..90e3521 --- /dev/null +++ b/doc_src/_templates/autosummary/base.rst @@ -0,0 +1,10 @@ +.. rst-class:: h4 text-secondary + +{{ fullname }} + +{{ objname | escape | underline}} + +.. currentmodule:: {{ module }} +{%- if not objname.startswith("test") %} +.. auto{{ objtype }}:: {{ objname }} +{%- endif %} diff --git a/doc_src/_templates/autosummary/class.rst b/doc_src/_templates/autosummary/class.rst new file mode 100644 index 0000000..fb5557b --- /dev/null +++ b/doc_src/_templates/autosummary/class.rst @@ -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 %} diff --git a/doc_src/_templates/autosummary/module.rst b/doc_src/_templates/autosummary/module.rst new file mode 100644 index 0000000..d664d28 --- /dev/null +++ b/doc_src/_templates/autosummary/module.rst @@ -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 -%} diff --git a/doc_src/_templates/layout.html b/doc_src/_templates/layout.html deleted file mode 100644 index 4e302db..0000000 --- a/doc_src/_templates/layout.html +++ /dev/null @@ -1,144 +0,0 @@ -{% extends "basic/layout.html" %} -{%- block doctype -%} - -{%- endblock %} -{%- set reldelim1 = reldelim1 is not defined and ' »' or reldelim1 %} -{%- set reldelim2 = reldelim2 is not defined and ' |' or reldelim2 %} -{%- block linktags %} - {%- if hasdoc('about') %} - - {%- endif %} - {%- if hasdoc('genindex') %} - - {%- endif %} - {%- if hasdoc('search') %} - - {%- endif %} - {%- if hasdoc('copyright') %} - - {%- endif %} - - {%- if parents %} - - {%- endif %} - {%- if next %} - - {%- endif %} - {%- if prev %} - - {%- endif %} -{%- endblock %} -{%- block extrahead %} {% endblock %} -{%- block header %}{% endblock %} -{%- block relbar1 %} -
        -

        {{docstitle}}

        -
        - -{% endblock %} - -{%- block sidebar1 %} -{%- if not embedded %}{% if not theme_nosidebar|tobool %} -
        -
        - {%- block sidebarlogo %} - {%- if logo %} - - {%- endif %} - {%- endblock %} - {%- block sidebartoc %} - - {{ toctree() }} - {%- endblock %} - {%- block sidebarrel %} - {%- endblock %} - {%- block sidebarsourcelink %} - {%- if show_source and has_source and sourcename %} -

        {{ _('This Page') }}

        - - {%- endif %} - {%- endblock %} - {%- if customsidebar %} - {% include customsidebar %} - {%- endif %} - {%- block sidebarsearch %} - {%- if pagename != "search" %} - - - {%- endif %} - {%- endblock %} -
        -
        -{%- endif %}{% endif %} - -{% endblock %} -{%- block document %} -
        - {%- if not embedded %}{% if not theme_nosidebar|tobool %} -
        - {%- endif %}{% endif %} -
        - {% block body %} {% endblock %} -
        - {%- if not embedded %}{% if not theme_nosidebar|tobool %} -
        - {%- endif %}{% endif %} -
        - -{%- endblock %} -{%- block sidebar2 %}{% endblock %} -{%- block relbar2 %}{% endblock %} -{%- block footer %} - - -{%- endblock %} diff --git a/doc_src/api/index.rst b/doc_src/api/index.rst new file mode 100644 index 0000000..7f95b37 --- /dev/null +++ b/doc_src/api/index.rst @@ -0,0 +1,8 @@ +API +=== + +.. autosummary:: + :toctree: + :recursive: + + categories diff --git a/doc_src/changelog.md b/doc_src/changelog.md new file mode 100644 index 0000000..66efc0f --- /dev/null +++ b/doc_src/changelog.md @@ -0,0 +1,2 @@ +```{include} ../CHANGELOG.md +``` diff --git a/doc_src/code_examples/custom_categories7.py b/doc_src/code_examples/custom_categories7.py deleted file mode 100644 index 4fcc1a2..0000000 --- a/doc_src/code_examples/custom_categories7.py +++ /dev/null @@ -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',), - }), - ) diff --git a/doc_src/conf.py b/doc_src/conf.py index 16c8213..e2cab65 100644 --- a/doc_src/conf.py +++ b/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 -# " v 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 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 diff --git a/doc_src/index.rst b/doc_src/index.rst index 9c330ed..22be8a7 100644 --- a/doc_src/index.rst +++ b/doc_src/index.rst @@ -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` - diff --git a/doc_src/reference/management_commands.rst b/doc_src/reference/management_commands.rst index b090695..4818672 100644 --- a/doc_src/reference/management_commands.rst +++ b/doc_src/reference/management_commands.rst @@ -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. \ No newline at end of file +Requires Django South. diff --git a/doc_src/reference/settings.rst b/doc_src/reference/settings.rst index dd0ebee..96c542f 100644 --- a/doc_src/reference/settings.rst +++ b/doc_src/reference/settings.rst @@ -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. \ No newline at end of file +**Description:** Allows for selective customization of the default behavior of adding the fields to the admin class. See :ref:`admin_settings` for more information. diff --git a/doc_src/adding_the_fields.rst b/doc_src/user_guide/adding_the_fields.rst similarity index 98% rename from doc_src/adding_the_fields.rst rename to doc_src/user_guide/adding_the_fields.rst index a6a920f..b40709b 100644 --- a/doc_src/adding_the_fields.rst +++ b/doc_src/user_guide/adding_the_fields.rst @@ -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`` \ No newline at end of file +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`` diff --git a/doc_src/admin_settings.rst b/doc_src/user_guide/admin_settings.rst similarity index 100% rename from doc_src/admin_settings.rst rename to doc_src/user_guide/admin_settings.rst diff --git a/doc_src/code_examples/custom_categories1.py b/doc_src/user_guide/code_examples/custom_categories1.py similarity index 75% rename from doc_src/code_examples/custom_categories1.py rename to doc_src/user_guide/code_examples/custom_categories1.py index c8d4652..68a437e 100644 --- a/doc_src/code_examples/custom_categories1.py +++ b/doc_src/user_guide/code_examples/custom_categories1.py @@ -7,4 +7,4 @@ class SimpleCategory(CategoryBase): """ class Meta: - verbose_name_plural = 'simple categories' + verbose_name_plural = "simple categories" diff --git a/doc_src/code_examples/custom_categories2.py b/doc_src/user_guide/code_examples/custom_categories2.py similarity index 100% rename from doc_src/code_examples/custom_categories2.py rename to doc_src/user_guide/code_examples/custom_categories2.py diff --git a/doc_src/code_examples/custom_categories3.py b/doc_src/user_guide/code_examples/custom_categories3.py similarity index 56% rename from doc_src/code_examples/custom_categories3.py rename to doc_src/user_guide/code_examples/custom_categories3.py index d2aaa6f..27babd9 100644 --- a/doc_src/code_examples/custom_categories3.py +++ b/doc_src/user_guide/code_examples/custom_categories3.py @@ -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>" + ) diff --git a/doc_src/code_examples/custom_categories4.py b/doc_src/user_guide/code_examples/custom_categories4.py similarity index 99% rename from doc_src/code_examples/custom_categories4.py rename to doc_src/user_guide/code_examples/custom_categories4.py index 87e1d3a..9356e05 100644 --- a/doc_src/code_examples/custom_categories4.py +++ b/doc_src/user_guide/code_examples/custom_categories4.py @@ -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: diff --git a/doc_src/code_examples/custom_categories5.py b/doc_src/user_guide/code_examples/custom_categories5.py similarity index 52% rename from doc_src/code_examples/custom_categories5.py rename to doc_src/user_guide/code_examples/custom_categories5.py index 67109c6..e2f48d4 100644 --- a/doc_src/code_examples/custom_categories5.py +++ b/doc_src/user_guide/code_examples/custom_categories5.py @@ -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") diff --git a/doc_src/code_examples/custom_categories6.py b/doc_src/user_guide/code_examples/custom_categories6.py similarity index 56% rename from doc_src/code_examples/custom_categories6.py rename to doc_src/user_guide/code_examples/custom_categories6.py index 3c09f03..387aa72 100644 --- a/doc_src/code_examples/custom_categories6.py +++ b/doc_src/user_guide/code_examples/custom_categories6.py @@ -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"] diff --git a/doc_src/user_guide/code_examples/custom_categories7.py b/doc_src/user_guide/code_examples/custom_categories7.py new file mode 100644 index 0000000..6ec7041 --- /dev/null +++ b/doc_src/user_guide/code_examples/custom_categories7.py @@ -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",), + }, + ), + ) diff --git a/doc_src/custom_categories.rst b/doc_src/user_guide/custom_categories.rst similarity index 100% rename from doc_src/custom_categories.rst rename to doc_src/user_guide/custom_categories.rst diff --git a/doc_src/user_guide/index.md b/doc_src/user_guide/index.md new file mode 100644 index 0000000..fb4062b --- /dev/null +++ b/doc_src/user_guide/index.md @@ -0,0 +1,12 @@ +# User Guide + +```{toctree} +--- +maxdepth: 2 +--- +usage +registering_models +adding_the_fields +admin_settings +custom_categories +``` diff --git a/doc_src/registering_models.rst b/doc_src/user_guide/registering_models.rst similarity index 100% rename from doc_src/registering_models.rst rename to doc_src/user_guide/registering_models.rst diff --git a/doc_src/usage.rst b/doc_src/user_guide/usage.rst similarity index 99% rename from doc_src/usage.rst rename to doc_src/user_guide/usage.rst index c54c3b2..83e9779 100644 --- a/doc_src/usage.rst +++ b/doc_src/user_guide/usage.rst @@ -59,4 +59,3 @@ comma-separated list of feature names. The valid feature names are: Books -> [] Sci-fi -> [u'Books'] Dystopian Futures -> [u'Books', u'Sci-fi'] - diff --git a/doc_src/usage_example_template.html b/doc_src/user_guide/usage_example_template.html similarity index 98% rename from doc_src/usage_example_template.html rename to doc_src/user_guide/usage_example_template.html index b0bee73..9428cf6 100644 --- a/doc_src/usage_example_template.html +++ b/doc_src/user_guide/usage_example_template.html @@ -25,4 +25,4 @@

        No entries for {{ category }}

        {% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/example/manage.py b/example/manage.py index f9726f9..195f6b9 100755 --- a/example/manage.py +++ b/example/manage.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +"""Entrypoint for custom django functions.""" import os import sys diff --git a/example/settings-testing.py b/example/settings-testing.py index 9d2c3f1..2105458 100644 --- a/example/settings-testing.py +++ b/example/settings-testing.py @@ -16,108 +16,105 @@ ADMINS = ( MANAGERS = ADMINS DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': 'dev.db', - 'USER': '', - 'PASSWORD': '', - 'HOST': '', - 'PORT': '', + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": "dev.db", + "USER": "", + "PASSWORD": "", + "HOST": "", + "PORT": "", } } INSTALLED_APPS = ( - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.flatpages', - 'categories', - 'categories.editor', - 'mptt', - 'simpletext', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.sites", + "django.contrib.messages", + "django.contrib.staticfiles", + "django.contrib.flatpages", + "categories", + "categories.editor", + "mptt", + "simpletext", ) -TIME_ZONE = 'America/Chicago' - -LANGUAGE_CODE = 'en-us' +TIME_ZONE = "America/Chicago" +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" +LANGUAGE_CODE = "en-us" SITE_ID = 1 USE_I18N = True -MEDIA_ROOT = os.path.abspath(os.path.join(PROJ_ROOT, 'media', 'uploads')) +MEDIA_ROOT = os.path.abspath(os.path.join(PROJ_ROOT, "media", "uploads")) -MEDIA_URL = '/uploads/' +MEDIA_URL = "/uploads/" -STATIC_ROOT = os.path.abspath(os.path.join(PROJ_ROOT, 'media', 'static')) +STATIC_ROOT = os.path.abspath(os.path.join(PROJ_ROOT, "media", "static")) -STATIC_URL = '/static/' +STATIC_URL = "/static/" STATICFILES_DIRS = () STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", ) -SECRET_KEY = 'bwq#m)-zsey-fs)0#4*o=2z(v5g!ei=zytl9t-1hesh4b&-u^d' +SECRET_KEY = "bwq#m)-zsey-fs)0#4*o=2z(v5g!ei=zytl9t-1hesh4b&-u^d" MIDDLEWARE = ( - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ) -ROOT_URLCONF = 'urls' +ROOT_URLCONF = "urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'APP_DIRS': True, - 'DIRS': [os.path.abspath(os.path.join(os.path.dirname(__file__), 'templates'))], - 'OPTIONS': { - 'debug': DEBUG, - 'context_processors': [ - 'django.contrib.auth.context_processors.auth', - 'django.template.context_processors.debug', - 'django.template.context_processors.i18n', - 'django.template.context_processors.media', - 'django.template.context_processors.static', - 'django.template.context_processors.tz', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "APP_DIRS": True, + "DIRS": [os.path.abspath(os.path.join(os.path.dirname(__file__), "templates"))], + "OPTIONS": { + "debug": DEBUG, + "context_processors": [ + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.debug", + "django.template.context_processors.i18n", + "django.template.context_processors.media", + "django.template.context_processors.static", + "django.template.context_processors.tz", + "django.contrib.messages.context_processors.messages", ], - } + }, }, ] CATEGORIES_SETTINGS = { - 'ALLOW_SLUG_CHANGE': True, - 'RELATION_MODELS': ['simpletext.simpletext', 'flatpages.flatpage'], - 'FK_REGISTRY': { - 'flatpages.flatpage': ( - 'category', - {'on_delete': models.CASCADE} - ), - 'simpletext.simpletext': ( - 'primary_category', - {'name': 'secondary_category', 'related_name': 'simpletext_sec_cat'}, + "ALLOW_SLUG_CHANGE": True, + "RELATION_MODELS": ["simpletext.simpletext", "flatpages.flatpage"], + "FK_REGISTRY": { + "flatpages.flatpage": ("category", {"on_delete": models.CASCADE}), + "simpletext.simpletext": ( + "primary_category", + {"name": "secondary_category", "related_name": "simpletext_sec_cat"}, ), }, - 'M2M_REGISTRY': { + "M2M_REGISTRY": { # 'simpletext.simpletext': {'name': 'categories', 'related_name': 'm2mcats'}, - 'flatpages.flatpage': ( - {'name': 'other_categories', 'related_name': 'other_cats'}, - {'name': 'more_categories', 'related_name': 'more_cats'}, + "flatpages.flatpage": ( + {"name": "other_categories", "related_name": "other_cats"}, + {"name": "more_categories", "related_name": "more_cats"}, ), }, } -TEST_RUNNER = 'django.test.runner.DiscoverRunner' +TEST_RUNNER = "django.test.runner.DiscoverRunner" diff --git a/example/settings.py b/example/settings.py index 6eab74a..ababf90 100644 --- a/example/settings.py +++ b/example/settings.py @@ -1,6 +1,7 @@ -# Django settings for sample project. +"""Django settings for sample project.""" import os import sys + import django from django.db import models @@ -16,109 +17,106 @@ ADMINS = ( MANAGERS = ADMINS DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': 'dev.db', - 'USER': '', - 'PASSWORD': '', - 'HOST': '', - 'PORT': '', + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": "dev.db", + "USER": "", + "PASSWORD": "", + "HOST": "", + "PORT": "", } } INSTALLED_APPS = ( - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.flatpages', - 'categories', - 'categories.editor', - 'mptt', - 'simpletext', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.sites", + "django.contrib.messages", + "django.contrib.staticfiles", + "django.contrib.flatpages", + "categories", + "categories.editor", + "mptt", + "simpletext", ) -TIME_ZONE = 'America/Chicago' +TIME_ZONE = "America/Chicago" -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" SITE_ID = 1 USE_I18N = True -MEDIA_ROOT = os.path.abspath(os.path.join(PROJ_ROOT, 'media', 'uploads')) +MEDIA_ROOT = os.path.abspath(os.path.join(PROJ_ROOT, "media", "uploads")) -MEDIA_URL = '/uploads/' +MEDIA_URL = "/uploads/" -STATIC_ROOT = os.path.abspath(os.path.join(PROJ_ROOT, 'media', 'static')) +STATIC_ROOT = os.path.abspath(os.path.join(PROJ_ROOT, "media", "static")) -STATIC_URL = '/static/' +STATIC_URL = "/static/" STATICFILES_DIRS = () STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", ) -SECRET_KEY = 'bwq#m)-zsey-fs)0#4*o=2z(v5g!ei=zytl9t-1hesh4b&-u^d' +SECRET_KEY = "bwq#m)-zsey-fs)0#4*o=2z(v5g!ei=zytl9t-1hesh4b&-u^d" MIDDLEWARE = ( - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ) -ROOT_URLCONF = 'urls' +ROOT_URLCONF = "urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'APP_DIRS': True, - 'DIRS': [os.path.abspath(os.path.join(os.path.dirname(__file__), 'templates'))], - 'OPTIONS': { - 'debug': DEBUG, - 'context_processors': [ - 'django.contrib.auth.context_processors.auth', - 'django.template.context_processors.debug', - 'django.template.context_processors.i18n', - 'django.template.context_processors.media', - 'django.template.context_processors.static', - 'django.template.context_processors.tz', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "APP_DIRS": True, + "DIRS": [os.path.abspath(os.path.join(os.path.dirname(__file__), "templates"))], + "OPTIONS": { + "debug": DEBUG, + "context_processors": [ + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.debug", + "django.template.context_processors.i18n", + "django.template.context_processors.media", + "django.template.context_processors.static", + "django.template.context_processors.tz", + "django.contrib.messages.context_processors.messages", ], - } + }, } ] CATEGORIES_SETTINGS = { - 'ALLOW_SLUG_CHANGE': True, - 'RELATION_MODELS': ['simpletext.simpletext', 'flatpages.flatpage'], - 'FK_REGISTRY': { - 'flatpages.flatpage': ( - 'category', - {'on_delete': models.CASCADE} - ), - 'simpletext.simpletext': ( - 'primary_category', - {'name': 'secondary_category', 'related_name': 'simpletext_sec_cat'}, + "ALLOW_SLUG_CHANGE": True, + "RELATION_MODELS": ["simpletext.simpletext", "flatpages.flatpage"], + "FK_REGISTRY": { + "flatpages.flatpage": ("category", {"on_delete": models.CASCADE}), + "simpletext.simpletext": ( + "primary_category", + {"name": "secondary_category", "related_name": "simpletext_sec_cat"}, ), }, - 'M2M_REGISTRY': { - 'simpletext.simpletext': {'name': 'categories', 'related_name': 'm2mcats'}, - 'flatpages.flatpage': ( - {'name': 'other_categories', 'related_name': 'other_cats'}, - {'name': 'more_categories', 'related_name': 'more_cats'}, + "M2M_REGISTRY": { + "simpletext.simpletext": {"name": "categories", "related_name": "m2mcats"}, + "flatpages.flatpage": ( + {"name": "other_categories", "related_name": "other_cats"}, + {"name": "more_categories", "related_name": "more_cats"}, ), }, } if django.VERSION[1] > 5: - TEST_RUNNER = 'django.test.runner.DiscoverRunner' + TEST_RUNNER = "django.test.runner.DiscoverRunner" diff --git a/example/simpletext/__init__.py b/example/simpletext/__init__.py old mode 100755 new mode 100644 diff --git a/example/simpletext/admin.py b/example/simpletext/admin.py index dcf1947..2d12815 100644 --- a/example/simpletext/admin.py +++ b/example/simpletext/admin.py @@ -1,24 +1,38 @@ -from .models import SimpleText, SimpleCategory +"""Admin interface for simple text.""" from django.contrib import admin from categories.admin import CategoryBaseAdmin, CategoryBaseAdminForm +from .models import SimpleCategory, SimpleText + class SimpleTextAdmin(admin.ModelAdmin): + """Admin for simple text model.""" + fieldsets = ( - (None, { - 'fields': ('name', 'description', ) - }), + ( + None, + { + "fields": ( + "name", + "description", + ) + }, + ), ) class SimpleCategoryAdminForm(CategoryBaseAdminForm): + """Admin form for simple category.""" + class Meta: model = SimpleCategory - fields = '__all__' + fields = "__all__" class SimpleCategoryAdmin(CategoryBaseAdmin): + """Admin for simple category.""" + form = SimpleCategoryAdminForm diff --git a/example/simpletext/migrations/0001_initial.py b/example/simpletext/migrations/0001_initial.py index d66db39..fe231e1 100644 --- a/example/simpletext/migrations/0001_initial.py +++ b/example/simpletext/migrations/0001_initial.py @@ -1,10 +1,9 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.9.13 on 2017-10-12 20:27 from __future__ import unicode_literals -from django.db import migrations, models import django.db.models.deletion import mptt.fields +from django.db import migrations, models class Migration(migrations.Migration): @@ -15,35 +14,45 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='SimpleCategory', + name="SimpleCategory", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100, verbose_name='name')), - ('slug', models.SlugField(verbose_name='slug')), - ('active', models.BooleanField(default=True, verbose_name='active')), - ('lft', models.PositiveIntegerField(db_index=True, editable=False)), - ('rght', models.PositiveIntegerField(db_index=True, editable=False)), - ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)), - ('level', models.PositiveIntegerField(db_index=True, editable=False)), - ('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='simpletext.SimpleCategory', verbose_name='parent')), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=100, verbose_name="name")), + ("slug", models.SlugField(verbose_name="slug")), + ("active", models.BooleanField(default=True, verbose_name="active")), + ("lft", models.PositiveIntegerField(db_index=True, editable=False)), + ("rght", models.PositiveIntegerField(db_index=True, editable=False)), + ("tree_id", models.PositiveIntegerField(db_index=True, editable=False)), + ("level", models.PositiveIntegerField(db_index=True, editable=False)), + ( + "parent", + mptt.fields.TreeForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="children", + to="simpletext.SimpleCategory", + verbose_name="parent", + ), + ), ], options={ - 'verbose_name_plural': 'simple categories', + "verbose_name_plural": "simple categories", }, ), migrations.CreateModel( - name='SimpleText', + name="SimpleText", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255)), - ('description', models.TextField(blank=True)), - ('created', models.DateTimeField(auto_now_add=True)), - ('updated', models.DateTimeField(auto_now=True)), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=255)), + ("description", models.TextField(blank=True)), + ("created", models.DateTimeField(auto_now_add=True)), + ("updated", models.DateTimeField(auto_now=True)), ], options={ - 'ordering': ('-created',), - 'get_latest_by': 'updated', - 'verbose_name_plural': 'Simple Text', + "ordering": ("-created",), + "get_latest_by": "updated", + "verbose_name_plural": "Simple Text", }, ), ] diff --git a/example/simpletext/migrations/0002_auto_20171204_0721.py b/example/simpletext/migrations/0002_auto_20171204_0721.py index 2205f4c..6ce4e0a 100644 --- a/example/simpletext/migrations/0002_auto_20171204_0721.py +++ b/example/simpletext/migrations/0002_auto_20171204_0721.py @@ -1,39 +1,46 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.10.5 on 2017-12-04 07:21 from __future__ import unicode_literals -from django.db import migrations, models import django.db.models.deletion import django.db.models.manager +from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('categories', '0002_auto_20170217_1111'), - ('simpletext', '0001_initial'), + ("categories", "0002_auto_20170217_1111"), + ("simpletext", "0001_initial"), ] operations = [ migrations.AlterModelManagers( - name='simplecategory', + name="simplecategory", managers=[ - ('tree', django.db.models.manager.Manager()), + ("tree", django.db.models.manager.Manager()), ], ), migrations.AddField( - model_name='simpletext', - name='categories', - field=models.ManyToManyField(blank=True, related_name='m2mcats', to='categories.Category'), + model_name="simpletext", + name="categories", + field=models.ManyToManyField(blank=True, related_name="m2mcats", to="categories.Category"), ), migrations.AddField( - model_name='simpletext', - name='primary_category', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='categories.Category'), + model_name="simpletext", + name="primary_category", + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="categories.Category" + ), ), migrations.AddField( - model_name='simpletext', - name='secondary_category', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='simpletext_sec_cat', to='categories.Category'), + model_name="simpletext", + name="secondary_category", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="simpletext_sec_cat", + to="categories.Category", + ), ), ] diff --git a/example/simpletext/migrations/0003_auto_20200306_0928.py b/example/simpletext/migrations/0003_auto_20200306_0928.py index 0d786bc..a33be27 100644 --- a/example/simpletext/migrations/0003_auto_20200306_0928.py +++ b/example/simpletext/migrations/0003_auto_20200306_0928.py @@ -6,23 +6,23 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('simpletext', '0002_auto_20171204_0721'), + ("simpletext", "0002_auto_20171204_0721"), ] operations = [ migrations.AlterField( - model_name='simplecategory', - name='level', + model_name="simplecategory", + name="level", field=models.PositiveIntegerField(editable=False), ), migrations.AlterField( - model_name='simplecategory', - name='lft', + model_name="simplecategory", + name="lft", field=models.PositiveIntegerField(editable=False), ), migrations.AlterField( - model_name='simplecategory', - name='rght', + model_name="simplecategory", + name="rght", field=models.PositiveIntegerField(editable=False), ), ] diff --git a/example/simpletext/models.py b/example/simpletext/models.py old mode 100755 new mode 100644 index 0fe96d1..7d01720 --- a/example/simpletext/models.py +++ b/example/simpletext/models.py @@ -1,3 +1,4 @@ +"""Example model.""" from django.db import models from categories.base import CategoryBase @@ -5,7 +6,7 @@ from categories.base import CategoryBase class SimpleText(models.Model): """ - (SimpleText description) + (SimpleText description). """ name = models.CharField(max_length=255) @@ -14,26 +15,30 @@ class SimpleText(models.Model): updated = models.DateTimeField(auto_now=True) class Meta: - verbose_name_plural = 'Simple Text' - ordering = ('-created',) - get_latest_by = 'updated' + verbose_name_plural = "Simple Text" + ordering = ("-created",) + get_latest_by = "updated" def __unicode__(self): return self.name def get_absolute_url(self): + """Get the absolute URL for this object.""" try: from django.db.models import permalink - return permalink('simpletext_detail_view_name', [str(self.id)]) + + return permalink("simpletext_detail_view_name", [str(self.id)]) except ImportError: from django.urls import reverse - return reverse('simpletext_detail_view_name', args=[str(self.id)]) + + return reverse("simpletext_detail_view_name", args=[str(self.id)]) class SimpleCategory(CategoryBase): - """A Test of catgorizing""" + """A Test of catgorizing.""" + class Meta: - verbose_name_plural = 'simple categories' + verbose_name_plural = "simple categories" # mport categories diff --git a/example/simpletext/tests.py b/example/simpletext/tests.py old mode 100755 new mode 100644 index 73d6465..f51d798 --- a/example/simpletext/tests.py +++ b/example/simpletext/tests.py @@ -16,9 +16,11 @@ class SimpleTest(TestCase): self.assertEqual(1 + 1, 2) -__test__ = {"doctest": """ +__test__ = { + "doctest": """ Another way to test that 1 + 1 is equal to 2. >>> 1 + 1 == 2 True -"""} +""" +} diff --git a/example/simpletext/views.py b/example/simpletext/views.py old mode 100755 new mode 100644 index 60f00ef..8a89169 --- a/example/simpletext/views.py +++ b/example/simpletext/views.py @@ -1 +1 @@ -# Create your views here. +"""Create your views here.""" diff --git a/example/static/editor/jquery.treeTable.css b/example/static/editor/jquery.treeTable.css index f9fccd9..26828b6 100644 --- a/example/static/editor/jquery.treeTable.css +++ b/example/static/editor/jquery.treeTable.css @@ -62,4 +62,4 @@ .treeTable .ui-draggable-dragging { color: #000; z-index: 1; -} \ No newline at end of file +} diff --git a/example/static/editor/jquery.treeTable.js b/example/static/editor/jquery.treeTable.js index 522b2c5..412bdaf 100644 --- a/example/static/editor/jquery.treeTable.js +++ b/example/static/editor/jquery.treeTable.js @@ -455,4 +455,4 @@ function parentOf(node) { return $(node).parentOf(); } -})(django.jQuery); \ No newline at end of file +})(django.jQuery); diff --git a/example/static/js/genericcollections.js b/example/static/js/genericcollections.js index 584208e..8081eb8 100644 --- a/example/static/js/genericcollections.js +++ b/example/static/js/genericcollections.js @@ -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; -} \ No newline at end of file +} diff --git a/example/urls.py b/example/urls.py index 4c34998..b20f351 100644 --- a/example/urls.py +++ b/example/urls.py @@ -1,3 +1,4 @@ +"""URL patterns for the example project.""" import os from django.conf.urls import include, url @@ -12,23 +13,16 @@ ROOT_PATH = os.path.dirname(os.path.dirname(__file__)) urlpatterns = ( # Example: # (r'^sample/', include('sample.foo.urls')), - # Uncomment the admin/doc line below and add 'django.contrib.admindocs' # to INSTALLED_APPS to enable admin documentation: # (r'^admin/doc/', include('django.contrib.admindocs.urls')), - # Uncomment the next line to enable the admin: - url(r'^admin/', admin.site.urls), - url(r'^categories/', include('categories.urls')), + url(r"^admin/", admin.site.urls), + url(r"^categories/", include("categories.urls")), # r'^cats/', include('categories.urls')), - - url(r'^static/categories/(?P.*)$', serve, - {'document_root': ROOT_PATH + '/categories/media/categories/'}), - + url(r"^static/categories/(?P.*)$", serve, {"document_root": ROOT_PATH + "/categories/media/categories/"}), # (r'^static/editor/(?P.*)$', 'django.views.static.serve', # {'document_root': ROOT_PATH + '/editor/media/editor/', # 'show_indexes':True}), - - url(r'^static/(?P.*)$', serve, {'document_root': os.path.join(ROOT_PATH, 'example', 'static')}), - + url(r"^static/(?P.*)$", serve, {"document_root": os.path.join(ROOT_PATH, "example", "static")}), ) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1cb253a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,82 @@ +[build-system] +requires = [ + "setuptools >= 40.9.0", + "wheel", +] +build-backend = "setuptools.build_meta" + +[tool.coverage.run] +branch = true +omit = ["**/test_*.py"] + +[tool.coverage.report] +omit = [ + "*site-packages*", + "*tests*", + "*.tox*", +] +show_missing = true +exclude_lines = [ + "raise NotImplementedError", + "pragma: no-coverage", +] + +[tool.interrogate] +ignore-init-method = true +ignore-init-module = false +ignore-magic = true +ignore-semiprivate = false +ignore-private = false +ignore-property-decorators = false +ignore-module = false +ignore-nested-functions = true +ignore-nested-classes = true +ignore-setters = false +fail-under = 60 +exclude = ["setup.py", "docs", "build", "test"] +ignore-regex = ["^get$", "^mock_.*", ".*BaseClass.*"] +verbose = 0 +quiet = false +whitelist-regex = [] +color = true + +[tool.isort] +py_version = "38" +force_grid_wrap = 0 +use_parentheses = true +line_length = 88 +known_typing = ["typing", "types", "typing_extensions", "mypy", "mypy_extensions"] +sections = ["FUTURE", "TYPING", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] +include_trailing_comma = true +profile = "black" +multi_line_output = 3 +indent = 4 +color_output = true + +[tool.pydocstyle] +convention = "google" +add-ignore = ["D104", "D105", "D106", "D107", "D200", "D212"] +match = "(?!test_).*\\.py" + +[tool.black] +line-length = 119 +target-version = ['py38', 'py39'] +include = '\.pyi?$' +exclude = ''' +/( + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist + # The following are specific to Black, you probably don't want those. + | blib2to3 + | tests/data + | profiling +)/ +''' diff --git a/requirements.txt b/requirements.txt index 0d767c5..5eaadb8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ -django-mptt==0.11.0 -unicode-slugify==0.1.3 +-r requirements/prod.txt diff --git a/requirements/dev.txt b/requirements/dev.txt new file mode 100644 index 0000000..1c3dcd1 --- /dev/null +++ b/requirements/dev.txt @@ -0,0 +1,8 @@ +--find-links https://github.com/PennyDreadfulMTG/pystache/releases/ +-r test.txt + +bump2version>=1.0.1 +git-fame>=1.12.2 +gitchangelog>=3.0.4 +pre-commit +pystache>=0.6.0 diff --git a/requirements/prod.txt b/requirements/prod.txt new file mode 100644 index 0000000..b0a4601 --- /dev/null +++ b/requirements/prod.txt @@ -0,0 +1,2 @@ +django-mptt +unicode-slugify diff --git a/requirements/test.txt b/requirements/test.txt new file mode 100644 index 0000000..4f858b5 --- /dev/null +++ b/requirements/test.txt @@ -0,0 +1,9 @@ +-r prod.txt +django<4.0.0 +ghp-import +linkify-it-py +myst-parser +pydata-sphinx-theme +Sphinx>=4.3.0 +sphinx-autodoc-typehints +sphinxcontrib-django2 diff --git a/setup.cfg b/setup.cfg index 2a9acf1..70fb57f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,59 @@ +[metadata] +name = django-categories +version = 1.8.0 +description = A way to handle one or more hierarchical category trees in django. +long_description = file:README.md +long_description_content_type = "text/markdown" +author = Corey Oordt +author_email = coreyoordt@gmail.com +url = http://github.com/jazzband/django-categories +classifiers = + Framework :: Django + +[options] +zip_safe=False +include_package_data=True + +[options.packages.find] +exclude = + example* + docs + build +include = categories + +[flake8] +ignore = D203,W503,E501 +exclude = + .git + .tox + docs + build + dist + doc_src +max-line-length = 119 + +[darglint] +ignore=DAR402 + [bdist_wheel] universal = 1 + +[bumpversion] +current_version = 1.8.0 +commit = True +tag = False +tag_name = {new_version} +parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\+\w+-(?P\d+))? +serialize = + {major}.{minor}.{patch}+{$USER}-{dev} + {major}.{minor}.{patch} +message = Version updated from {current_version} to {new_version} + +[bumpversion:part:dev] + +[bumpversion:file:setup.cfg] + +[bumpversion:file:categories/__init__.py] + +[bumpversion:file:CHANGELOG.md] +search = Unreleased diff --git a/setup.py b/setup.py index cbf5c6f..d67a743 100644 --- a/setup.py +++ b/setup.py @@ -1,30 +1,50 @@ -from setuptools import setup, find_packages -import categories -import os +"""The setup script.""" -try: - long_description = open('README.rst').read() -except IOError: - long_description = '' +from pathlib import Path -try: - reqs = open(os.path.join(os.path.dirname(__file__), 'requirements.txt')).read() -except (IOError, OSError): - reqs = '' +from setuptools import setup + + +def parse_reqs(filepath: str) -> list: + """ + Parse a file path containing requirements and return a list of requirements. + + Will properly follow ``-r`` and ``--requirements`` links like ``pip``. This + means nested requirements will be returned as one list. + + Other ``pip``-specific lines are excluded. + + Args: + filepath: The path to the requirements file + + Returns: + All the requirements as a list. + """ + path = Path(filepath) + reqstr = path.read_text() + reqs = [] + for line in reqstr.splitlines(): + line = line.strip() + if line == "": + continue + elif not line or line.startswith("#"): + # comments are lines that start with # only + continue + elif line.startswith("-r") or line.startswith("--requirement"): + _, new_filename = line.split() + new_file_path = path.parent / new_filename + reqs.extend(parse_reqs(new_file_path)) + elif line.startswith("-f") or line.startswith("-i") or line.startswith("--"): + continue + elif line.startswith("-Z") or line.startswith("--always-unzip"): + continue + else: + reqs.append(line) + return reqs + + +requirements = parse_reqs("requirements.txt") setup( - name='django-categories', - version=categories.get_version(), - description='A way to handle one or more hierarchical category trees in django.', - long_description=long_description, - author='Corey Oordt', - author_email='coreyoordt@gmail.com', - include_package_data=True, - url='http://github.com/callowayproject/django-categories', - packages=find_packages(exclude=['example*', ]), - classifiers=[ - 'Framework :: Django', - ], - install_requires=reqs, - dependency_links=[] + install_requires=requirements, ) diff --git a/tox.ini b/tox.ini index 04440c5..53dd214 100644 --- a/tox.ini +++ b/tox.ini @@ -2,10 +2,19 @@ envlist = begin py36-lint - py36-django{111,2,21,22,3,31} + py{36,37,38,39}-django{111,2,21,22,3,31} coverage-report +[gh-actions] +python = + 3.6: py36 + 3.7: py37 + 3.8: py38 + 3.9: py39 + [testenv] +passenv = GITHUB_* + deps= django2: Django>=2.0,<2.1 django21: Django>=2.1,<2.2 @@ -13,13 +22,23 @@ deps= django3: Django>=3.0,<3.1 django31: Django>=3.1,<3.2 django111: Django>=1.11,<1.12 - coverage + coverage[toml] pillow ipdb + codecov -r{toxinidir}/requirements.txt commands= - coverage run --source=categories --omit='.tox/*,example/*,*/tests/*' {toxinidir}/example/manage.py test --settings='settings-testing' categories{posargs} + coverage erase + coverage run \ + --source=categories \ + --omit='.tox/*,example/*,*/tests/*' \ + {toxinidir}/example/manage.py \ + test \ + --settings='settings-testing' \ + categories{posargs} + coverage report -m + coverage xml [testenv:begin] commands = coverage erase @@ -29,9 +48,10 @@ deps= flake8 commands= - flake8 . --ignore=E501 --exclude=build/,dist/,.tox/,doc_src + flake8 [testenv:coverage-report] commands = - coverage report -m - coverage html + coverage report -m + coverage xml + codecov