diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..5487327b7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# top-most EditorConfig file +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[*.py] +indent_size = 4 diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..63570efc3 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,27 @@ +module.exports = { + parser: "@babel/eslint-parser", + env: { + browser: true, + es6: true, + node: true, + }, + extends: ["eslint:recommended", "prettier"], + parserOptions: { + ecmaFeatures: { + experimentalObjectRestSpread: true, + jsx: true, + }, + ecmaVersion: 2021, + requireConfigFile: false, + sourceType: "module", + }, + rules: { + "no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + }, + ], + }, +} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 000000000..52b3760de --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,36 @@ +name: Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + tests: + name: Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: + - "3.8" + - "3.9" + - "3.10" + - "3.11" + - "3.12" + - "3.13" + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip wheel setuptools tox + - name: Run tox targets for ${{ matrix.python-version }} + run: | + ENV_PREFIX=$(tr -C -d "0-9" <<< "${{ matrix.python-version }}") + TOXENV=$(tox --listenvs | grep "^py$ENV_PREFIX" | tr '\n' ',') python -m tox diff --git a/.gitignore b/.gitignore index 45f25c901..c02765c74 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,23 @@ *.pyc *~ .*.swp -\#*# -/secrets.py .DS_Store ._* +.coverage +.tox /FeinCMS.egg-info /MANIFEST /_build /build /dist -tests/test.zip /docs/_build -/tests/.tox +/secrets.py /tests/.coverage +/tests/.tox /tests/htmlcov +\#*# +htmlcov +node_modules +test.zip +tests/test.zip venv diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..b4c083047 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,40 @@ +exclude: ".yarn/|yarn.lock|\\.min\\.(css|js)$" +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-added-large-files + - id: check-builtin-literals + - id: check-executables-have-shebangs + - id: check-merge-conflict + - id: check-toml + - id: check-yaml + - id: detect-private-key + - id: end-of-file-fixer + - id: mixed-line-ending + - id: trailing-whitespace + - repo: https://github.com/adamchainz/django-upgrade + rev: 1.28.0 + hooks: + - id: django-upgrade + args: [--target-version, "3.2"] + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.13.0" + hooks: + - id: ruff + args: [--unsafe-fixes] + - id: ruff-format + - repo: https://github.com/biomejs/pre-commit + rev: "v2.2.4" + hooks: + - id: biome-check + args: [--unsafe] + verbose: true + - repo: https://github.com/tox-dev/pyproject-fmt + rev: v2.6.0 + hooks: + - id: pyproject-fmt + - repo: https://github.com/abravalheri/validate-pyproject + rev: v0.24.1 + hooks: + - id: validate-pyproject diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..1c0ca47e9 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +feincms/static/feincms/jquery-1.11.3.min.js +feincms/static/feincms/jquery-ui-1.10.3.custom.min.js +feincms/static/feincms/js.cookie.js diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 000000000..bcb0c684e --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,17 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +version: 2 + +build: + os: ubuntu-24.04 + tools: + python: "3.11" + +sphinx: + configuration: docs/conf.py +# python: +# install: +# - requirements: docs/requirements.txt +# - method: pip +# path: . diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2ccfe3b5d..000000000 --- a/.travis.yml +++ /dev/null @@ -1,44 +0,0 @@ -language: python -sudo: false -cache: pip -python: - - "3.6" -env: - - REQ="" -matrix: - include: - - python: "2.7" - env: REQ="Django>=1.7,<1.8 django-mptt<0.8" - - python: "2.7" - env: REQ="Django>=1.8,<1.9 django-mptt<0.8" - - python: "2.7" - env: REQ="Django>=1.9,<1.10 django-mptt<0.9" - - python: "2.7" - env: REQ="Django>=1.10,<1.11 django-mptt<0.9" - - python: "2.7" - env: REQ="Django>=1.11,<2.0 django-mptt" - - python: "3.4" - env: REQ="Django>=1.11,<2.0 django-mptt" - - python: "3.4" - env: REQ="Django>=2.0,<2.1 django-mptt" - - python: "3.5" - env: REQ="Django>=1.11,<2.0 django-mptt" - - python: "3.5" - env: REQ="Django>=2.0,<2.1 django-mptt" - - python: "3.5" - env: REQ="Django>=2.1,<2.2 django-mptt" - - python: "3.6" - env: REQ="Django>=1.11,<2.0 django-mptt" - - python: "3.6" - env: REQ="Django>=2.0,<2.1 django-mptt" - - python: "3.6" - env: REQ="Django>=2.1,<2.2 django-mptt" - - python: "3.6" - env: REQ="https://github.com/django/django/archive/master.zip django-mptt" - allow_failures: - - env: REQ="https://github.com/django/django/archive/master.zip django-mptt" -install: - - pip install -U pip wheel setuptools - - pip install $REQ Pillow flake8 pytz - - python setup.py install -script: "cd tests && ./manage.py test testapp && cd .. && flake8 ." diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cd9db6621..d72d9d44d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,13 +3,208 @@ Change log ========== -`Next version`_ -~~~~~~~~~~~~~~~ +Next version +~~~~~~~~~~~~ + +- Added a tinymce 7 integration for the richtext content type. + + +v24.8.1 (2024-08-07) +~~~~~~~~~~~~~~~~~~~~ + +- Fixed another edge case: JPEGs cannot be saved as RGBA. +- Added Django 5.1 to the CI. + + +v24.7.1 (2024-07-10) +~~~~~~~~~~~~~~~~~~~~ + +- Fixed the read the docs build. +- Disabled the CKEditor 4 version nag. + + +v24.4.2 (2024-04-18) +~~~~~~~~~~~~~~~~~~~~ + +- Fixed the filters to work with Django 5. + + +v24.4.1 (2024-04-16) +~~~~~~~~~~~~~~~~~~~~ + +- Forwarded cookies set by ``ApplicationContent`` apps to the final response. +- Added support for webp image formats to the media library. + + +v24.4.0 (2024-04-08) +~~~~~~~~~~~~~~~~~~~~ + +- Fetched the CSRF token value from the input field instead of from the cookie. + This allows making the CSRF cookie ``httponly``. Thanks to Samuel Lim for the + contribution! + + +v23.12.0 (2023-12-22) +~~~~~~~~~~~~~~~~~~~~~ + +- Added Python 3.12, Django 5.0. +- Closed images after reading their dimensions. Raised the logging level to + exception when thumbnailing fails. Thanks to Jeroen Pulles for those two + contributions! + + +`v23.8.0`_ (2023-08-07) +~~~~~~~~~~~~~~~~~~~~~~~ + +.. _v23.8.0: https://github.com/feincms/feincms/compare/v23.1.0...v23.8.0 + +- Made the filter argument of content base's ``get_queryset`` method optional. + This enables easier interoperability of FeinCMS content types with feincms3 + plugins. +- Added Python 3.11. +- Fixed the Pillow resampling constant. + + +`v23.1.0`_ (2023-03-09) +~~~~~~~~~~~~~~~~~~~~~~~ + +.. _v23.1.0: https://github.com/feincms/feincms/compare/v22.4.0...v23.1.0 + +- Fixed a place where ``ACTION_CHECKBOX_NAME`` was imported from the wrong + place. +- Dropped the ``is_dst`` argument to ``timezone.make_aware``. +- Added Django 4.1 and 4.2 to the CI matrix. + + +`v22.4.0`_ (2022-06-02) +~~~~~~~~~~~~~~~~~~~~~~~ + +.. _v22.4.0: https://github.com/feincms/feincms/compare/v22.3.0...v22.4.0 + +- Changed the ``template_key`` field type to avoid boring migrations because of + changing choices. + + +`v22.3.0`_ (2022-05-17) +~~~~~~~~~~~~~~~~~~~~~~~ + +.. _v22.3.0: https://github.com/feincms/feincms/compare/v22.2.0...v22.3.0 + +- The ``render()`` methods of bundled content types have been changed to return + a tuple instead of a HTML fragment in FeinCMS v22.0.0. This was backwards + incompatible in some scenarios. Those methods have been changed to return a + tuple subclass which automatically renders a HTML fragment if evaluated in a + string context. + + +`v22.2.0`_ (2022-05-06) +~~~~~~~~~~~~~~~~~~~~~~~ + +.. _v22.2.0: https://github.com/feincms/feincms/compare/v22.1.0...v22.2.0 + +- Dropped support for Python < 3.8. +- Fixed the thumbnailing support of the ``MediaFileForeignKey``. It has been + broken since Django switched to template-based widget rendering. + + +`v22.1.0`_ (2022-03-31) +~~~~~~~~~~~~~~~~~~~~~~~ + +.. _v22.1.0: https://github.com/feincms/feincms/compare/v22.0.0...v22.1.0 + +- Fixed the ``feincms_render_level`` render recursion protection. +- Wrapped the recursive saving of pages in a transaction, so if anything fails + we have a consistent state. +- Dropped more compatibility code for Django 1.x. +- Made ``medialibrary_orphans`` work again. +- Removed the ``six`` dependency since we're Python 3-only now. +- Updated the pre-commit hooks, cleaned up the JavaScript a bit. + + +`v22.0.0`_ (2022-01-07) +~~~~~~~~~~~~~~~~~~~~~~~ + +.. _v22.0.0: https://github.com/feincms/feincms/compare/v1.20.0...v22.0.0 + +- **Possibly backwards incompatible** Changed all bundled content types' + ``render()`` methods to return the ``(template_name, context)`` tuple instead + of rendering content themselves. +- Dropped compatibility guarantees with Python < 3.6, Django < 3.2. +- Added pre-commit. +- The default view was changed to accept the path as a ``path`` keyword + argument, not only as a positional argument. +- Changed the item editor action buttons CSS to not use transitions so that the + sprite buttons look as they should. + + +`v1.20.0`_ (2021-03-22) +~~~~~~~~~~~~~~~~~~~~~~~ + +- Changed ``#main`` to the more specific ``#feincmsmain`` so that it doesn't + collide with Django's admin panel markup. +- Stopped the JavaScript code from constructing invalid POST action URLs in the + change form. +- Renamed the main branch to main. +- Switched to a declarative setup. +- Switched to GitHub actions. +- Sorted imports. +- Reformated the JavaScript code using prettier. +- Added Python up to 3.9, Django up to the main branch (the upcoming 4.0) to + the CI list. + + +`v1.19.0`_ (2021-03-04) +~~~~~~~~~~~~~~~~~~~~~~~ + +- Fixed a bug where the thumbnailer would try to save JPEGs as RGBA. +- Reformatted the code using black, again. +- Added Python 3.8, Django 3.1 to the build. +- Added the Django 3.2 `.headers` property to the internal dummy response used + in the etag request processor. +- Added a workaround for ``AppConfig``-autodiscovery related crashes. (Because + ``feincms.apps`` now has more meanings). Changed the documentation to prefer + ``feincms.content.application.models.*`` to ``feincms.apps.*``. +- Updated the TinyMCE CDN URL to an version which doesn't show JavaScript + alerts. +- Added missing ``on_delete`` values to the django-filer content types. + + +`v1.18.0`_ (2020-01-21) +~~~~~~~~~~~~~~~~~~~~~~~ + +- Added a style checking job to the CI matrix. +- Dropped compatibility with Django 1.7. + + +`v1.17.0`_ (2019-11-21) +~~~~~~~~~~~~~~~~~~~~~~~ + +- Added compatibility with Django 3.0. + + +`v1.16.0`_ (2019-02-01) +~~~~~~~~~~~~~~~~~~~~~~~ + +- Reformatted everything using black. +- Added a fallback import for the ``staticfiles`` template tag library + which will be gone in Django 3.0. + + +`v1.15.0`_ (2018-12-21) +~~~~~~~~~~~~~~~~~~~~~~~ - Actually made use of the timeout specified as ``FEINCMS_THUMBNAIL_CACHE_TIMEOUT`` instead of the hardcoded value of seven days. - Reverted the deprecation of navigation extension autodiscovery. +- Fixed the item editor JavaScript and HTML to work with Django 2.1's + updated inlines. +- Fixed ``TranslatedObjectManager.only_language`` to evaluate callables + before filtering. +- Changed the ``render`` protocol of content types to allow returning a + tuple of ``(ct_template, ct_context)`` which works the same way as + `feincms3's template renderers + `__. `v1.14.0`_ (2018-08-16) @@ -44,4 +239,9 @@ Change log .. _v1.14.0: https://github.com/feincms/feincms/compare/v1.13.0...v1.14.0 -.. _Next version: https://github.com/feincms/feincms/compare/v1.14.0...master +.. _v1.15.0: https://github.com/feincms/feincms/compare/v1.14.0...v1.15.0 +.. _v1.16.0: https://github.com/feincms/feincms/compare/v1.15.0...v1.16.0 +.. _v1.17.0: https://github.com/feincms/feincms/compare/v1.16.0...v1.17.0 +.. _v1.18.0: https://github.com/feincms/feincms/compare/v1.17.0...v1.18.0 +.. _v1.19.0: https://github.com/feincms/feincms/compare/v1.18.0...v1.19.0 +.. _v1.20.0: https://github.com/feincms/feincms/compare/v1.19.0...v1.20.0 diff --git a/README.rst b/README.rst index a3fb80618..cb0ebc95e 100644 --- a/README.rst +++ b/README.rst @@ -1,11 +1,11 @@ +**NOTE! If you're starting a new project you may want to take a look at feincms3 (https://feincms3.readthedocs.io/). FeinCMS is still maintained and works well, but feincms3 is where current development is happening.** + ======================================== FeinCMS - An extensible Django-based CMS ======================================== -.. image:: https://travis-ci.org/feincms/feincms.svg?branch=next - :target: https://travis-ci.org/feincms/feincms -.. image:: https://travis-ci.org/feincms/feincms.svg?branch=master - :target: https://travis-ci.org/feincms/feincms +.. image:: https://github.com/feincms/feincms/workflows/Tests/badge.svg + :target: https://github.com/feincms/feincms When was the last time, that a pre-built software package you wanted to use got many things right, but in the end, you still needed to modify @@ -89,9 +89,9 @@ The FeinCMS repository on github has several branches. Their purpose and rewinding policies are described below. * ``maint``: Maintenance branch for the second-newest version of FeinCMS. -* ``master``: Stable version of FeinCMS. +* ``main``: Stable version of FeinCMS. -``master`` and ``maint`` are never rebased or rewound. +``main`` and ``maint`` are never rebased or rewound. * ``next``: Upcoming version of FeinCMS. This branch is rarely rebased if ever, but this might happen. A note will be sent to the official diff --git a/biome.json b/biome.json new file mode 100644 index 000000000..915669572 --- /dev/null +++ b/biome.json @@ -0,0 +1,61 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.2.3/schema.json", + "assist": { "actions": { "source": { "organizeImports": "off" } } }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "a11y": { + "noSvgWithoutTitle": "off" + }, + "complexity": { + "noImportantStyles": "off" + }, + "correctness": { + "noInnerDeclarations": "off", + "noUndeclaredVariables": "warn", + "noUnknownTypeSelector": "warn", + "noUnusedImports": "error", + "noUnusedVariables": "error", + "useHookAtTopLevel": "error" + }, + "security": { + "noDangerouslySetInnerHtml": "warn" + }, + "style": { + "noDescendingSpecificity": "warn", + "noParameterAssign": "off", + "useForOf": "warn", + "useArrayLiterals": "error" + }, + "suspicious": { + "noArrayIndexKey": "warn", + "noAssignInExpressions": "off" + } + } + }, + "javascript": { + "formatter": { + "semicolons": "asNeeded" + }, + "globals": ["django", "CKEDITOR"] + }, + "css": { + "formatter": { + "enabled": true + }, + "linter": { + "enabled": true + } + }, + "json": { + "formatter": { + "enabled": false + } + } +} diff --git a/docs/advanced/base.rst b/docs/advanced/base.rst index 76605d03c..3b50bca08 100644 --- a/docs/advanced/base.rst +++ b/docs/advanced/base.rst @@ -15,7 +15,7 @@ manage content with the :class:`~feincms.admin.item_editor.ItemEditor`. .. attribute:: Base.content Beware not to name subclass field `content` as this will overshadow `ContentProxy` and you will - not be able to reference `ContentProxy`. + not be able to reference `ContentProxy`. .. method:: Base.create_content_type(model, regions=None, [**kwargs]) diff --git a/docs/advanced/caching.rst b/docs/advanced/caching.rst index 9c6b67df2..cf4f2b6cd 100644 --- a/docs/advanced/caching.rst +++ b/docs/advanced/caching.rst @@ -36,16 +36,15 @@ Here's an (incomplete) list of variables to use in {% cache %} blocks [#djangoca Depending on the extensions loaded, this varies with the page, the page's modification date, its language, etc. This is always a safe bet to use on page specific fragments. - + * LANGUAGE_CODE -- even if two requests are asking for the same page, the html code rendered might differ in translated elements in the navigation or elsewhere. If the fragment varies on language, include LANGUAGE_CODE in the cache specifier. - + * request.user.id -- different users might be allowed to see different views of the site. Add request.user.id to the cache specifier if this is the case. -.. [#djangocache] Please see the django documentation for detailed +.. [#djangocache] Please see the django documentation for detailed description of the {% cache %} template tag. - diff --git a/docs/conf.py b/docs/conf.py index 2acc7da62..ec2f3cd0f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # FeinCMS documentation build configuration file, created by # sphinx-quickstart on Mon Aug 10 17:03:33 2009. @@ -14,6 +13,7 @@ import os import sys + # 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. @@ -25,20 +25,20 @@ extensions = [] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8' +# source_encoding = 'utf-8' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'FeinCMS' -copyright = u'2009-2010, Feinheit GmbH and contributors' +project = "FeinCMS" +copyright = "2009-2010, Feinheit GmbH and contributors" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -46,48 +46,49 @@ # # The short X.Y version. sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) -import feincms +import feincms # noqa + -version = '.'.join(map(str, feincms.VERSION)) +version = ".".join(map(str, feincms.VERSION)) # The full version, including alpha/beta/rc tags. release = feincms.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. -#unused_docs = [] +# unused_docs = [] # List of directories, relative to source directory, that shouldn't be searched # for source files. -exclude_trees = ['_build'] +exclude_trees = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- @@ -95,104 +96,109 @@ # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. # html_theme_path = ['_theme'] -# html_theme = 'nature' +# html_theme = "sphinx_rtd_theme" # 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. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_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. -#html_favicon = None +# html_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'] +html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_use_modindex = True +# html_use_modindex = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_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. -#html_use_opensearch = '' +# html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = '' +# html_file_suffix = '' # Output file base name for HTML help builder. -htmlhelp_basename = 'FeinCMSdoc' +htmlhelp_basename = "FeinCMSdoc" # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). -latex_paper_size = 'a4' +latex_paper_size = "a4" # The font size ('10pt', '11pt' or '12pt'). -latex_font_size = '10pt' +latex_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', 'FeinCMS.tex', u'FeinCMS Documentation', - u'Feinheit GmbH and contributors', 'manual'), +latex_documents = [ + ( + "index", + "FeinCMS.tex", + "FeinCMS Documentation", + "Feinheit GmbH and contributors", + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # Additional stuff for the LaTeX preamble. -#latex_preamble = '' +# latex_preamble = '' # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_use_modindex = True +# latex_use_modindex = True diff --git a/docs/contenttypes.rst b/docs/contenttypes.rst index dbb365fd2..e9fadb201 100644 --- a/docs/contenttypes.rst +++ b/docs/contenttypes.rst @@ -307,7 +307,7 @@ Bundled content types Application content ------------------- -.. module:: feincms.apps +.. module:: feincms.content.application.models .. class:: ApplicationContent() Used to let the administrator freely integrate 3rd party applications into diff --git a/docs/contributing.rst b/docs/contributing.rst index 7f9ca1991..4feed80a0 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -12,9 +12,9 @@ The FeinCMS repository on github has several branches. Their purpose and rewinding policies are described below. * ``maint``: Maintenance branch for the second-newest version of FeinCMS. -* ``master``: Stable version of FeinCMS. +* ``main``: Stable version of FeinCMS. -``master`` and ``maint`` are never rebased or rewound. +``main`` and ``maint`` are never rebased or rewound. * ``next``: Upcoming version of FeinCMS. This branch is rarely rebased if ever, but this might happen. A note will be sent to the official diff --git a/docs/extensions.rst b/docs/extensions.rst index 979328d3c..55fa96dcf 100644 --- a/docs/extensions.rst +++ b/docs/extensions.rst @@ -74,4 +74,4 @@ is as follows: Only model and admin instances which inherit from :class:`~feincms.extensions.ExtensionsMixin` and :class:`~feincms.extensions.ExtensionModelAdmin` can be extended - this way. \ No newline at end of file + this way. diff --git a/docs/index.rst b/docs/index.rst index 2188ee8d2..b5110b395 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -63,24 +63,27 @@ Contents deprecation -Releases -======== +.. include:: ../CHANGELOG.rst + + +Old release notes +================= .. toctree:: :maxdepth: 1 - releases/1.2 - releases/1.3 - releases/1.4 - releases/1.5 - releases/1.6 - releases/1.7 - releases/1.8 - releases/1.9 - releases/1.10 - releases/1.11 - releases/1.12 releases/1.13 + releases/1.12 + releases/1.11 + releases/1.10 + releases/1.9 + releases/1.8 + releases/1.7 + releases/1.6 + releases/1.5 + releases/1.4 + releases/1.3 + releases/1.2 Indices and tables diff --git a/docs/installation.rst b/docs/installation.rst index 98fbdc790..e607bccf2 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -16,8 +16,8 @@ You can download a stable release of FeinCMS using ``pip``. Pip will install feincms and its dependencies. Dependencies which are automatically installed are: feedparser_, Pillow_ and django-mptt_. For outdated versions of Django the best place to find supported combinations of library versions is the -`Travis CI build configuration -`_. +`tox build configuration +`_. $ pip install feincms @@ -26,6 +26,10 @@ instead:: $ git clone git://github.com/feincms/feincms.git +Please be aware that feincms3 is being developed as a separate, new project. +For new CMS projects I’m more likely to use django-content-editor and feincms3. +You can read more about feincms3 on https://feincms3.readthedocs.io/en/latest/ + If you are looking to implement a blog, check out elephantblog_. You will also need a Javascript WYSIWYG editor of your choice (Not included). @@ -54,12 +58,6 @@ There isn't much left to do apart from adding a few entries to feincms.module.page, feincms.module.medialibrary -Also, you should add the request context processor to the list of -``TEMPLATE_CONTEXT_PROCESSORS``, the template tag and the administration -interface require it:: - - django.core.context_processors.request - The customized administration interface needs some media and javascript libraries which you have to make available to the browser. FeinCMS uses Django's ``django.contrib.staticfiles`` application for this purpose. The media diff --git a/docs/integration.rst b/docs/integration.rst index c3a3b3eb4..47de52696 100644 --- a/docs/integration.rst +++ b/docs/integration.rst @@ -139,7 +139,7 @@ details) mimicking the interface of Django's standard functionality:: from django.db import models - from feincms.apps import app_reverse + from feincms.content.application.models import app_reverse class Entry(models.Model): title = models.CharField(max_length=200) @@ -267,7 +267,7 @@ content. .. note:: Older versions of FeinCMS only offered fragments for a similar purpose. They - are still suported, but it's recommended you switch over to this style instead. + are still supported, but it's recommended you switch over to this style instead. .. warning:: @@ -287,7 +287,7 @@ that it resolves URLs from application contents. The second argument, ``urlconf``, has to correspond to the URLconf parameter passed in the ``APPLICATIONS`` list to ``Page.create_content_type``:: - from feincms.apps import app_reverse + from feincms.content.application.models import app_reverse app_reverse('mymodel-detail', 'myapp.urls', args=...) or:: @@ -478,5 +478,3 @@ to work, ie. at least the following attributes should exist: level = page.level+1 lft = page.lft rght = page.rght - - diff --git a/docs/medialibrary.rst b/docs/medialibrary.rst index e7ed0dd5d..4be37efdc 100644 --- a/docs/medialibrary.rst +++ b/docs/medialibrary.rst @@ -128,4 +128,3 @@ from FeinCMSInline:: class MyContent(models.Model): feincms_item_editor_inline = MyContentInline - diff --git a/docs/page.rst b/docs/page.rst index e54a460e6..18e19c94d 100644 --- a/docs/page.rst +++ b/docs/page.rst @@ -34,7 +34,7 @@ be to create :class:`~feincms.content.medialibrary.models.MediaFileContent` and by adding the following lines somewhere into your project, for example in a ``models.py`` file that will be processed anyway:: - from django.utils.translation import ugettext_lazy as _ + from django.utils.translation import gettext_lazy as _ from feincms.module.page.models import Page from feincms.contents import RichTextContent @@ -291,7 +291,7 @@ This allows for various actions dependent on page and request, for example a simple user access check can be implemented like this:: def authenticated_request_processor(page, request): - if not request.user.is_authenticated(): + if not request.user.is_authenticated: raise django.core.exceptions.PermissionDenied Page.register_request_processor(authenticated_request_processor) diff --git a/docs/settings.rst b/docs/settings.rst index 312d2d22f..bc4f037ed 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -37,12 +37,13 @@ initialization snippet for the rich text editor. Bundled templates are: * ``admin/content/richtext/init_tinymce.html`` for TinyMCE 3.x. * ``admin/content/richtext/init_tinymce4.html`` for TinyMCE 4.x. +* ``admin/content/richtext/init_tinymce7.html`` for TinyMCE 7.x. * ``admin/content/richtext/init_ckeditor.html`` for CKEditor. ``FEINCMS_RICHTEXT_INIT_CONTEXT``: Defaults to -``{'TINYMCE_JS_URL': '//tinymce.cachefly.net/4.1/tinymce.min.js'}``. A dictionary -which is passed to the template mentioned above. Please refer to the templates -directly to see all available variables. +``{'TINYMCE_JS_URL': 'https://cdnjs.cloudflare.com/ajax/libs/tinymce/4.9.11/tinymce.min.js'}``. +A dictionary which is passed to the template mentioned above. Please +refer to the templates directly to see all available variables. Settings for the tree editor @@ -98,6 +99,10 @@ pages. Prevent admin page deletion for pages which have been allocated a Template with ``singleton=True``. +``FEINCMS_MEDIAFILE_TRANSLATIONS``: Defaults to ``True``. Set to ``False`` if +you want FeinCMS to not translate ``MediaFile`` names, and instead just use the +filename directly. + Various settings ================ diff --git a/docs/versioning.rst b/docs/versioning.rst index 68d87fd2d..b4374c9f1 100644 --- a/docs/versioning.rst +++ b/docs/versioning.rst @@ -12,10 +12,13 @@ with django-reversion_: * Add ``'reversion'`` to the list of installed applications. * Add ``'reversion.middleware.RevisionMiddleware'`` to ``MIDDLEWARE_CLASSES``. -* Call ``Page.register_with_reversion()`` after all content types have been - created (after all ``create_content_type`` invocations). +* Call ``Page.register_with_reversion(**kwargs)`` after all content types have been + created (after all ``create_content_type`` invocations). You can optionally + supply kwargs_ that will be passed to ``reversion.register()``. * Add ``FEINCMS_USE_PAGE_ADMIN = False`` to your ``settings`` file. +.. _kwargs: https://django-reversion.readthedocs.io/en/stable/api.html#registration-api + Now, you need to create your own model admin subclass inheriting from both FeinCMS' ``PageAdmin`` and from reversions ``VersionAdmin``:: diff --git a/feincms/__init__.py b/feincms/__init__.py index 78d50b052..8cd0f3219 100644 --- a/feincms/__init__.py +++ b/feincms/__init__.py @@ -1,16 +1,15 @@ -from __future__ import absolute_import, unicode_literals +VERSION = (25, 5, 1) +__version__ = ".".join(map(str, VERSION)) -VERSION = (1, 14, 1) -__version__ = '.'.join(map(str, VERSION)) - -class LazySettings(object): +class LazySettings: def _load_settings(self): - from feincms import default_settings from django.conf import settings as django_settings + from feincms import default_settings + for key in dir(default_settings): - if not key.startswith('FEINCMS_'): + if not key.startswith("FEINCMS_"): continue value = getattr(default_settings, key) @@ -24,71 +23,3 @@ def __getattr__(self, attr): settings = LazySettings() - - -COMPLETELY_LOADED = False - - -def ensure_completely_loaded(force=False): - """ - This method ensures all models are completely loaded - - FeinCMS requires Django to be completely initialized before proceeding, - because of the extension mechanism and the dynamically created content - types. - - For more informations, have a look at issue #23 on github: - http://github.com/feincms/feincms/issues#issue/23 - """ - - global COMPLETELY_LOADED - if COMPLETELY_LOADED and not force: - return True - - from django.apps import apps - if not apps.ready: - return - - # Ensure meta information concerning related fields is up-to-date. - # Upon accessing the related fields information from Model._meta, - # the related fields are cached and never refreshed again (because - # models and model relations are defined upon import time, if you - # do not fumble around with models like we do in FeinCMS.) - # - # Here we flush the caches rather than actually _filling them so - # that relations defined after all content types registrations - # don't miss out. - import django - from distutils.version import LooseVersion - - if LooseVersion(django.get_version()) < LooseVersion('1.8'): - - for model in apps.get_models(): - for cache_name in ( - '_field_cache', '_field_name_cache', '_m2m_cache', - '_related_objects_cache', '_related_many_to_many_cache', - '_name_map'): - try: - delattr(model._meta, cache_name) - except AttributeError: - pass - - # Randomly call some cache filling methods - # http://goo.gl/XNI2qz - model._meta._fill_fields_cache() - - # Calls to get_models(...) are cached by the arguments used in the - # call. This cache is normally cleared in loading.register_models(), - # but we invalidate the get_models() cache, by calling get_models above - # before all apps have loaded. (Django's load_app() doesn't clear the - # get_models cache as it perhaps should). So instead we clear the - # get_models cache again here. If we don't do this, Django 1.5 chokes - # on a model validation error (Django 1.4 doesn't exhibit this - # problem). See Issue #323 on github. - if hasattr(apps, 'cache'): - apps.cache.get_models.cache_clear() - - if apps.ready: - COMPLETELY_LOADED = True - - return True diff --git a/feincms/_internal.py b/feincms/_internal.py index 134046da1..ab72b9e17 100644 --- a/feincms/_internal.py +++ b/feincms/_internal.py @@ -4,16 +4,7 @@ http://mail.python.org/pipermail/python-dev/2008-January/076194.html """ -from __future__ import absolute_import, unicode_literals - -from distutils.version import LooseVersion -from django import get_version -from django.template.loader import render_to_string - - -__all__ = ( - 'monkeypatch_method', 'monkeypatch_property', -) +__all__ = ("monkeypatch_method", "monkeypatch_property") def monkeypatch_method(cls): @@ -28,6 +19,7 @@ def (self, [...]): def decorator(func): setattr(cls, func.__name__, func) return func + return decorator @@ -43,24 +35,5 @@ def (self, [...]): def decorator(func): setattr(cls, func.__name__, property(func)) return func - return decorator - -if LooseVersion(get_version()) < LooseVersion('1.10'): - def ct_render_to_string(template, ctx, **kwargs): - from django.template import RequestContext - - context_instance = kwargs.get('context') - if context_instance is None and kwargs.get('request'): - context_instance = RequestContext(kwargs['request']) - - return render_to_string( - template, - ctx, - context_instance=context_instance) -else: - def ct_render_to_string(template, ctx, **kwargs): - return render_to_string( - template, - ctx, - request=kwargs.get('request')) + return decorator diff --git a/feincms/admin/__init__.py b/feincms/admin/__init__.py index 0a2edfe1f..bb45c8662 100644 --- a/feincms/admin/__init__.py +++ b/feincms/admin/__init__.py @@ -1,14 +1,15 @@ -from __future__ import absolute_import - from django.contrib.admin.filters import FieldListFilter -from .filters import ParentFieldListFilter, CategoryFieldListFilter + +from .filters import CategoryFieldListFilter, ParentFieldListFilter FieldListFilter.register( - lambda f: getattr(f, 'parent_filter', False), + lambda f: getattr(f, "parent_filter", False), ParentFieldListFilter, - take_priority=True) + take_priority=True, +) FieldListFilter.register( - lambda f: getattr(f, 'category_filter', False), + lambda f: getattr(f, "category_filter", False), CategoryFieldListFilter, - take_priority=True) + take_priority=True, +) diff --git a/feincms/admin/filters.py b/feincms/admin/filters.py index cb3187ded..7ae5bdc5a 100644 --- a/feincms/admin/filters.py +++ b/feincms/admin/filters.py @@ -1,17 +1,17 @@ -# encoding=utf-8 # Thanks to http://www.djangosnippets.org/snippets/1051/ # # Authors: Marinho Brandao # Guilherme M. Gondim (semente) -from __future__ import absolute_import, unicode_literals +from operator import itemgetter + +import django from django.contrib.admin.filters import ChoicesFieldListFilter from django.db.models import Count -from django.utils.encoding import smart_text +from django.utils.encoding import smart_str from django.utils.safestring import mark_safe -from django.utils.translation import ugettext as _ -from django import VERSION as DJANGO_VERSION +from django.utils.translation import gettext as _ from feincms.utils import shorten_string @@ -26,38 +26,50 @@ class ParentFieldListFilter(ChoicesFieldListFilter): my_model_field.page_parent_filter = True """ - def __init__(self, f, request, params, model, model_admin, - field_path=None): - super(ParentFieldListFilter, self).__init__( - f, request, params, model, model_admin, field_path) + def __init__(self, field, request, params, model, model_admin, field_path=None): + super().__init__(field, request, params, model, model_admin, field_path) - parent_ids = model.objects.exclude(parent=None).values_list( - "parent__id", flat=True).order_by("parent__id").distinct() + parent_ids = ( + model.objects.exclude(parent=None) + .values_list("parent__id", flat=True) + .order_by("parent__id") + .distinct() + ) parents = model.objects.filter(pk__in=parent_ids).values_list( - "pk", "title", "level") - self.lookup_choices = [( - pk, - "%s%s" % ( - "  " * level, - shorten_string(title, max_length=25)), - ) for pk, title, level in parents] - - def choices(self, cl): + "pk", "title", "level" + ) + self.lookup_choices = [ + ( + pk, + "{}{}".format( + "  " * level, shorten_string(title, max_length=25) + ), + ) + for pk, title, level in parents + ] + + def choices(self, changelist): yield { - 'selected': self.lookup_val is None, - 'query_string': cl.get_query_string({}, [self.lookup_kwarg]), - 'display': _('All') + "selected": self.lookup_val is None, + "query_string": changelist.get_query_string({}, [self.lookup_kwarg]), + "display": _("All"), } + # Pre Django 5 lookup_val would be a scalar, now it can do multiple + # selections and thus is a list. Deal with that. + lookup_vals = self.lookup_val + if lookup_vals is not None and django.VERSION < (5,): + lookup_vals = [lookup_vals] + for pk, title in self.lookup_choices: yield { - 'selected': pk == int(self.lookup_val or '0'), - 'query_string': cl.get_query_string({self.lookup_kwarg: pk}), - 'display': mark_safe(smart_text(title)) + "selected": lookup_vals is not None and str(pk) in lookup_vals, + "query_string": changelist.get_query_string({self.lookup_kwarg: pk}), + "display": mark_safe(smart_str(title)), } def title(self): - return _('Parent') + return _("Parent") class CategoryFieldListFilter(ChoicesFieldListFilter): @@ -67,45 +79,42 @@ class CategoryFieldListFilter(ChoicesFieldListFilter): my_model_field.category_filter = True """ - def __init__(self, f, request, params, model, model_admin, - field_path=None): - super(CategoryFieldListFilter, self).__init__( - f, request, params, model, model_admin, field_path) + def __init__(self, field, *args, **kwargs): + super().__init__(field, *args, **kwargs) # Restrict results to categories which are actually in use: - if DJANGO_VERSION < (1, 8): - related_model = f.related.parent_model - related_name = f.related.var_name - elif DJANGO_VERSION < (2, 0): - related_model = f.rel.to - related_name = f.related_query_name() - else: - related_model = f.remote_field.model - related_name = f.related_query_name() + related_model = field.remote_field.model + related_name = field.related_query_name() self.lookup_choices = sorted( - [ - (i.pk, '%s (%s)' % (i, i._related_count)) + ( + (i.pk, f"{i} ({i._related_count})") for i in related_model.objects.annotate( _related_count=Count(related_name) ).exclude(_related_count=0) - ], - key=lambda i: i[1], + ), + key=itemgetter(1), ) - def choices(self, cl): + def choices(self, changelist): yield { - 'selected': self.lookup_val is None, - 'query_string': cl.get_query_string({}, [self.lookup_kwarg]), - 'display': _('All') + "selected": self.lookup_val is None, + "query_string": changelist.get_query_string({}, [self.lookup_kwarg]), + "display": _("All"), } + # Pre Django 5 lookup_val would be a scalar, now it can do multiple + # selections and thus is a list. Deal with that. + lookup_vals = self.lookup_val + if lookup_vals is not None and django.VERSION < (5,): + lookup_vals = [lookup_vals] + for pk, title in self.lookup_choices: yield { - 'selected': pk == int(self.lookup_val or '0'), - 'query_string': cl.get_query_string({self.lookup_kwarg: pk}), - 'display': mark_safe(smart_text(title)) + "selected": lookup_vals is not None and str(pk) in lookup_vals, + "query_string": changelist.get_query_string({self.lookup_kwarg: pk}), + "display": mark_safe(smart_str(title)), } def title(self): - return _('Category') + return _("Category") diff --git a/feincms/admin/item_editor.py b/feincms/admin/item_editor.py index 3666599b5..8638c9089 100644 --- a/feincms/admin/item_editor.py +++ b/feincms/admin/item_editor.py @@ -1,8 +1,6 @@ # ------------------------------------------------------------------------ -# coding=utf-8 # ------------------------------------------------------------------------ -from __future__ import absolute_import, unicode_literals import copy import logging @@ -14,14 +12,13 @@ from django.contrib.auth import get_permission_codename from django.http import Http404 -from feincms import ensure_completely_loaded from feincms.extensions import ExtensionModelAdmin from feincms.signals import itemeditor_post_save_related # ------------------------------------------------------------------------ -FEINCMS_CONTENT_FIELDSET_NAME = 'FEINCMS_CONTENT' -FEINCMS_CONTENT_FIELDSET = (FEINCMS_CONTENT_FIELDSET_NAME, {'fields': ()}) +FEINCMS_CONTENT_FIELDSET_NAME = "FEINCMS_CONTENT" +FEINCMS_CONTENT_FIELDSET = (FEINCMS_CONTENT_FIELDSET_NAME, {"fields": ()}) logger = logging.getLogger(__name__) @@ -45,8 +42,8 @@ class FeinCMSInline(InlineModelAdmin): form = ItemEditorForm extra = 0 - fk_name = 'parent' - template = 'admin/feincms/content_inline.html' + fk_name = "parent" + template = "admin/feincms/content_inline.html" # ------------------------------------------------------------------------ @@ -60,14 +57,8 @@ class ItemEditor(ExtensionModelAdmin): the standard ``ModelAdmin`` class. """ - def __init__(self, model, admin_site): - ensure_completely_loaded() - - super(ItemEditor, self).__init__(model, admin_site) - def get_inline_instances(self, request, *args, **kwargs): - inline_instances = super(ItemEditor, self).get_inline_instances( - request, *args, **kwargs) + inline_instances = super().get_inline_instances(request, *args, **kwargs) self.append_feincms_inlines(inline_instances, request) return inline_instances @@ -80,13 +71,16 @@ def append_feincms_inlines(self, inline_instances, request): inline_instances.append(inline_instance) def can_add_content(self, request, content_type): - perm = '.'.join(( - content_type._meta.app_label, - get_permission_codename('add', content_type._meta))) + perm = ".".join( + ( + content_type._meta.app_label, + get_permission_codename("add", content_type._meta), + ) + ) return request.user.has_perm(perm) def get_feincms_inlines(self, model, request): - """ Generate genuine django inlines for registered content types. """ + """Generate genuine django inlines for registered content types.""" model._needs_content_types() inlines = [] @@ -94,53 +88,50 @@ def get_feincms_inlines(self, model, request): if not self.can_add_content(request, content_type): continue - attrs = { - '__module__': model.__module__, - 'model': content_type, - } + attrs = {"__module__": model.__module__, "model": content_type} - if hasattr(content_type, 'feincms_item_editor_inline'): + if hasattr(content_type, "feincms_item_editor_inline"): inline = content_type.feincms_item_editor_inline - attrs['form'] = inline.form + attrs["form"] = inline.form - if hasattr(content_type, 'feincms_item_editor_form'): + if hasattr(content_type, "feincms_item_editor_form"): warnings.warn( - 'feincms_item_editor_form on %s is ignored because ' - 'feincms_item_editor_inline is set too' % content_type, - RuntimeWarning) + "feincms_item_editor_form on %s is ignored because " + "feincms_item_editor_inline is set too" % content_type, + RuntimeWarning, + ) else: inline = FeinCMSInline - attrs['form'] = getattr( - content_type, 'feincms_item_editor_form', inline.form) + attrs["form"] = getattr( + content_type, "feincms_item_editor_form", inline.form + ) - name = '%sFeinCMSInline' % content_type.__name__ + name = "%sFeinCMSInline" % content_type.__name__ # TODO: We generate a new class every time. Is that really wanted? inline_class = type(str(name), (inline,), attrs) inlines.append(inline_class) return inlines def get_content_type_map(self, request): - """ Prepare mapping of content types to their prettified names. """ + """Prepare mapping of content types to their prettified names.""" content_types = [] for content_type in self.model._feincms_content_types: if self.model == content_type._feincms_content_class: content_name = content_type._meta.verbose_name - content_types.append( - (content_name, content_type.__name__.lower())) + content_types.append((content_name, content_type.__name__.lower())) return content_types def get_extra_context(self, request): - """ Return extra context parameters for add/change views. """ + """Return extra context parameters for add/change views.""" extra_context = { - 'request': request, - 'model': self.model, - 'available_templates': getattr( - self.model, '_feincms_templates', ()), - 'has_parent_attribute': hasattr(self.model, 'parent'), - 'content_types': self.get_content_type_map(request), - 'FEINCMS_CONTENT_FIELDSET_NAME': FEINCMS_CONTENT_FIELDSET_NAME, + "request": request, + "model": self.model, + "available_templates": getattr(self.model, "_feincms_templates", ()), + "has_parent_attribute": hasattr(self.model, "parent"), + "content_types": self.get_content_type_map(request), + "FEINCMS_CONTENT_FIELDSET_NAME": FEINCMS_CONTENT_FIELDSET_NAME, } for processor in self.model.feincms_item_editor_context_processors: @@ -151,9 +142,7 @@ def get_extra_context(self, request): def add_view(self, request, **kwargs): if not self.has_add_permission(request): logger.warning( - "Denied adding %s to \"%s\" (no add permission)", - self.model, - request.user + 'Denied adding %s to "%s" (no add permission)', self.model, request.user ) raise Http404 @@ -161,64 +150,60 @@ def add_view(self, request, **kwargs): # insert dummy object as 'original' so template code can grab defaults # for template, etc. - context['original'] = self.model() + context["original"] = self.model() # If there are errors in the form, we need to preserve the object's # template as it was set when the user attempted to save it, so that # the same regions appear on screen. - if request.method == 'POST' and \ - hasattr(self.model, '_feincms_templates'): - context['original'].template_key = request.POST['template_key'] + if request.method == "POST" and hasattr(self.model, "_feincms_templates"): + context["original"].template_key = request.POST["template_key"] context.update(self.get_extra_context(request)) - context.update(kwargs.get('extra_context', {})) - kwargs['extra_context'] = context - return super(ItemEditor, self).add_view(request, **kwargs) + context.update(kwargs.get("extra_context", {})) + kwargs["extra_context"] = context + return super().add_view(request, **kwargs) def render_change_form(self, request, context, **kwargs): - if kwargs.get('add'): - if request.method == 'GET' and 'adminform' in context: - if 'template_key' in context['adminform'].form.initial: - context['original'].template_key = ( - context['adminform'].form.initial['template_key']) + if kwargs.get("add"): + if request.method == "GET" and "adminform" in context: + if "template_key" in context["adminform"].form.initial: + context["original"].template_key = context[ + "adminform" + ].form.initial["template_key"] # ensure that initially-selected template in form is also # used to render the initial regions in the item editor - return super( - ItemEditor, self).render_change_form(request, context, **kwargs) + return super().render_change_form(request, context, **kwargs) def change_view(self, request, object_id, **kwargs): obj = self.get_object(request, unquote(object_id)) if not self.has_change_permission(request, obj): logger.warning( - "Denied editing %s to \"%s\" (no edit permission)", + 'Denied editing %s to "%s" (no edit permission)', self.model, - request.user + request.user, ) raise Http404 context = {} context.update(self.get_extra_context(request)) - context.update(kwargs.get('extra_context', {})) - kwargs['extra_context'] = context - return super(ItemEditor, self).change_view( - request, object_id, **kwargs) + context.update(kwargs.get("extra_context", {})) + kwargs["extra_context"] = context + return super().change_view(request, object_id, **kwargs) def save_related(self, request, form, formsets, change): - super(ItemEditor, self).save_related( - request, form, formsets, change) + super().save_related(request, form, formsets, change) itemeditor_post_save_related.send( - sender=form.instance.__class__, - instance=form.instance, - created=not change) + sender=form.instance.__class__, instance=form.instance, created=not change + ) @property def change_form_template(self): opts = self.model._meta return [ - 'admin/feincms/%s/%s/item_editor.html' % ( - opts.app_label, opts.object_name.lower()), - 'admin/feincms/%s/item_editor.html' % opts.app_label, - 'admin/feincms/item_editor.html', + "admin/feincms/%s/%s/item_editor.html" + % (opts.app_label, opts.object_name.lower()), + "admin/feincms/%s/item_editor.html" % opts.app_label, + "admin/feincms/item_editor.html", ] def get_fieldsets(self, request, obj=None): @@ -227,9 +212,7 @@ def get_fieldsets(self, request, obj=None): Is it reasonable to assume this should always be included? """ - fieldsets = copy.deepcopy( - super(ItemEditor, self).get_fieldsets(request, obj) - ) + fieldsets = copy.deepcopy(super().get_fieldsets(request, obj)) names = [f[0] for f in fieldsets] if FEINCMS_CONTENT_FIELDSET_NAME not in names: @@ -244,16 +227,20 @@ def get_fieldsets(self, request, obj=None): recover_form_template = "admin/feincms/recover_form.html" # For Reversion < v2.0.0 - def render_revision_form(self, request, obj, version, context, - revert=False, recover=False): + def render_revision_form( + self, request, obj, version, context, revert=False, recover=False + ): context.update(self.get_extra_context(request)) - return super(ItemEditor, self).render_revision_form( - request, obj, version, context, revert, recover) + return super().render_revision_form( + request, obj, version, context, revert, recover + ) # For Reversion >= v2.0.0 - def _reversion_revisionform_view(self, request, version, template_name, - extra_context=None): + def _reversion_revisionform_view( + self, request, version, template_name, extra_context=None + ): context = extra_context or {} context.update(self.get_extra_context(request)) - return super(ItemEditor, self)._reversion_revisionform_view( - request, version, template_name, context) + return super()._reversion_revisionform_view( + request, version, template_name, context + ) diff --git a/feincms/admin/tree_editor.py b/feincms/admin/tree_editor.py index bff48fa40..07aa1d0ab 100644 --- a/feincms/admin/tree_editor.py +++ b/feincms/admin/tree_editor.py @@ -1,26 +1,27 @@ # ------------------------------------------------------------------------ -# coding=utf-8 # ------------------------------------------------------------------------ -from __future__ import absolute_import, unicode_literals -from functools import reduce import json import logging +from functools import reduce -from django.contrib.admin.views import main from django.contrib.admin.actions import delete_selected +from django.contrib.admin.views import main from django.contrib.auth import get_permission_codename -from django.contrib.staticfiles.templatetags.staticfiles import static from django.db.models import Q from django.http import ( - HttpResponse, HttpResponseBadRequest, - HttpResponseForbidden, HttpResponseNotFound, HttpResponseServerError) + HttpResponse, + HttpResponseBadRequest, + HttpResponseForbidden, + HttpResponseNotFound, + HttpResponseServerError, +) +from django.templatetags.static import static +from django.utils.encoding import force_str from django.utils.html import escape from django.utils.safestring import mark_safe -from django.utils.translation import ugettext_lazy as _, ugettext -from django.utils.encoding import force_text - +from django.utils.translation import gettext, gettext_lazy as _ from mptt.exceptions import InvalidMove from mptt.forms import MPTTAdminForm @@ -38,15 +39,14 @@ def django_boolean_icon(field_val, alt_text=None, title=None): """ # Origin: contrib/admin/templatetags/admin_list.py - BOOLEAN_MAPPING = {True: 'yes', False: 'no', None: 'unknown'} + BOOLEAN_MAPPING = {True: "yes", False: "no", None: "unknown"} alt_text = alt_text or BOOLEAN_MAPPING[field_val] if title is not None: title = 'title="%s" ' % title else: - title = '' - icon_url = static('feincms/img/icon-%s.gif' % BOOLEAN_MAPPING[field_val]) - return mark_safe( - '%s' % (icon_url, alt_text, title)) + title = "" + icon_url = static("feincms/img/icon-%s.gif" % BOOLEAN_MAPPING[field_val]) + return mark_safe(f'{alt_text}') def _build_tree_structure(queryset): @@ -64,23 +64,16 @@ def _build_tree_structure(queryset): all_nodes = {} mptt_opts = queryset.model._mptt_meta - items = queryset.order_by( - mptt_opts.tree_id_attr, - mptt_opts.left_attr, - ).values_list( - "pk", - "%s_id" % mptt_opts.parent_attr, + items = queryset.order_by(mptt_opts.tree_id_attr, mptt_opts.left_attr).values_list( + "pk", "%s_id" % mptt_opts.parent_attr ) for p_id, parent_id in items: - all_nodes.setdefault( - str(parent_id) if parent_id else 0, - [], - ).append(p_id) + all_nodes.setdefault(str(parent_id) if parent_id else 0, []).append(p_id) return all_nodes # ------------------------------------------------------------------------ -def ajax_editable_boolean_cell(item, attr, text='', override=None): +def ajax_editable_boolean_cell(item, attr, text="", override=None): """ Generate a html snippet for showing a boolean value on the admin page. Item is an object, attr is the attribute name we should display. Text @@ -95,7 +88,7 @@ def ajax_editable_boolean_cell(item, attr, text='', override=None): (useful for "disabled and you can't change it" situations). """ if text: - text = ' (%s)' % text + text = " (%s)" % text if override is not None: a = [django_boolean_icon(override, text), text] @@ -103,15 +96,13 @@ def ajax_editable_boolean_cell(item, attr, text='', override=None): value = getattr(item, attr) a = [ '' % ( - item.pk, - attr, - 'checked="checked"' if value else '', - )] + ' data-inplace-attribute="%s" %s>' + % (item.pk, attr, 'checked="checked"' if value else "") + ] a.insert(0, '
' % (attr, item.pk)) - a.append('
') - return mark_safe(''.join(a)) + a.append("") + return mark_safe("".join(a)) # ------------------------------------------------------------------------ @@ -127,8 +118,10 @@ class MyTreeEditor(TreeEditor): active_toggle = ajax_editable_boolean('active', _('is active')) """ + def _fn(self, item): return ajax_editable_boolean_cell(item, attr) + _fn.short_description = short_description _fn.editable_boolean_field = attr return _fn @@ -143,12 +136,15 @@ class ChangeList(main.ChangeList): def __init__(self, request, *args, **kwargs): self.user = request.user - super(ChangeList, self).__init__(request, *args, **kwargs) + super().__init__(request, *args, **kwargs) def get_queryset(self, *args, **kwargs): mptt_opts = self.model._mptt_meta - qs = super(ChangeList, self).get_queryset(*args, **kwargs).\ - order_by(mptt_opts.tree_id_attr, mptt_opts.left_attr) + qs = ( + super() + .get_queryset(*args, **kwargs) + .order_by(mptt_opts.tree_id_attr, mptt_opts.left_attr) + ) # Force has_filters, so that the expand/collapse in sidebar is visible self.has_filters = True return qs @@ -157,14 +153,15 @@ def get_results(self, request): mptt_opts = self.model._mptt_meta if settings.FEINCMS_TREE_EDITOR_INCLUDE_ANCESTORS: clauses = [ - Q(**{ - mptt_opts.tree_id_attr: tree_id, - mptt_opts.left_attr + '__lte': lft, - mptt_opts.right_attr + '__gte': rght, - }) for lft, rght, tree_id in self.queryset.values_list( - mptt_opts.left_attr, - mptt_opts.right_attr, - mptt_opts.tree_id_attr, + Q( + **{ + mptt_opts.tree_id_attr: tree_id, + mptt_opts.left_attr + "__lte": lft, + mptt_opts.right_attr + "__gte": rght, + } + ) + for lft, rght, tree_id in self.queryset.values_list( + mptt_opts.left_attr, mptt_opts.right_attr, mptt_opts.tree_id_attr ) ] # We could optimise a bit here by explicitely filtering out @@ -176,21 +173,25 @@ def get_results(self, request): # Note: Django ORM is smart enough to drop additional # clauses if the initial query set is unfiltered. This # is good. - self.queryset |= self.model._default_manager.filter( - reduce(lambda p, q: p | q, clauses), - ) + self.queryset = self.queryset.union( + self.model._default_manager.filter( + reduce(lambda p, q: p | q, clauses) + ) + ).order_by(mptt_opts.tree_id_attr, mptt_opts.left_attr) - super(ChangeList, self).get_results(request) + super().get_results(request) # Pre-process permissions because we still have the request here, # which is not passed in later stages in the tree editor for item in self.result_list: item.feincms_changeable = self.model_admin.has_change_permission( - request, item) + request, item + ) item.feincms_addable = ( - item.feincms_changeable and - self.model_admin.has_add_permission(request, item)) + item.feincms_changeable + and self.model_admin.has_add_permission(request, item) + ) # ------------------------------------------------------------------------ @@ -211,33 +212,36 @@ class TreeEditor(ExtensionModelAdmin): list_per_page = 999999999 def __init__(self, *args, **kwargs): - super(TreeEditor, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.list_display = list(self.list_display) - if 'indented_short_title' not in self.list_display: - if self.list_display[0] == 'action_checkbox': - self.list_display[1] = 'indented_short_title' + if "indented_short_title" not in self.list_display: + if self.list_display[0] == "action_checkbox": + self.list_display[1] = "indented_short_title" else: - self.list_display[0] = 'indented_short_title' - self.list_display_links = ('indented_short_title',) + self.list_display[0] = "indented_short_title" + self.list_display_links = ("indented_short_title",) opts = self.model._meta self.change_list_template = [ - 'admin/feincms/%s/%s/tree_editor.html' % ( - opts.app_label, opts.object_name.lower()), - 'admin/feincms/%s/tree_editor.html' % opts.app_label, - 'admin/feincms/tree_editor.html', + "admin/feincms/%s/%s/tree_editor.html" + % (opts.app_label, opts.object_name.lower()), + "admin/feincms/%s/tree_editor.html" % opts.app_label, + "admin/feincms/tree_editor.html", ] - self.object_change_permission =\ - opts.app_label + '.' + get_permission_codename('change', opts) - self.object_add_permission =\ - opts.app_label + '.' + get_permission_codename('add', opts) - self.object_delete_permission =\ - opts.app_label + '.' + get_permission_codename('delete', opts) + self.object_change_permission = ( + opts.app_label + "." + get_permission_codename("change", opts) + ) + self.object_add_permission = ( + opts.app_label + "." + get_permission_codename("add", opts) + ) + self.object_delete_permission = ( + opts.app_label + "." + get_permission_codename("delete", opts) + ) def changeable(self, item): - return getattr(item, 'feincms_changeable', True) + return getattr(item, "feincms_changeable", True) def indented_short_title(self, item): """ @@ -245,7 +249,7 @@ def indented_short_title(self, item): the object's depth in the hierarchy. """ mptt_opts = item._mptt_meta - r = '' + r = "" try: url = item.get_absolute_url() except (AttributeError,): @@ -254,31 +258,35 @@ def indented_short_title(self, item): if url: r = ( '') % (url, item.pk) + ' value="%s" id="_refkey_%d" />' + ) % (url, item.pk) - changeable_class = '' + changeable_class = "" if not self.changeable(item): - changeable_class = ' tree-item-not-editable' - tree_root_class = '' + changeable_class = " tree-item-not-editable" + tree_root_class = "" if not item.parent_id: - tree_root_class = ' tree-root' + tree_root_class = " tree-root" r += ( '  ') % ( + ' style="width: %dpx;">  ' + ) % ( item.pk, changeable_class, tree_root_class, - 14 + getattr(item, mptt_opts.level_attr) * 18) + 14 + getattr(item, mptt_opts.level_attr) * 18, + ) -# r += '' - if hasattr(item, 'short_title') and callable(item.short_title): + # r += '' + if hasattr(item, "short_title") and callable(item.short_title): r += escape(item.short_title()) else: - r += escape('%s' % item) -# r += '' + r += escape("%s" % item) + # r += '' return mark_safe(r) - indented_short_title.short_description = _('title') + + indented_short_title.short_description = _("title") def _collect_editable_booleans(self): """ @@ -286,7 +294,7 @@ def _collect_editable_booleans(self): want the user to be able to edit arbitrary fields by crafting an AJAX request by hand. """ - if hasattr(self, '_ajax_editable_booleans'): + if hasattr(self, "_ajax_editable_booleans"): return self._ajax_editable_booleans = {} @@ -299,14 +307,17 @@ def _collect_editable_booleans(self): except (AttributeError, TypeError): continue - attr = getattr(item, 'editable_boolean_field', None) + attr = getattr(item, "editable_boolean_field", None) if attr: - if hasattr(item, 'editable_boolean_result'): + if hasattr(item, "editable_boolean_result"): result_func = item.editable_boolean_result else: + def _fn(attr): return lambda self, instance: [ - ajax_editable_boolean_cell(instance, attr)] + ajax_editable_boolean_cell(instance, attr) + ] + result_func = _fn(attr) self._ajax_editable_booleans[attr] = result_func @@ -315,17 +326,22 @@ def _toggle_boolean(self, request): Handle an AJAX toggle_boolean request """ try: - item_id = int(request.POST.get('item_id', None)) - attr = str(request.POST.get('attr', None)) + item_id = int(request.POST.get("item_id", None)) + attr = str(request.POST.get("attr", None)) except Exception: return HttpResponseBadRequest("Malformed request") if not request.user.is_staff: logger.warning( - "Denied AJAX request by non-staff \"%s\" to toggle boolean" - " %s for object #%s", request.user, attr, item_id) + 'Denied AJAX request by non-staff "%s" to toggle boolean' + " %s for object #%s", + request.user, + attr, + item_id, + ) return HttpResponseForbidden( - _("You do not have permission to modify this object")) + _("You do not have permission to modify this object") + ) self._collect_editable_booleans() @@ -339,15 +355,24 @@ def _toggle_boolean(self, request): if not self.has_change_permission(request, obj=obj): logger.warning( - "Denied AJAX request by \"%s\" to toggle boolean %s for" - " object %s", request.user, attr, item_id) + 'Denied AJAX request by "%s" to toggle boolean %s for object %s', + request.user, + attr, + item_id, + ) return HttpResponseForbidden( - _("You do not have permission to modify this object")) + _("You do not have permission to modify this object") + ) new_state = not getattr(obj, attr) logger.info( - "Toggle %s on #%d %s to %s by \"%s\"", - attr, obj.pk, obj, "on" if new_state else "off", request.user) + 'Toggle %s on #%d %s to %s by "%s"', + attr, + obj.pk, + obj, + "on" if new_state else "off", + request.user, + ) try: before_data = self._ajax_editable_booleans[attr](self, obj) @@ -359,17 +384,16 @@ def _toggle_boolean(self, request): data = self._ajax_editable_booleans[attr](self, obj) except Exception: - logger.exception( - "Unhandled exception while toggling %s on %s", attr, obj) - return HttpResponseServerError( - "Unable to toggle %s on %s" % (attr, obj)) + logger.exception("Unhandled exception while toggling %s on %s", attr, obj) + return HttpResponseServerError(f"Unable to toggle {attr} on {obj}") # Weed out unchanged cells to keep the updates small. This assumes # that the order a possible get_descendents() returns does not change # before and after toggling this attribute. Unlikely, but still... return HttpResponse( json.dumps([b for a, b in zip(before_data, data) if a != b]), - content_type="application/json") + content_type="application/json", + ) def get_changelist(self, request, **kwargs): return ChangeList @@ -380,30 +404,43 @@ def changelist_view(self, request, extra_context=None, *args, **kwargs): change list/actions page. """ - if 'actions_column' not in self.list_display: - self.list_display.append('actions_column') + if "actions_column" not in self.list_display: + self.list_display.append("actions_column") # handle common AJAX requests - if request.is_ajax(): - cmd = request.POST.get('__cmd') - if cmd == 'toggle_boolean': + if "__cmd" in request.POST: + cmd = request.POST.get("__cmd") + if cmd == "toggle_boolean": return self._toggle_boolean(request) - elif cmd == 'move_node': + elif cmd == "move_node": return self._move_node(request) - return HttpResponseBadRequest('Oops. AJAX request not understood.') + return HttpResponseBadRequest("Oops. AJAX request not understood.") extra_context = extra_context or {} - extra_context['tree_structure'] = mark_safe( - json.dumps(_build_tree_structure(self.get_queryset(request)))) - extra_context['node_levels'] = mark_safe(json.dumps( - dict(self.get_queryset(request).order_by().values_list( - 'pk', self.model._mptt_meta.level_attr - )) - )) - return super(TreeEditor, self).changelist_view( - request, extra_context, *args, **kwargs) + extra_context.update( + { + "tree_structure": mark_safe( + json.dumps( + obj=_build_tree_structure(self.get_queryset(request)), + separators=(",", ":"), + ) + ), + "node_levels": mark_safe( + json.dumps( + dict( + self.get_queryset(request) + .order_by() + .values_list("pk", self.model._mptt_meta.level_attr) + ), + separators=(",", ":"), + ) + ), + } + ) + + return super().changelist_view(request, extra_context, *args, **kwargs) def has_add_permission(self, request, obj=None): """ @@ -416,7 +453,7 @@ def has_add_permission(self, request, obj=None): else: r = request.user.has_perm(perm) - return r and super(TreeEditor, self).has_add_permission(request) + return r and super().has_add_permission(request) def has_change_permission(self, request, obj=None): """ @@ -429,8 +466,7 @@ def has_change_permission(self, request, obj=None): else: r = request.user.has_perm(perm) - return r and super(TreeEditor, self).has_change_permission( - request, obj) + return r and super().has_change_permission(request, obj) def has_delete_permission(self, request, obj=None): """ @@ -443,30 +479,29 @@ def has_delete_permission(self, request, obj=None): else: r = request.user.has_perm(perm) - return r and super(TreeEditor, self).has_delete_permission( - request, obj) + return r and super().has_delete_permission(request, obj) def _move_node(self, request): - if hasattr(self.model.objects, 'move_node'): + if hasattr(self.model.objects, "move_node"): tree_manager = self.model.objects else: tree_manager = self.model._tree_manager queryset = self.get_queryset(request) - cut_item = queryset.get(pk=request.POST.get('cut_item')) - pasted_on = queryset.get(pk=request.POST.get('pasted_on')) - position = request.POST.get('position') + cut_item = queryset.get(pk=request.POST.get("cut_item")) + pasted_on = queryset.get(pk=request.POST.get("pasted_on")) + position = request.POST.get("position") if not self.has_change_permission(request, cut_item): - self.message_user(request, _('No permission')) - return HttpResponse('FAIL') + self.message_user(request, _("No permission")) + return HttpResponse("FAIL") - if position in ('last-child', 'left', 'right'): + if position in ("last-child", "left", "right"): try: tree_manager.move_node(cut_item, pasted_on, position) except InvalidMove as e: - self.message_user(request, '%s' % e) - return HttpResponse('FAIL') + self.message_user(request, "%s" % e) + return HttpResponse("FAIL") # Ensure that model save methods have been run (required to # update Page._cached_url values, might also be helpful for other @@ -475,12 +510,12 @@ def _move_node(self, request): item.save() self.message_user( - request, - ugettext('%s has been moved to a new position.') % cut_item) - return HttpResponse('OK') + request, gettext("%s has been moved to a new position.") % cut_item + ) + return HttpResponse("OK") - self.message_user(request, _('Did not understand moving instruction.')) - return HttpResponse('FAIL') + self.message_user(request, _("Did not understand moving instruction.")) + return HttpResponse("FAIL") def _actions_column(self, instance): if self.changeable(instance): @@ -488,8 +523,9 @@ def _actions_column(self, instance): return [] def actions_column(self, instance): - return mark_safe(' '.join(self._actions_column(instance))) - actions_column.short_description = _('actions') + return mark_safe(" ".join(self._actions_column(instance))) + + actions_column.short_description = _("actions") def delete_selected_tree(self, modeladmin, request, queryset): """ @@ -498,7 +534,7 @@ def delete_selected_tree(self, modeladmin, request, queryset): trigger the post_delete hooks.) """ # If this is True, the confirmation page has been displayed - if request.POST.get('post'): + if request.POST.get("post"): n = 0 # TODO: The disable_mptt_updates / rebuild is a work around # for what seems to be a mptt problem when deleting items @@ -508,17 +544,19 @@ def delete_selected_tree(self, modeladmin, request, queryset): if self.has_delete_permission(request, obj): obj.delete() n += 1 - obj_display = force_text(obj) + obj_display = force_str(obj) self.log_deletion(request, obj, obj_display) else: logger.warning( - "Denied delete request by \"%s\" for object #%s", - request.user, obj.id) + 'Denied delete request by "%s" for object #%s', + request.user, + obj.id, + ) if n > 0: queryset.model.objects.rebuild() self.message_user( - request, - _("Successfully deleted %(count)d items.") % {"count": n}) + request, _("Successfully deleted %(count)d items.") % {"count": n} + ) # Return None to display the change list page again return None else: @@ -526,10 +564,11 @@ def delete_selected_tree(self, modeladmin, request, queryset): return delete_selected(self, request, queryset) def get_actions(self, request): - actions = super(TreeEditor, self).get_actions(request) - if 'delete_selected' in actions: - actions['delete_selected'] = ( + actions = super().get_actions(request) + if "delete_selected" in actions: + actions["delete_selected"] = ( self.delete_selected_tree, - 'delete_selected', - _("Delete selected %(verbose_name_plural)s")) + "delete_selected", + _("Delete selected %(verbose_name_plural)s"), + ) return actions diff --git a/feincms/apps.py b/feincms/apps.py index fa5e7ac8c..8e5394646 100644 --- a/feincms/apps.py +++ b/feincms/apps.py @@ -1,3 +1,17 @@ -# flake8: noqa +def __getattr__(key): + # Work around Django 3.2's autoloading of *.apps modules (AppConfig + # autodiscovery) + if key in { + "ApplicationContent", + "app_reverse", + "app_reverse_lazy", + "permalink", + "UnpackTemplateResponse", + "standalone", + "unpack", + }: + from feincms.content.application import models -from feincms.content.application.models import * + return getattr(models, key) + + raise AttributeError("Unknown attribute '%s'" % key) diff --git a/feincms/content/application/models.py b/feincms/content/application/models.py index d872b1731..46d5b44c9 100644 --- a/feincms/content/application/models.py +++ b/feincms/content/application/models.py @@ -1,30 +1,26 @@ -from __future__ import absolute_import - +import warnings from collections import OrderedDict from email.utils import parsedate from functools import partial, wraps from time import mktime -import warnings from django.conf import settings from django.core.cache import cache from django.db import models from django.http import HttpResponse from django.template.response import TemplateResponse +from django.urls import ( + NoReverseMatch, + Resolver404, + get_script_prefix, + resolve, + reverse, + set_script_prefix, +) from django.utils.functional import lazy from django.utils.http import http_date from django.utils.safestring import mark_safe -from django.utils.translation import get_language, ugettext_lazy as _ -try: - from django.urls import ( - NoReverseMatch, reverse, get_script_prefix, set_script_prefix, - Resolver404, resolve, - ) -except ImportError: - from django.core.urlresolvers import ( - NoReverseMatch, reverse, get_script_prefix, set_script_prefix, - Resolver404, resolve, - ) +from django.utils.translation import get_language, gettext_lazy as _ from feincms.admin.item_editor import ItemEditorForm from feincms.contrib.fields import JSONField @@ -36,9 +32,13 @@ __all__ = ( - 'ApplicationContent', - 'app_reverse', 'app_reverse_lazy', 'permalink', - 'UnpackTemplateResponse', 'standalone', 'unpack', + "ApplicationContent", + "app_reverse", + "app_reverse_lazy", + "permalink", + "UnpackTemplateResponse", + "standalone", + "unpack", ) @@ -47,6 +47,7 @@ class UnpackTemplateResponse(TemplateResponse): Completely the same as marking applicationcontent-contained views with the ``feincms.views.decorators.unpack`` decorator. """ + _feincms_unpack = True @@ -62,6 +63,7 @@ def inner(request, *args, **kwargs): if isinstance(response, HttpResponse): response.standalone = True return response + return wraps(view_func)(inner) @@ -76,19 +78,20 @@ def inner(request, *args, **kwargs): if isinstance(response, TemplateResponse): response._feincms_unpack = True return response + return wraps(view_func)(inner) def cycle_app_reverse_cache(*args, **kwargs): warnings.warn( - 'cycle_app_reverse_cache does nothing and will be removed in' - ' a future version of FeinCMS.', - DeprecationWarning, stacklevel=2, + "cycle_app_reverse_cache does nothing and will be removed in" + " a future version of FeinCMS.", + DeprecationWarning, + stacklevel=2, ) -def app_reverse(viewname, urlconf=None, args=None, kwargs=None, - *vargs, **vkwargs): +def app_reverse(viewname, urlconf=None, args=None, kwargs=None, *vargs, **vkwargs): """ Reverse URLs from application contents @@ -109,9 +112,9 @@ def app_reverse(viewname, urlconf=None, args=None, kwargs=None, # First parameter might be a request instead of an urlconf path, so # we'll try to be helpful and extract the current urlconf from it - extra_context = getattr(urlconf, '_feincms_extra_context', {}) - appconfig = extra_context.get('app_config', {}) - urlconf = appconfig.get('urlconf_path', urlconf) + extra_context = getattr(urlconf, "_feincms_extra_context", {}) + appconfig = extra_context.get("app_config", {}) + urlconf = appconfig.get("urlconf_path", urlconf) appcontent_class = ApplicationContent._feincms_content_models[0] cache_key = appcontent_class.app_reverse_cache_key(urlconf) @@ -124,10 +127,10 @@ def app_reverse(viewname, urlconf=None, args=None, kwargs=None, if urlconf in appcontent_class.ALL_APPS_CONFIG: # We have an overridden URLconf app_config = appcontent_class.ALL_APPS_CONFIG[urlconf] - urlconf = app_config['config'].get('urls', urlconf) + urlconf = app_config["config"].get("urls", urlconf) prefix = content.parent.get_absolute_url() - prefix += '/' if prefix[-1] != '/' else '' + prefix += "/" if prefix[-1] != "/" else "" url_prefix = (urlconf, prefix) cache.set(cache_key, url_prefix, timeout=APP_REVERSE_CACHE_TIMEOUT) @@ -139,11 +142,8 @@ def app_reverse(viewname, urlconf=None, args=None, kwargs=None, try: set_script_prefix(url_prefix[1]) return reverse( - viewname, - url_prefix[0], - args=args, - kwargs=kwargs, - *vargs, **vkwargs) + viewname, url_prefix[0], args=args, kwargs=kwargs, *vargs, **vkwargs + ) finally: set_script_prefix(prefix) @@ -167,8 +167,10 @@ class MyModel(models.Model): def get_absolute_url(self): return ('myapp.urls', 'model_detail', (), {'slug': self.slug}) """ + def inner(*args, **kwargs): return app_reverse(*func(*args, **kwargs)) + return wraps(func)(inner) @@ -182,8 +184,8 @@ class ApplicationContent(models.Model): class Meta: abstract = True - verbose_name = _('application content') - verbose_name_plural = _('application contents') + verbose_name = _("application content") + verbose_name_plural = _("application contents") @classmethod def initialize_type(cls, APPLICATIONS): @@ -192,7 +194,8 @@ def initialize_type(cls, APPLICATIONS): raise ValueError( "APPLICATIONS must be provided with tuples containing at" " least two parameters (urls, name) and an optional extra" - " config dict") + " config dict" + ) urls, name = i[0:2] @@ -202,20 +205,20 @@ def initialize_type(cls, APPLICATIONS): if not isinstance(app_conf, dict): raise ValueError( "The third parameter of an APPLICATIONS entry must be" - " a dict or the name of one!") + " a dict or the name of one!" + ) else: app_conf = {} - cls.ALL_APPS_CONFIG[urls] = { - "urls": urls, - "name": name, - "config": app_conf - } + cls.ALL_APPS_CONFIG[urls] = {"urls": urls, "name": name, "config": app_conf} cls.add_to_class( - 'urlconf_path', - models.CharField(_('application'), max_length=100, choices=[ - (c['urls'], c['name']) for c in cls.ALL_APPS_CONFIG.values()]) + "urlconf_path", + models.CharField( + _("application"), + max_length=100, + choices=[(c["urls"], c["name"]) for c in cls.ALL_APPS_CONFIG.values()], + ), ) class ApplicationContentItemEditorForm(ItemEditorForm): @@ -223,8 +226,7 @@ class ApplicationContentItemEditorForm(ItemEditorForm): custom_fields = {} def __init__(self, *args, **kwargs): - super(ApplicationContentItemEditorForm, self).__init__( - *args, **kwargs) + super().__init__(*args, **kwargs) instance = kwargs.get("instance", None) @@ -233,20 +235,20 @@ def __init__(self, *args, **kwargs): # TODO use urlconf_path from POST if set # urlconf_path = request.POST.get('...urlconf_path', # instance.urlconf_path) - self.app_config = cls.ALL_APPS_CONFIG[ - instance.urlconf_path]['config'] + self.app_config = cls.ALL_APPS_CONFIG[instance.urlconf_path][ + "config" + ] except KeyError: self.app_config = {} self.custom_fields = {} - admin_fields = self.app_config.get('admin_fields', {}) + admin_fields = self.app_config.get("admin_fields", {}) if isinstance(admin_fields, dict): self.custom_fields.update(admin_fields) else: get_fields = get_object(admin_fields) - self.custom_fields.update( - get_fields(self, *args, **kwargs)) + self.custom_fields.update(get_fields(self, *args, **kwargs)) params = self.instance.parameters for k, v in self.custom_fields.items(): @@ -261,12 +263,13 @@ def save(self, commit=True, *args, **kwargs): # get the model so we can set .parameters to the values of our # custom fields before calling save(commit=True) - m = super(ApplicationContentItemEditorForm, self).save( - commit=False, *args, **kwargs) + m = super().save(commit=False, *args, **kwargs) - m.parameters = dict( - (k, self.cleaned_data[k]) - for k in self.custom_fields if k in self.cleaned_data) + m.parameters = { + k: self.cleaned_data[k] + for k in self.custom_fields + if k in self.cleaned_data + } if commit: m.save(**kwargs) @@ -278,9 +281,10 @@ def save(self, commit=True, *args, **kwargs): cls.feincms_item_editor_form = ApplicationContentItemEditorForm def __init__(self, *args, **kwargs): - super(ApplicationContent, self).__init__(*args, **kwargs) - self.app_config = self.ALL_APPS_CONFIG.get( - self.urlconf_path, {}).get('config', {}) + super().__init__(*args, **kwargs) + self.app_config = self.ALL_APPS_CONFIG.get(self.urlconf_path, {}).get( + "config", {} + ) def process(self, request, **kw): page_url = self.parent.get_absolute_url() @@ -290,21 +294,20 @@ def process(self, request, **kw): if "path_mapper" in self.app_config: path_mapper = get_object(self.app_config["path_mapper"]) path, page_url = path_mapper( - request.path, - page_url, - appcontent_parameters=self.parameters + request.path, page_url, appcontent_parameters=self.parameters ) else: - path = request._feincms_extra_context['extra_path'] + path = request._feincms_extra_context["extra_path"] # Resolve the module holding the application urls. - urlconf_path = self.app_config.get('urls', self.urlconf_path) + urlconf_path = self.app_config.get("urls", self.urlconf_path) try: fn, args, kwargs = resolve(path, urlconf_path) except (ValueError, Resolver404): - raise Resolver404(str('Not found (resolving %r in %r failed)') % ( - path, urlconf_path)) + raise Resolver404( + f"Not found (resolving {path!r} in {urlconf_path!r} failed)" + ) # Variables from the ApplicationContent parameters are added to request # so we can expose them to our templates via the appcontent_parameters @@ -312,19 +315,14 @@ def process(self, request, **kw): request._feincms_extra_context.update(self.parameters) # Save the application configuration for reuse elsewhere - request._feincms_extra_context.update({ - 'app_config': dict( - self.app_config, - urlconf_path=self.urlconf_path, - ), - }) + request._feincms_extra_context.update( + {"app_config": dict(self.app_config, urlconf_path=self.urlconf_path)} + ) view_wrapper = self.app_config.get("view_wrapper", None) if view_wrapper: fn = partial( - get_object(view_wrapper), - view=fn, - appcontent_parameters=self.parameters + get_object(view_wrapper), view=fn, appcontent_parameters=self.parameters ) output = fn(request, *args, **kwargs) @@ -333,34 +331,36 @@ def process(self, request, **kw): if self.send_directly(request, output): return output elif output.status_code == 200: - - if self.unpack(request, output) and 'view' in kw: + if self.unpack(request, output) and "view" in kw: # Handling of @unpack and UnpackTemplateResponse - kw['view'].template_name = output.template_name - kw['view'].request._feincms_extra_context.update( - output.context_data) + kw["view"].template_name = output.template_name + kw["view"].request._feincms_extra_context.update( + output.context_data + ) else: # If the response supports deferred rendering, render the # response right now. We do not handle template response # middleware. - if hasattr(output, 'render') and callable(output.render): + if hasattr(output, "render") and callable(output.render): output.render() - self.rendered_result = mark_safe( - output.content.decode('utf-8')) + self.rendered_result = mark_safe(output.content.decode("utf-8")) self.rendered_headers = {} # Copy relevant headers for later perusal - for h in ('Cache-Control', 'Last-Modified', 'Expires'): + for h in ("Cache-Control", "Last-Modified", "Expires"): if h in output: - self.rendered_headers.setdefault( - h, []).append(output[h]) + self.rendered_headers.setdefault(h, []).append(output[h]) - elif isinstance(output, tuple) and 'view' in kw: - kw['view'].template_name = output[0] - kw['view'].request._feincms_extra_context.update(output[1]) + application_cookies = output.cookies + if application_cookies: + self.rendered_headers["X-Feincms-Cookie"] = application_cookies + + elif isinstance(output, tuple) and "view" in kw: + kw["view"].template_name = output[0] + kw["view"].request._feincms_extra_context.update(output[1]) else: self.rendered_result = mark_safe(output) @@ -368,25 +368,31 @@ def process(self, request, **kw): return True # successful def send_directly(self, request, response): - mimetype = response.get('Content-Type', 'text/plain') - if ';' in mimetype: - mimetype = mimetype.split(';')[0] + mimetype = response.get("Content-Type", "text/plain") + if ";" in mimetype: + mimetype = mimetype.split(";")[0] mimetype = mimetype.strip() + is_ajax = ( + request.is_ajax() + if hasattr(request, "is_ajax") + else request.headers.get("x-requested-with") == "XMLHttpRequest" + ) return ( - response.status_code != 200 or - request.is_ajax() or - getattr(response, 'standalone', False) or - mimetype not in ('text/html', 'text/plain')) + response.status_code != 200 + or is_ajax + or getattr(response, "standalone", False) + or mimetype not in ("text/html", "text/plain") + ) def unpack(self, request, response): - return getattr(response, '_feincms_unpack', False) + return getattr(response, "_feincms_unpack", False) def render(self, **kwargs): - return getattr(self, 'rendered_result', '') + return getattr(self, "rendered_result", "") def finalize(self, request, response): - headers = getattr(self, 'rendered_headers', None) + headers = getattr(self, "rendered_headers", None) if headers: self._update_response_headers(request, response, headers) @@ -399,29 +405,45 @@ def _update_response_headers(self, request, response, headers): # Ideally, for the Cache-Control header, we'd want to do some # intelligent combining, but that's hard. Let's just collect and unique # them and let the client worry about that. - cc_headers = set(('must-revalidate',)) - for x in (cc.split(",") for cc in headers.get('Cache-Control', ())): - cc_headers |= set((s.strip() for s in x)) + cc_headers = {"must-revalidate"} + for x in (cc.split(",") for cc in headers.get("Cache-Control", ())): + cc_headers |= {s.strip() for s in x} if len(cc_headers): - response['Cache-Control'] = ", ".join(cc_headers) - else: # Default value - response['Cache-Control'] = 'no-cache, must-revalidate' + response["Cache-Control"] = ", ".join(cc_headers) + else: # Default value + response["Cache-Control"] = "no-cache, must-revalidate" # Check all Last-Modified headers, choose the latest one - lm_list = [parsedate(x) for x in headers.get('Last-Modified', ())] + lm_list = [parsedate(x) for x in headers.get("Last-Modified", ())] if len(lm_list) > 0: - response['Last-Modified'] = http_date(mktime(max(lm_list))) + response["Last-Modified"] = http_date(mktime(max(lm_list))) # Check all Expires headers, choose the earliest one - lm_list = [parsedate(x) for x in headers.get('Expires', ())] + lm_list = [parsedate(x) for x in headers.get("Expires", ())] if len(lm_list) > 0: - response['Expires'] = http_date(mktime(min(lm_list))) + response["Expires"] = http_date(mktime(min(lm_list))) + + # Add all cookies + cookies = headers.get("X-Feincms-Cookie", None) + if cookies: + for kookie, val in cookies.items(): + response.set_cookie( + kookie, + value=val.value, + max_age=val["max-age"], + expires=val["expires"], + path=val["path"], + domain=val["domain"], + secure=val["secure"], + httponly=val["httponly"], + samesite=val["samesite"], + ) @classmethod - def app_reverse_cache_key(self, urlconf_path, **kwargs): - return 'FEINCMS:%s:APPCONTENT:%s:%s' % ( - getattr(settings, 'SITE_ID', 0), + def app_reverse_cache_key(cls, urlconf_path, **kwargs): + return "FEINCMS:{}:APPCONTENT:{}:{}".format( + getattr(settings, "SITE_ID", 0), get_language(), urlconf_path, ) @@ -433,17 +455,21 @@ def closest_match(cls, urlconf_path): except AttributeError: page_class = cls.parent.field.rel.to - contents = cls.objects.filter( - parent__in=page_class.objects.active(), - urlconf_path=urlconf_path, - ).order_by('pk').select_related('parent') + contents = ( + cls.objects.filter( + parent__in=page_class.objects.active(), urlconf_path=urlconf_path + ) + .order_by("pk") + .select_related("parent") + ) if len(contents) > 1: try: current = short_language_code(get_language()) return [ - content for content in contents if - short_language_code(content.parent.language) == current + content + for content in contents + if short_language_code(content.parent.language) == current ][0] except (AttributeError, IndexError): diff --git a/feincms/content/contactform/__init__.py b/feincms/content/contactform/__init__.py index f76f4292e..2b97afb5a 100644 --- a/feincms/content/contactform/__init__.py +++ b/feincms/content/contactform/__init__.py @@ -1,8 +1,10 @@ # flake8: noqa -from __future__ import absolute_import, unicode_literals import warnings + warnings.warn( - 'The contactform content has been deprecated. Use form-designer instead.', - DeprecationWarning, stacklevel=2) + "The contactform content has been deprecated. Use form-designer instead.", + DeprecationWarning, + stacklevel=2, +) diff --git a/feincms/content/contactform/models.py b/feincms/content/contactform/models.py index cfda97377..6699703cc 100644 --- a/feincms/content/contactform/models.py +++ b/feincms/content/contactform/models.py @@ -5,26 +5,20 @@ ``form=YourClass`` argument to the ``create_content_type`` call. """ -from __future__ import absolute_import, unicode_literals - from django import forms from django.core.mail import send_mail from django.db import models from django.http import HttpResponseRedirect from django.template.loader import render_to_string -from django.utils.translation import ugettext_lazy as _ - -from feincms._internal import ct_render_to_string +from django.utils.translation import gettext_lazy as _ class ContactForm(forms.Form): - name = forms.CharField(label=_('name')) - email = forms.EmailField(label=_('email')) - subject = forms.CharField(label=_('subject')) + name = forms.CharField(label=_("name")) + email = forms.EmailField(label=_("email")) + subject = forms.CharField(label=_("subject")) - content = forms.CharField( - widget=forms.Textarea, required=False, - label=_('content')) + content = forms.CharField(widget=forms.Textarea, required=False, label=_("content")) class ContactFormContent(models.Model): @@ -35,8 +29,8 @@ class ContactFormContent(models.Model): class Meta: abstract = True - verbose_name = _('contact form') - verbose_name_plural = _('contact forms') + verbose_name = _("contact form") + verbose_name_plural = _("contact forms") @classmethod def initialize_type(cls, form=None): @@ -44,42 +38,40 @@ def initialize_type(cls, form=None): cls.form = form def process(self, request, **kwargs): - if request.GET.get('_cf_thanks'): - self.rendered_output = ct_render_to_string( - 'content/contactform/thanks.html', - {'content': self}, - request=request) + if request.GET.get("_cf_thanks"): + self.rendered_output = render_to_string( + "content/contactform/thanks.html", {"content": self}, request=request + ) return - if request.method == 'POST': + if request.method == "POST": form = self.form(request.POST) if form.is_valid(): send_mail( - form.cleaned_data['subject'], - render_to_string('content/contactform/email.txt', { - 'data': form.cleaned_data, - }), - form.cleaned_data['email'], + form.cleaned_data["subject"], + render_to_string( + "content/contactform/email.txt", {"data": form.cleaned_data} + ), + form.cleaned_data["email"], [self.email], fail_silently=True, ) - return HttpResponseRedirect('?_cf_thanks=1') + return HttpResponseRedirect("?_cf_thanks=1") else: - initial = {'subject': self.subject} - if request.user.is_authenticated(): - initial['email'] = request.user.email - initial['name'] = request.user.get_full_name() + initial = {"subject": self.subject} + if request.user.is_authenticated: + initial["email"] = request.user.email + initial["name"] = request.user.get_full_name() form = self.form(initial=initial) - self.rendered_output = ct_render_to_string( - 'content/contactform/form.html', { - 'content': self, - 'form': form, - }, - request=request) + self.rendered_output = render_to_string( + "content/contactform/form.html", + {"content": self, "form": form}, + request=request, + ) def render(self, **kwargs): - return getattr(self, 'rendered_output', '') + return getattr(self, "rendered_output", "") diff --git a/feincms/content/file/models.py b/feincms/content/file/models.py index fda078a9f..7fc8bec58 100644 --- a/feincms/content/file/models.py +++ b/feincms/content/file/models.py @@ -3,15 +3,13 @@ instead. """ -from __future__ import absolute_import, unicode_literals - import os from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from feincms import settings -from feincms._internal import ct_render_to_string +from feincms.utils.tuple import AutoRenderTuple class FileContent(models.Model): @@ -20,21 +18,20 @@ class FileContent(models.Model): title = models.CharField(max_length=200) file = models.FileField( - _('file'), max_length=255, - upload_to=os.path.join(settings.FEINCMS_UPLOAD_PREFIX, 'filecontent')) + _("file"), + max_length=255, + upload_to=os.path.join(settings.FEINCMS_UPLOAD_PREFIX, "filecontent"), + ) class Meta: abstract = True - verbose_name = _('file') - verbose_name_plural = _('files') + verbose_name = _("file") + verbose_name_plural = _("files") def render(self, **kwargs): - return ct_render_to_string( - [ - 'content/file/%s.html' % self.region, - 'content/file/default.html', - ], - {'content': self}, - request=kwargs.get('request'), - context=kwargs.get('context'), + return AutoRenderTuple( + ( + ["content/file/%s.html" % self.region, "content/file/default.html"], + {"content": self}, + ) ) diff --git a/feincms/content/filer/models.py b/feincms/content/filer/models.py index f728f0fa1..df48d7147 100644 --- a/feincms/content/filer/models.py +++ b/feincms/content/filer/models.py @@ -1,12 +1,11 @@ -from __future__ import absolute_import, unicode_literals - from django.contrib import admin from django.core.exceptions import ImproperlyConfigured from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from feincms.admin.item_editor import FeinCMSInline -from feincms._internal import ct_render_to_string +from feincms.utils.tuple import AutoRenderTuple + try: from filer.fields.file import FilerFileField @@ -15,46 +14,50 @@ __all__ = () else: - __all__ = ( - 'MediaFileContentInline', 'ContentWithFilerFile', - 'FilerFileContent', 'FilerImageContent', + "MediaFileContentInline", + "ContentWithFilerFile", + "FilerFileContent", + "FilerImageContent", ) class MediaFileContentInline(FeinCMSInline): - radio_fields = {'type': admin.VERTICAL} + radio_fields = {"type": admin.VERTICAL} class ContentWithFilerFile(models.Model): """ File content """ + feincms_item_editor_inline = MediaFileContentInline class Meta: abstract = True def render(self, **kwargs): - return ct_render_to_string( - [ - 'content/filer/%s_%s.html' % (self.file_type, self.type), - 'content/filer/%s.html' % self.type, - 'content/filer/%s.html' % self.file_type, - 'content/filer/default.html', - ], - {'content': self}, - request=kwargs.get('request'), - context=kwargs.get('context'), + return AutoRenderTuple( + ( + [ + f"content/filer/{self.file_type}_{self.type}.html", + "content/filer/%s.html" % self.type, + "content/filer/%s.html" % self.file_type, + "content/filer/default.html", + ], + {"content": self}, + ) ) class FilerFileContent(ContentWithFilerFile): - mediafile = FilerFileField(verbose_name=_('file'), related_name='+') - file_type = 'file' - type = 'download' + mediafile = FilerFileField( + verbose_name=_("file"), related_name="+", on_delete=models.CASCADE + ) + file_type = "file" + type = "download" class Meta: abstract = True - verbose_name = _('file') - verbose_name_plural = _('files') + verbose_name = _("file") + verbose_name_plural = _("files") class FilerImageContent(ContentWithFilerFile): """ @@ -83,36 +86,30 @@ class FilerImageContent(ContentWithFilerFile): must_always_publish_copyright, date_taken, file, id, is_public, url """ - mediafile = FilerImageField(verbose_name=_('image'), related_name='+') - caption = models.CharField( - _('caption'), - max_length=1000, - blank=True, - ) - url = models.CharField( - _('URL'), - max_length=1000, - blank=True, + mediafile = FilerImageField( + verbose_name=_("image"), related_name="+", on_delete=models.CASCADE ) + caption = models.CharField(_("caption"), max_length=1000, blank=True) + url = models.CharField(_("URL"), max_length=1000, blank=True) - file_type = 'image' + file_type = "image" class Meta: abstract = True - verbose_name = _('image') - verbose_name_plural = _('images') + verbose_name = _("image") + verbose_name_plural = _("images") @classmethod def initialize_type(cls, TYPE_CHOICES=None): if TYPE_CHOICES is None: raise ImproperlyConfigured( - 'You have to set TYPE_CHOICES when' - ' creating a %s' % cls.__name__) + "You have to set TYPE_CHOICES when creating a %s" % cls.__name__ + ) cls.add_to_class( - 'type', + "type", models.CharField( - _('type'), + _("type"), max_length=20, choices=TYPE_CHOICES, default=TYPE_CHOICES[0][0], diff --git a/feincms/content/image/models.py b/feincms/content/image/models.py index 170236a92..1593ba5ec 100644 --- a/feincms/content/image/models.py +++ b/feincms/content/image/models.py @@ -3,16 +3,14 @@ instead. """ -from __future__ import absolute_import, unicode_literals - import os from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from feincms import settings -from feincms._internal import ct_render_to_string from feincms.templatetags import feincms_thumbnail +from feincms.utils.tuple import AutoRenderTuple class ImageContent(models.Model): @@ -43,54 +41,54 @@ class ImageContent(models.Model): """ image = models.ImageField( - _('image'), max_length=255, - upload_to=os.path.join(settings.FEINCMS_UPLOAD_PREFIX, 'imagecontent')) + _("image"), + max_length=255, + upload_to=os.path.join(settings.FEINCMS_UPLOAD_PREFIX, "imagecontent"), + ) alt_text = models.CharField( - _('alternate text'), max_length=255, blank=True, - help_text=_('Description of image')) - caption = models.CharField(_('caption'), max_length=255, blank=True) + _("alternate text"), + max_length=255, + blank=True, + help_text=_("Description of image"), + ) + caption = models.CharField(_("caption"), max_length=255, blank=True) class Meta: abstract = True - verbose_name = _('image') - verbose_name_plural = _('images') + verbose_name = _("image") + verbose_name_plural = _("images") def render(self, **kwargs): - templates = ['content/image/default.html'] - if hasattr(self, 'position'): - templates.insert(0, 'content/image/%s.html' % self.position) - - return ct_render_to_string( - templates, - {'content': self}, - request=kwargs.get('request'), - context=kwargs.get('context'), - ) + templates = ["content/image/default.html"] + if hasattr(self, "position"): + templates.insert(0, "content/image/%s.html" % self.position) + + return AutoRenderTuple((templates, {"content": self})) def get_image(self): - type, separator, size = getattr(self, 'format', '').partition(':') + img_type, _, size = getattr(self, "format", "").partition(":") if not size: return self.image - thumbnailer = { - 'cropscale': feincms_thumbnail.CropscaleThumbnailer, - }.get(type, feincms_thumbnail.Thumbnailer) + thumbnailer = {"cropscale": feincms_thumbnail.CropscaleThumbnailer}.get( + img_type, feincms_thumbnail.Thumbnailer + ) return thumbnailer(self.image, size) @classmethod def initialize_type(cls, POSITION_CHOICES=None, FORMAT_CHOICES=None): if POSITION_CHOICES: models.CharField( - _('position'), + _("position"), max_length=10, choices=POSITION_CHOICES, - default=POSITION_CHOICES[0][0] - ).contribute_to_class(cls, 'position') + default=POSITION_CHOICES[0][0], + ).contribute_to_class(cls, "position") if FORMAT_CHOICES: models.CharField( - _('format'), + _("format"), max_length=64, choices=FORMAT_CHOICES, - default=FORMAT_CHOICES[0][0] - ).contribute_to_class(cls, 'format') + default=FORMAT_CHOICES[0][0], + ).contribute_to_class(cls, "format") diff --git a/feincms/content/medialibrary/models.py b/feincms/content/medialibrary/models.py index 9e8d2e30d..3c575e56c 100644 --- a/feincms/content/medialibrary/models.py +++ b/feincms/content/medialibrary/models.py @@ -1,10 +1,10 @@ # flake8: noqa -from __future__ import absolute_import, unicode_literals import warnings -from feincms.module.medialibrary.contents import MediaFileContent warnings.warn( - 'Import MediaFileContent from feincms.module.medialibrary.contents.', - DeprecationWarning, stacklevel=2) + "Import MediaFileContent from feincms.module.medialibrary.contents.", + DeprecationWarning, + stacklevel=2, +) diff --git a/feincms/content/raw/models.py b/feincms/content/raw/models.py index 487c5a241..ca8068642 100644 --- a/feincms/content/raw/models.py +++ b/feincms/content/raw/models.py @@ -1,8 +1,6 @@ -from __future__ import absolute_import, unicode_literals - from django.db import models from django.utils.safestring import mark_safe -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ class RawContent(models.Model): @@ -13,12 +11,12 @@ class RawContent(models.Model): snippets too. """ - text = models.TextField(_('content'), blank=True) + text = models.TextField(_("content"), blank=True) class Meta: abstract = True - verbose_name = _('raw content') - verbose_name_plural = _('raw contents') + verbose_name = _("raw content") + verbose_name_plural = _("raw contents") def render(self, **kwargs): return mark_safe(self.text) diff --git a/feincms/content/richtext/models.py b/feincms/content/richtext/models.py index 8ad5a1e9c..cd698ee69 100644 --- a/feincms/content/richtext/models.py +++ b/feincms/content/richtext/models.py @@ -1,11 +1,9 @@ -from __future__ import absolute_import, unicode_literals - from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from feincms import settings -from feincms._internal import ct_render_to_string from feincms.contrib.richtext import RichTextField +from feincms.utils.tuple import AutoRenderTuple class RichTextContent(models.Model): @@ -24,26 +22,16 @@ class RichTextContent(models.Model): feincms_item_editor_context_processors = ( lambda x: settings.FEINCMS_RICHTEXT_INIT_CONTEXT, ) - feincms_item_editor_includes = { - 'head': [settings.FEINCMS_RICHTEXT_INIT_TEMPLATE], - } + feincms_item_editor_includes = {"head": [settings.FEINCMS_RICHTEXT_INIT_TEMPLATE]} class Meta: abstract = True - verbose_name = _('rich text') - verbose_name_plural = _('rich texts') + verbose_name = _("rich text") + verbose_name_plural = _("rich texts") def render(self, **kwargs): - return ct_render_to_string( - 'content/richtext/default.html', - {'content': self}, - request=kwargs.get('request'), - context=kwargs.get('context'), - ) + return AutoRenderTuple(("content/richtext/default.html", {"content": self})) @classmethod def initialize_type(cls, cleanse=None): - cls.add_to_class( - 'text', - RichTextField(_('text'), blank=True, cleanse=cleanse), - ) + cls.add_to_class("text", RichTextField(_("text"), blank=True, cleanse=cleanse)) diff --git a/feincms/content/section/models.py b/feincms/content/section/models.py index 44dbdf571..48f344cc7 100644 --- a/feincms/content/section/models.py +++ b/feincms/content/section/models.py @@ -1,22 +1,20 @@ -from __future__ import absolute_import, unicode_literals - from django.conf import settings as django_settings from django.contrib import admin from django.core.exceptions import ImproperlyConfigured from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from feincms import settings -from feincms._internal import ct_render_to_string from feincms.admin.item_editor import FeinCMSInline from feincms.contrib.richtext import RichTextField from feincms.module.medialibrary.fields import MediaFileForeignKey from feincms.module.medialibrary.models import MediaFile +from feincms.utils.tuple import AutoRenderTuple class SectionContentInline(FeinCMSInline): - raw_id_fields = ('mediafile',) - radio_fields = {'type': admin.VERTICAL} + raw_id_fields = ("mediafile",) + radio_fields = {"type": admin.VERTICAL} class SectionContent(models.Model): @@ -28,39 +26,46 @@ class SectionContent(models.Model): feincms_item_editor_context_processors = ( lambda x: settings.FEINCMS_RICHTEXT_INIT_CONTEXT, ) - feincms_item_editor_includes = { - 'head': [settings.FEINCMS_RICHTEXT_INIT_TEMPLATE], - } + feincms_item_editor_includes = {"head": [settings.FEINCMS_RICHTEXT_INIT_TEMPLATE]} - title = models.CharField(_('title'), max_length=200, blank=True) - richtext = RichTextField(_('text'), blank=True) + title = models.CharField(_("title"), max_length=200, blank=True) + richtext = RichTextField(_("text"), blank=True) mediafile = MediaFileForeignKey( - MediaFile, on_delete=models.CASCADE, - verbose_name=_('media file'), - related_name='+', blank=True, null=True) + MediaFile, + on_delete=models.CASCADE, + verbose_name=_("media file"), + related_name="+", + blank=True, + null=True, + ) class Meta: abstract = True - verbose_name = _('section') - verbose_name_plural = _('sections') + verbose_name = _("section") + verbose_name_plural = _("sections") @classmethod def initialize_type(cls, TYPE_CHOICES=None, cleanse=None): - if 'feincms.module.medialibrary' not in django_settings.INSTALLED_APPS: + if "feincms.module.medialibrary" not in django_settings.INSTALLED_APPS: raise ImproperlyConfigured( - 'You have to add \'feincms.module.medialibrary\' to your' - ' INSTALLED_APPS before creating a %s' % cls.__name__) + "You have to add 'feincms.module.medialibrary' to your" + " INSTALLED_APPS before creating a %s" % cls.__name__ + ) if TYPE_CHOICES is None: raise ImproperlyConfigured( - 'You need to set TYPE_CHOICES when creating a' - ' %s' % cls.__name__) - - cls.add_to_class('type', models.CharField( - _('type'), - max_length=10, choices=TYPE_CHOICES, - default=TYPE_CHOICES[0][0] - )) + "You need to set TYPE_CHOICES when creating a %s" % cls.__name__ + ) + + cls.add_to_class( + "type", + models.CharField( + _("type"), + max_length=10, + choices=TYPE_CHOICES, + default=TYPE_CHOICES[0][0], + ), + ) if cleanse: cls.cleanse = cleanse @@ -68,29 +73,28 @@ def initialize_type(cls, TYPE_CHOICES=None, cleanse=None): @classmethod def get_queryset(cls, filter_args): # Explicitly add nullable FK mediafile to minimize the DB query count - return cls.objects.select_related('parent', 'mediafile').filter( - filter_args) + return cls.objects.select_related("parent", "mediafile").filter(filter_args) def render(self, **kwargs): if self.mediafile: mediafile_type = self.mediafile.type else: - mediafile_type = 'nomedia' - - return ct_render_to_string( - [ - 'content/section/%s_%s.html' % (mediafile_type, self.type), - 'content/section/%s.html' % mediafile_type, - 'content/section/%s.html' % self.type, - 'content/section/default.html', - ], - {'content': self}, - request=kwargs.get('request'), - context=kwargs.get('context'), + mediafile_type = "nomedia" + + return AutoRenderTuple( + ( + [ + f"content/section/{mediafile_type}_{self.type}.html", + "content/section/%s.html" % mediafile_type, + "content/section/%s.html" % self.type, + "content/section/default.html", + ], + {"content": self}, + ) ) def save(self, *args, **kwargs): - if getattr(self, 'cleanse', None): + if getattr(self, "cleanse", None): try: # Passes the rich text content as first argument because # the passed callable has been converted into a bound method @@ -100,5 +104,6 @@ def save(self, *args, **kwargs): # content instance along self.richtext = self.cleanse.im_func(self.richtext) - super(SectionContent, self).save(*args, **kwargs) + super().save(*args, **kwargs) + save.alters_data = True diff --git a/feincms/content/template/models.py b/feincms/content/template/models.py index 26ec26e44..57e99ab70 100644 --- a/feincms/content/template/models.py +++ b/feincms/content/template/models.py @@ -1,11 +1,9 @@ -from __future__ import absolute_import, unicode_literals - from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ -from feincms._internal import ct_render_to_string from feincms.content.raw.models import RawContent # noqa from feincms.content.richtext.models import RichTextContent # noqa +from feincms.utils.tuple import AutoRenderTuple class TemplateContent(models.Model): @@ -19,23 +17,18 @@ class TemplateContent(models.Model): ('base.html', 'makes no sense'), ]) """ + class Meta: abstract = True - verbose_name = _('template content') - verbose_name_plural = _('template contents') + verbose_name = _("template content") + verbose_name_plural = _("template contents") @classmethod def initialize_type(cls, TEMPLATES): - cls.add_to_class('template', models.CharField( - _('template'), - max_length=100, - choices=TEMPLATES, - )) + cls.add_to_class( + "template", + models.CharField(_("template"), max_length=100, choices=TEMPLATES), + ) def render(self, **kwargs): - return ct_render_to_string( - self.template, - {'content': self}, - request=kwargs.get('request'), - context=kwargs.get('context'), - ) + return AutoRenderTuple((self.template, {"content": self})) diff --git a/feincms/content/video/models.py b/feincms/content/video/models.py index 32a4cae81..e6f67da70 100644 --- a/feincms/content/video/models.py +++ b/feincms/content/video/models.py @@ -1,11 +1,9 @@ -from __future__ import absolute_import, unicode_literals - import re from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ -from feincms._internal import ct_render_to_string +from feincms.utils.tuple import AutoRenderTuple class VideoContent(models.Model): @@ -20,38 +18,43 @@ class VideoContent(models.Model): """ PORTALS = ( - ('youtube', re.compile(r'youtube'), lambda url: { - 'v': re.search(r'([?&]v=|./././)([^#&]+)', url).group(2), - }), - ('vimeo', re.compile(r'vimeo'), lambda url: { - 'id': re.search(r'/(\d+)', url).group(1), - }), - ('sf', re.compile(r'sf\.tv'), lambda url: { - 'id': re.search(r'/([a-z0-9\-]+)', url).group(1), - }), + ( + "youtube", + re.compile(r"youtube"), + lambda url: {"v": re.search(r"([?&]v=|./././)([^#&]+)", url).group(2)}, + ), + ( + "vimeo", + re.compile(r"vimeo"), + lambda url: {"id": re.search(r"/(\d+)", url).group(1)}, + ), + ( + "sf", + re.compile(r"sf\.tv"), + lambda url: {"id": re.search(r"/([a-z0-9\-]+)", url).group(1)}, + ), ) video = models.URLField( - _('video link'), + _("video link"), help_text=_( - 'This should be a link to a youtube or vimeo video,' - ' i.e.: http://www.youtube.com/watch?v=zmj1rpzDRZ0')) + "This should be a link to a youtube or vimeo video," + " i.e.: http://www.youtube.com/watch?v=zmj1rpzDRZ0" + ), + ) class Meta: abstract = True - verbose_name = _('video') - verbose_name_plural = _('videos') + verbose_name = _("video") + verbose_name_plural = _("videos") def get_context_dict(self): "Extend this if you need more variables passed to template" - return {'content': self, 'portal': 'unknown'} + return {"content": self, "portal": "unknown"} - def get_templates(self, portal='unknown'): + def get_templates(self, portal="unknown"): "Extend/override this if you want to modify the templates used" - return [ - 'content/video/%s.html' % portal, - 'content/video/unknown.html', - ] + return ["content/video/%s.html" % portal, "content/video/unknown.html"] def ctx_for_video(self, vurl): "Get a context dict for a given video URL" @@ -60,7 +63,7 @@ def ctx_for_video(self, vurl): if match.search(vurl): try: ctx.update(context_fn(vurl)) - ctx['portal'] = portal + ctx["portal"] = portal break except AttributeError: continue @@ -68,9 +71,4 @@ def ctx_for_video(self, vurl): def render(self, **kwargs): ctx = self.ctx_for_video(self.video) - return ct_render_to_string( - self.get_templates(ctx['portal']), - ctx, - request=kwargs.get('request'), - context=kwargs.get('context'), - ) + return AutoRenderTuple((self.get_templates(ctx["portal"]), ctx)) diff --git a/feincms/contents.py b/feincms/contents.py index 9dc37f9a7..dd31f2f18 100644 --- a/feincms/contents.py +++ b/feincms/contents.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, unicode_literals - from feincms.content.filer.models import * # noqa from feincms.content.raw.models import RawContent # noqa from feincms.content.richtext.models import RichTextContent # noqa diff --git a/feincms/context_processors.py b/feincms/context_processors.py index 3f5c9b1a7..30bf7de70 100644 --- a/feincms/context_processors.py +++ b/feincms/context_processors.py @@ -7,8 +7,6 @@ def add_page_if_missing(request): """ try: - return { - 'feincms_page': Page.objects.for_request(request, best_match=True), - } + return {"feincms_page": Page.objects.for_request(request, best_match=True)} except Page.DoesNotExist: return {} diff --git a/feincms/contrib/fields.py b/feincms/contrib/fields.py index 37af3bdec..38f13a714 100644 --- a/feincms/contrib/fields.py +++ b/feincms/contrib/fields.py @@ -1,14 +1,9 @@ -from __future__ import absolute_import, unicode_literals - import json import logging -from distutils.version import LooseVersion -from django import get_version from django import forms -from django.db import models from django.core.serializers.json import DjangoJSONEncoder -from django.utils import six +from django.db import models class JSONFormField(forms.fields.CharField): @@ -16,7 +11,7 @@ def clean(self, value, *args, **kwargs): # It seems that sometimes we receive dict objects here, not only # strings. Partial form validation maybe? if value: - if isinstance(value, six.string_types): + if isinstance(value, str): try: value = json.loads(value) except ValueError: @@ -29,17 +24,10 @@ def clean(self, value, *args, **kwargs): except ValueError: raise forms.ValidationError("Invalid JSON data!") - return super(JSONFormField, self).clean(value, *args, **kwargs) - - -if LooseVersion(get_version()) > LooseVersion('1.8'): - workaround_class = models.TextField -else: - workaround_class = six.with_metaclass( - models.SubfieldBase, models.TextField) + return super().clean(value, *args, **kwargs) -class JSONField(workaround_class): +class JSONField(models.TextField): """ TextField which transparently serializes/unserializes JSON objects @@ -54,8 +42,7 @@ def to_python(self, value): if isinstance(value, dict): return value - elif (isinstance(value, six.string_types) or - isinstance(value, six.binary_type)): + elif isinstance(value, str) or isinstance(value, bytes): # Avoid asking the JSON decoder to handle empty values: if not value: return {} @@ -64,13 +51,14 @@ def to_python(self, value): return json.loads(value) except ValueError: logging.getLogger("feincms.contrib.fields").exception( - "Unable to deserialize store JSONField data: %s", value) + "Unable to deserialize store JSONField data: %s", value + ) return {} else: assert value is None return {} - def from_db_value(self, value, expression, connection, context): + def from_db_value(self, value, expression, connection, context=None): return self.to_python(value) def get_prep_value(self, value): @@ -97,6 +85,6 @@ def _flatten_value(self, value): if isinstance(value, dict): value = json.dumps(value, cls=DjangoJSONEncoder) - assert isinstance(value, six.string_types) + assert isinstance(value, str) return value diff --git a/feincms/contrib/preview/urls.py b/feincms/contrib/preview/urls.py index 1f4e324f0..bf9f21a7d 100644 --- a/feincms/contrib/preview/urls.py +++ b/feincms/contrib/preview/urls.py @@ -1,9 +1,8 @@ -from django.conf.urls import url +from django.urls import re_path from feincms.contrib.preview.views import PreviewHandler urlpatterns = [ - url(r'^(.*)/_preview/(\d+)/$', PreviewHandler.as_view(), - name='feincms_preview'), + re_path(r"^(.*)/_preview/(\d+)/$", PreviewHandler.as_view(), name="feincms_preview") ] diff --git a/feincms/contrib/preview/views.py b/feincms/contrib/preview/views.py index e1c37586c..08b3e5aaa 100644 --- a/feincms/contrib/preview/views.py +++ b/feincms/contrib/preview/views.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, unicode_literals - from django.http import Http404 from django.shortcuts import get_object_or_404 @@ -18,8 +16,13 @@ class PreviewHandler(Handler): def get_object(self): """Get the page by the id in the url here instead.""" - page = get_object_or_404(self.page_model, pk=self.args[1]) + # This might happen when there is a preview request for + # a page that doesn't exist, and now FeinCMS wants to + # show a 404 page via FEINCMS_CMS_404_PAGE. + if len(self.args) < 2: + return super().get_object() + page = get_object_or_404(self.page_model, pk=self.args[1]) # Remove _preview/42/ from URL, the rest of the handler code should not # know that anything about previewing. Handler.prepare will still raise # a 404 if the extra_path isn't consumed by any content type @@ -29,9 +32,7 @@ def get_object(self): def handler(self, request, *args, **kwargs): if not request.user.is_staff: - raise Http404('Not found (not allowed)') - response = super(PreviewHandler, self).handler( - request, *args, **kwargs) - response['Cache-Control'] =\ - 'no-cache, must-revalidate, no-store, private' + raise Http404("Not found (not allowed)") + response = super().handler(request, *args, **kwargs) + response["Cache-Control"] = "no-cache, must-revalidate, no-store, private" return response diff --git a/feincms/contrib/richtext.py b/feincms/contrib/richtext.py index 3b9985e55..5a6a7da19 100644 --- a/feincms/contrib/richtext.py +++ b/feincms/contrib/richtext.py @@ -1,19 +1,17 @@ -from __future__ import absolute_import, unicode_literals - from django import forms from django.db import models class RichTextFormField(forms.fields.CharField): def __init__(self, *args, **kwargs): - self.cleanse = kwargs.pop('cleanse', None) - super(RichTextFormField, self).__init__(*args, **kwargs) - css_class = self.widget.attrs.get('class', '') - css_class += ' item-richtext' - self.widget.attrs['class'] = css_class + self.cleanse = kwargs.pop("cleanse", None) + super().__init__(*args, **kwargs) + css_class = self.widget.attrs.get("class", "") + css_class += " item-richtext" + self.widget.attrs["class"] = css_class def clean(self, value): - value = super(RichTextFormField, self).clean(value) + value = super().clean(value) if self.cleanse: value = self.cleanse(value) return value @@ -24,12 +22,10 @@ class RichTextField(models.TextField): Drop-in replacement for Django's ``models.TextField`` which allows editing rich text instead of plain text in the item editor. """ + def __init__(self, *args, **kwargs): - self.cleanse = kwargs.pop('cleanse', None) - super(RichTextField, self).__init__(*args, **kwargs) + self.cleanse = kwargs.pop("cleanse", None) + super().__init__(*args, **kwargs) def formfield(self, form_class=RichTextFormField, **kwargs): - return super(RichTextField, self).formfield( - form_class=form_class, - cleanse=self.cleanse, - **kwargs) + return super().formfield(form_class=form_class, cleanse=self.cleanse, **kwargs) diff --git a/feincms/contrib/tagging.py b/feincms/contrib/tagging.py index 6520f069c..6c29eb727 100644 --- a/feincms/contrib/tagging.py +++ b/feincms/contrib/tagging.py @@ -1,5 +1,4 @@ # ------------------------------------------------------------------------ -# coding=utf-8 # ------------------------------------------------------------------------ # FeinCMS django-tagging support. To add tagging to your (page) model, # simply do a @@ -8,31 +7,26 @@ # tagging.tag_model(Page) # ------------------------------------------------------------------------ -from __future__ import absolute_import, unicode_literals -from django import forms, VERSION +from django import forms from django.contrib.admin.widgets import FilteredSelectMultiple from django.db.models.signals import pre_save -from django.utils import six -from django.utils.translation import ugettext_lazy as _ - +from django.utils.translation import gettext_lazy as _ from tagging.fields import TagField from tagging.models import Tag +from tagging.registry import AlreadyRegistered, register as tagging_register from tagging.utils import parse_tag_input -try: - from tagging.registry import AlreadyRegistered -except ImportError: - from tagging import AlreadyRegistered # ------------------------------------------------------------------------ def taglist_to_string(taglist): - retval = '' + retval = "" if len(taglist) >= 1: taglist.sort() - retval = ','.join(taglist) + retval = ",".join(taglist) return retval + # ------------------------------------------------------------------------ # The following is lifted from: # http://code.google.com/p/django-tagging/issues/detail?id=189 @@ -56,20 +50,10 @@ def clean(self, value): return taglist_to_string(list(value)) -if VERSION >= (1, 10): - class Tag_formatvalue_mixin(object): - def format_value(self, value): - value = parse_tag_input(value or '') - return super(Tag_formatvalue_mixin, self).format_value(value) -else: - # _format_value is a private method previous to Django 1.10, - # do the job in render() instead to avoid fiddling with - # anybody's privates - class Tag_formatvalue_mixin(object): - def render(self, name, value, attrs=None, *args, **kwargs): - value = parse_tag_input(value or '') - return super(Tag_formatvalue_mixin, self).render( - name, value, attrs, *args, **kwargs) +class Tag_formatvalue_mixin: + def format_value(self, value): + value = parse_tag_input(value or "") + return super().format_value(value) class fv_FilteredSelectMultiple(Tag_formatvalue_mixin, FilteredSelectMultiple): @@ -82,22 +66,18 @@ class fv_SelectMultiple(Tag_formatvalue_mixin, forms.SelectMultiple): class TagSelectField(TagField): def __init__(self, filter_horizontal=False, *args, **kwargs): - super(TagSelectField, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.filter_horizontal = filter_horizontal def formfield(self, **defaults): if self.filter_horizontal: - widget = fv_FilteredSelectMultiple( - self.verbose_name, is_stacked=False) + widget = fv_FilteredSelectMultiple(self.verbose_name, is_stacked=False) else: widget = fv_SelectMultiple() - defaults['widget'] = widget - choices = [( - six.text_type(t), - six.text_type(t)) for t in Tag.objects.all()] - return TagSelectFormField( - choices=choices, required=not self.blank, **defaults) + defaults["widget"] = widget + choices = [(str(t), str(t)) for t in Tag.objects.all()] + return TagSelectFormField(choices=choices, required=not self.blank, **defaults) # ------------------------------------------------------------------------ @@ -111,9 +91,15 @@ def pre_save_handler(sender, instance, **kwargs): # ------------------------------------------------------------------------ -def tag_model(cls, admin_cls=None, field_name='tags', sort_tags=False, - select_field=False, auto_add_admin_field=True, - admin_list_display=True): +def tag_model( + cls, + admin_cls=None, + field_name="tags", + sort_tags=False, + select_field=False, + auto_add_admin_field=True, + admin_list_display=True, +): """ tag_model accepts a number of named parameters: @@ -131,19 +117,18 @@ def tag_model(cls, admin_cls=None, field_name='tags', sort_tags=False, auto_add_admin_field If True, attempts to add the tag field to the admin class. """ - try: - from tagging.registry import register as tagging_register - except ImportError: - from tagging import register as tagging_register - cls.add_to_class(field_name, ( - TagSelectField if select_field else TagField - )(field_name.capitalize(), blank=True)) + cls.add_to_class( + field_name, + (TagSelectField if select_field else TagField)( + field_name.capitalize(), blank=True + ), + ) # use another name for the tag descriptor # See http://code.google.com/p/django-tagging/issues/detail?id=95 for the # reason why try: - tagging_register(cls, tag_descriptor_attr='tagging_' + field_name) + tagging_register(cls, tag_descriptor_attr="tagging_" + field_name) except AlreadyRegistered: return @@ -152,13 +137,11 @@ def tag_model(cls, admin_cls=None, field_name='tags', sort_tags=False, admin_cls.list_display.append(field_name) admin_cls.list_filter.append(field_name) - if auto_add_admin_field and hasattr( - admin_cls, 'add_extension_options'): - admin_cls.add_extension_options(_('Tagging'), { - 'fields': (field_name,) - }) + if auto_add_admin_field and hasattr(admin_cls, "add_extension_options"): + admin_cls.add_extension_options(_("Tagging"), {"fields": (field_name,)}) if sort_tags: pre_save.connect(pre_save_handler, sender=cls) + # ------------------------------------------------------------------------ diff --git a/feincms/default_settings.py b/feincms/default_settings.py index 3cbf95f2c..488bf673f 100644 --- a/feincms/default_settings.py +++ b/feincms/default_settings.py @@ -1,5 +1,4 @@ # ------------------------------------------------------------------------ -# coding=utf-8 # ------------------------------------------------------------------------ """ Default settings for FeinCMS @@ -8,48 +7,46 @@ ``settings.py`` file. """ -from __future__ import absolute_import, unicode_literals - from django.conf import settings + # e.g. 'uploads' if you would prefer /uploads/imagecontent/test.jpg # to /imagecontent/test.jpg. -FEINCMS_UPLOAD_PREFIX = getattr( - settings, - 'FEINCMS_UPLOAD_PREFIX', - '') +FEINCMS_UPLOAD_PREFIX = getattr(settings, "FEINCMS_UPLOAD_PREFIX", "") # ------------------------------------------------------------------------ # Settings for MediaLibrary #: Local path to newly uploaded media files FEINCMS_MEDIALIBRARY_UPLOAD_TO = getattr( - settings, - 'FEINCMS_MEDIALIBRARY_UPLOAD_TO', - 'medialibrary/%Y/%m/') + settings, "FEINCMS_MEDIALIBRARY_UPLOAD_TO", "medialibrary/%Y/%m/" +) #: Thumbnail function for suitable mediafiles. Only receives the media file #: and should return a thumbnail URL (or nothing). FEINCMS_MEDIALIBRARY_THUMBNAIL = getattr( settings, - 'FEINCMS_MEDIALIBRARY_THUMBNAIL', - 'feincms.module.medialibrary.thumbnail.default_admin_thumbnail') + "FEINCMS_MEDIALIBRARY_THUMBNAIL", + "feincms.module.medialibrary.thumbnail.default_admin_thumbnail", +) # ------------------------------------------------------------------------ # Settings for RichText FEINCMS_RICHTEXT_INIT_TEMPLATE = getattr( settings, - 'FEINCMS_RICHTEXT_INIT_TEMPLATE', - 'admin/content/richtext/init_tinymce4.html') + "FEINCMS_RICHTEXT_INIT_TEMPLATE", + "admin/content/richtext/init_tinymce4.html", +) FEINCMS_RICHTEXT_INIT_CONTEXT = getattr( settings, - 'FEINCMS_RICHTEXT_INIT_CONTEXT', { - 'TINYMCE_JS_URL': '//tinymce.cachefly.net/4.2/tinymce.min.js', - 'TINYMCE_DOMAIN': None, - 'TINYMCE_CONTENT_CSS_URL': None, - 'TINYMCE_LINK_LIST_URL': None - } + "FEINCMS_RICHTEXT_INIT_CONTEXT", + { + "TINYMCE_JS_URL": "https://cdnjs.cloudflare.com/ajax/libs/tinymce/4.9.11/tinymce.min.js", # noqa + "TINYMCE_DOMAIN": None, + "TINYMCE_CONTENT_CSS_URL": None, + "TINYMCE_LINK_LIST_URL": None, + }, ) # ------------------------------------------------------------------------ @@ -57,38 +54,29 @@ #: Include ancestors in filtered tree editor lists FEINCMS_TREE_EDITOR_INCLUDE_ANCESTORS = getattr( - settings, - 'FEINCMS_TREE_EDITOR_INCLUDE_ANCESTORS', - False) + settings, "FEINCMS_TREE_EDITOR_INCLUDE_ANCESTORS", False +) #: Enable checking of object level permissions. Note that if this option is #: enabled, you must plug in an authentication backend that actually does #: implement object level permissions or no page will be editable. FEINCMS_TREE_EDITOR_OBJECT_PERMISSIONS = getattr( - settings, - 'FEINCMS_TREE_EDITOR_OBJECT_PERMISSIONS', - False) + settings, "FEINCMS_TREE_EDITOR_OBJECT_PERMISSIONS", False +) #: When enabled, the page module is automatically registered with Django's #: default admin site (this is activated by default). -FEINCMS_USE_PAGE_ADMIN = getattr( - settings, - 'FEINCMS_USE_PAGE_ADMIN', - True) +FEINCMS_USE_PAGE_ADMIN = getattr(settings, "FEINCMS_USE_PAGE_ADMIN", True) #: app_label.model_name as per apps.get_model. #: defaults to page.Page FEINCMS_DEFAULT_PAGE_MODEL = getattr( - settings, - 'FEINCMS_DEFAULT_PAGE_MODEL', - 'page.Page') + settings, "FEINCMS_DEFAULT_PAGE_MODEL", "page.Page" +) # ------------------------------------------------------------------------ #: Allow random gunk after a valid page? -FEINCMS_ALLOW_EXTRA_PATH = getattr( - settings, - 'FEINCMS_ALLOW_EXTRA_PATH', - False) +FEINCMS_ALLOW_EXTRA_PATH = getattr(settings, "FEINCMS_ALLOW_EXTRA_PATH", False) # ------------------------------------------------------------------------ #: How to switch languages. @@ -96,10 +84,7 @@ #: and overwrites whatever was set before. #: * ``'EXPLICIT'``: The language set has priority, may only be overridden #: by explicitely a language with ``?set_language=xx``. -FEINCMS_TRANSLATION_POLICY = getattr( - settings, - 'FEINCMS_TRANSLATION_POLICY', - 'STANDARD') +FEINCMS_TRANSLATION_POLICY = getattr(settings, "FEINCMS_TRANSLATION_POLICY", "STANDARD") # ------------------------------------------------------------------------ #: Makes the page handling mechanism try to find a cms page with that @@ -107,60 +92,54 @@ #: customised cms-styled error pages. Do not go overboard, this should #: be as simple and as error resistant as possible, so refrain from #: deeply nested error pages or advanced content types. -FEINCMS_CMS_404_PAGE = getattr( - settings, - 'FEINCMS_CMS_404_PAGE', - None) +FEINCMS_CMS_404_PAGE = getattr(settings, "FEINCMS_CMS_404_PAGE", None) # ------------------------------------------------------------------------ #: When uploading files to the media library, replacing an existing entry, #: try to save the new file under the old file name in order to keep the #: media file path (and thus the media url) constant. #: Experimental, this might not work with all storage backends. -FEINCMS_MEDIAFILE_OVERWRITE = getattr( - settings, - 'FEINCMS_MEDIAFILE_OVERWRITE', - False) +FEINCMS_MEDIAFILE_OVERWRITE = getattr(settings, "FEINCMS_MEDIAFILE_OVERWRITE", False) # ------------------------------------------------------------------------ #: Prefix for thumbnails. Set this to something non-empty to separate thumbs #: from uploads. The value should end with a slash, but this is not enforced. -FEINCMS_THUMBNAIL_DIR = getattr( - settings, - 'FEINCMS_THUMBNAIL_DIR', - '_thumbs/') +FEINCMS_THUMBNAIL_DIR = getattr(settings, "FEINCMS_THUMBNAIL_DIR", "_thumbs/") # ------------------------------------------------------------------------ #: feincms_thumbnail template filter library cache timeout. The default is to #: not cache anything for backwards compatibility. FEINCMS_THUMBNAIL_CACHE_TIMEOUT = getattr( - settings, - 'FEINCMS_THUMBNAIL_CACHE_TIMEOUT', - 0) + settings, "FEINCMS_THUMBNAIL_CACHE_TIMEOUT", 0 +) # ------------------------------------------------------------------------ #: Prevent changing template within admin for pages which have been #: allocated a Template with singleton=True -- template field will become #: read-only for singleton pages. FEINCMS_SINGLETON_TEMPLATE_CHANGE_ALLOWED = getattr( - settings, - 'FEINCMS_SINGLETON_TEMPLATE_CHANGE_ALLOWED', - False) + settings, "FEINCMS_SINGLETON_TEMPLATE_CHANGE_ALLOWED", False +) #: Prevent admin page deletion for pages which have been allocated a #: Template with singleton=True FEINCMS_SINGLETON_TEMPLATE_DELETION_ALLOWED = getattr( - settings, - 'FEINCMS_SINGLETON_TEMPLATE_DELETION_ALLOWED', - False) + settings, "FEINCMS_SINGLETON_TEMPLATE_DELETION_ALLOWED", False +) # ------------------------------------------------------------------------ #: Filter languages available for front end users to this set. This allows #: to have languages not yet ready for prime time while being able to access #: those pages in the admin backend. -FEINCMS_FRONTEND_LANGUAGES = getattr( - settings, - 'FEINCMS_FRONTEND_LANGUAGES', - None) +FEINCMS_FRONTEND_LANGUAGES = getattr(settings, "FEINCMS_FRONTEND_LANGUAGES", None) + +# ------------------------------------------------------------------------ + +# ------------------------------------------------------------------------ +#: Attempt to get translations of MediaFile objects. If `False`, FeinCMS will +#: instead just output the file name. +FEINCMS_MEDIAFILE_TRANSLATIONS = getattr( + settings, "FEINCMS_MEDIAFILE_TRANSLATIONS", True +) # ------------------------------------------------------------------------ diff --git a/feincms/extensions/__init__.py b/feincms/extensions/__init__.py index 853f27091..f35d3ed49 100644 --- a/feincms/extensions/__init__.py +++ b/feincms/extensions/__init__.py @@ -1,10 +1,14 @@ -from __future__ import absolute_import - from .base import ( - ExtensionsMixin, Extension, ExtensionModelAdmin, - prefetch_modeladmin_get_queryset) + Extension, + ExtensionModelAdmin, + ExtensionsMixin, + prefetch_modeladmin_get_queryset, +) + __all__ = ( - 'ExtensionsMixin', 'Extension', 'ExtensionModelAdmin', - 'prefetch_modeladmin_get_queryset', + "ExtensionsMixin", + "Extension", + "ExtensionModelAdmin", + "prefetch_modeladmin_get_queryset", ) diff --git a/feincms/extensions/base.py b/feincms/extensions/base.py index 31773ff6f..c1b84b382 100644 --- a/feincms/extensions/base.py +++ b/feincms/extensions/base.py @@ -2,19 +2,16 @@ Base types for extensions refactor """ -from __future__ import absolute_import, unicode_literals - -from functools import wraps import inspect +from functools import wraps from django.contrib import admin from django.core.exceptions import ImproperlyConfigured -from django.utils import six from feincms.utils import get_object -class ExtensionsMixin(object): +class ExtensionsMixin: @classmethod def register_extensions(cls, *extensions): """ @@ -27,7 +24,7 @@ def register_extensions(cls, *extensions): sufficient. """ - if not hasattr(cls, '_extensions'): + if not hasattr(cls, "_extensions"): cls._extensions = [] cls._extensions_seen = [] @@ -40,47 +37,48 @@ def register_extensions(cls, *extensions): if inspect.isclass(ext) and issubclass(ext, Extension): extension = ext - elif isinstance(ext, six.string_types): + elif isinstance(ext, str): try: extension = get_object(ext) except (AttributeError, ImportError, ValueError): if not extension: raise ImproperlyConfigured( - '%s is not a valid extension for %s' % ( - ext, cls.__name__)) + f"{ext} is not a valid extension for {cls.__name__}" + ) - if hasattr(extension, 'Extension'): + if hasattr(extension, "Extension"): extension = extension.Extension - elif hasattr(extension, 'register'): + elif hasattr(extension, "register"): extension = extension.register - elif hasattr(extension, '__call__'): + elif hasattr(extension, "__call__"): pass else: raise ImproperlyConfigured( - '%s is not a valid extension for %s' % ( - ext, cls.__name__)) + f"{ext} is not a valid extension for {cls.__name__}" + ) if extension in cls._extensions_seen: continue cls._extensions_seen.append(extension) - if hasattr(extension, 'handle_model'): + if hasattr(extension, "handle_model"): cls._extensions.append(extension(cls)) else: - raise ImproperlyConfigured( - '%r is an invalid extension.' % extension) + raise ImproperlyConfigured("%r is an invalid extension." % extension) -class Extension(object): +class Extension: def __init__(self, model, **kwargs): self.model = model for key, value in kwargs.items(): if not hasattr(self, key): - raise TypeError('%s() received an invalid keyword %r' % ( - self.__class__.__name__, key)) + raise TypeError( + "%s() received an invalid keyword %r" + % (self.__class__.__name__, key) + ) setattr(self, key, value) self.handle_model() @@ -94,30 +92,30 @@ def handle_modeladmin(self, modeladmin): class ExtensionModelAdmin(admin.ModelAdmin): def __init__(self, *args, **kwargs): - super(ExtensionModelAdmin, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.initialize_extensions() def initialize_extensions(self): - if not hasattr(self, '_extensions_initialized'): + if not hasattr(self, "_extensions_initialized"): self._extensions_initialized = True - for extension in getattr(self.model, '_extensions', []): + for extension in getattr(self.model, "_extensions", []): extension.handle_modeladmin(self) def add_extension_options(self, *f): if self.fieldsets is None: return - if isinstance(f[-1], dict): # called with a fieldset + if isinstance(f[-1], dict): # called with a fieldset self.fieldsets.insert(self.fieldset_insertion_index, f) - f[1]['classes'] = list(f[1].get('classes', [])) - f[1]['classes'].append('collapse') - elif f: # assume called with "other" fields + f[1]["classes"] = list(f[1].get("classes", [])) + f[1]["classes"].append("collapse") + elif f: # assume called with "other" fields try: - self.fieldsets[1][1]['fields'].extend(f) + self.fieldsets[1][1]["fields"].extend(f) except IndexError: # Fall back to first fieldset if second does not exist # XXX This is really messy. - self.fieldsets[0][1]['fields'].extend(f) + self.fieldsets[0][1]["fields"].extend(f) def extend_list(self, attribute, iterable): extended = list(getattr(self, attribute, ())) @@ -129,12 +127,14 @@ def prefetch_modeladmin_get_queryset(modeladmin, *lookups): """ Wraps default modeladmin ``get_queryset`` to prefetch related lookups. """ + def do_wrap(f): @wraps(f) def wrapper(request, *args, **kwargs): qs = f(request, *args, **kwargs) qs = qs.prefetch_related(*lookups) return qs + return wrapper modeladmin.get_queryset = do_wrap(modeladmin.get_queryset) diff --git a/feincms/extensions/changedate.py b/feincms/extensions/changedate.py index cb21d288a..06370bcd9 100644 --- a/feincms/extensions/changedate.py +++ b/feincms/extensions/changedate.py @@ -1,20 +1,17 @@ # ------------------------------------------------------------------------ -# coding=utf-8 # ------------------------------------------------------------------------ """ Track the modification date for objects. """ -from __future__ import absolute_import, unicode_literals - -from email.utils import parsedate_tz, mktime_tz +from email.utils import mktime_tz, parsedate_tz from time import mktime from django.db import models from django.db.models.signals import pre_save from django.utils import timezone from django.utils.http import http_date -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from feincms import extensions @@ -38,10 +35,14 @@ def dt_to_utc_timestamp(dt): class Extension(extensions.Extension): def handle_model(self): - self.model.add_to_class('creation_date', models.DateTimeField( - _('creation date'), null=True, editable=False)) - self.model.add_to_class('modification_date', models.DateTimeField( - _('modification date'), null=True, editable=False)) + self.model.add_to_class( + "creation_date", + models.DateTimeField(_("creation date"), null=True, editable=False), + ) + self.model.add_to_class( + "modification_date", + models.DateTimeField(_("modification date"), null=True, editable=False), + ) self.model.last_modified = lambda p: p.modification_date @@ -51,16 +52,17 @@ def handle_model(self): # ------------------------------------------------------------------------ def last_modified_response_processor(page, request, response): # Don't include Last-Modified if we don't want to be cached - if "no-cache" in response.get('Cache-Control', ''): + if "no-cache" in response.get("Cache-Control", ""): return # If we already have a Last-Modified, take the later one last_modified = dt_to_utc_timestamp(page.last_modified()) - if response.has_header('Last-Modified'): + if response.has_header("Last-Modified"): last_modified = max( - last_modified, - mktime_tz(parsedate_tz(response['Last-Modified']))) + last_modified, mktime_tz(parsedate_tz(response["Last-Modified"])) + ) + + response["Last-Modified"] = http_date(last_modified) - response['Last-Modified'] = http_date(last_modified) # ------------------------------------------------------------------------ diff --git a/feincms/extensions/ct_tracker.py b/feincms/extensions/ct_tracker.py index 51308e5f8..8dd10ee88 100644 --- a/feincms/extensions/ct_tracker.py +++ b/feincms/extensions/ct_tracker.py @@ -1,6 +1,4 @@ # ------------------------------------------------------------------------ -# coding=utf-8 -# ------------------------------------------------------------------------ # # ct_tracker.py # FeinCMS @@ -17,11 +15,9 @@ saving time, thus saving at least one DB query on page delivery. """ -from __future__ import absolute_import, unicode_literals - from django.contrib.contenttypes.models import ContentType from django.db.models.signals import class_prepared, post_save, pre_save -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from feincms import extensions from feincms.contrib.fields import JSONField @@ -46,34 +42,36 @@ def _fetch_content_type_counts(self): empty _ct_inventory. """ - if 'counts' not in self._cache: - if (self.item._ct_inventory and - self.item._ct_inventory.get('_version_', -1) == - INVENTORY_VERSION): - + if "counts" not in self._cache: + if ( + self.item._ct_inventory + and self.item._ct_inventory.get("_version_", -1) == INVENTORY_VERSION + ): try: - self._cache['counts'] = self._from_inventory( - self.item._ct_inventory) + self._cache["counts"] = self._from_inventory( + self.item._ct_inventory + ) except KeyError: # It's possible that the inventory does not fit together # with the current models anymore, f.e. because a content # type has been removed. pass - if 'counts' not in self._cache: - super(TrackerContentProxy, self)._fetch_content_type_counts() + if "counts" not in self._cache: + super()._fetch_content_type_counts() - self.item._ct_inventory = self._to_inventory( - self._cache['counts']) + self.item._ct_inventory = self._to_inventory(self._cache["counts"]) self.item.__class__.objects.filter(id=self.item.id).update( - _ct_inventory=self.item._ct_inventory) + _ct_inventory=self.item._ct_inventory + ) # Run post save handler by hand - if hasattr(self.item, 'get_descendants'): + if hasattr(self.item, "get_descendants"): self.item.get_descendants(include_self=False).update( - _ct_inventory=None) - return self._cache['counts'] + _ct_inventory=None + ) + return self._cache["counts"] def _translation_map(self): cls = self.item.__class__ @@ -82,15 +80,18 @@ def _translation_map(self): # done late as opposed to at class definition time as not all # information is ready, especially when we are doing a "syncdb" the # ContentType table does not yet exist - map = {} + tmap = {} + model_to_contenttype = ContentType.objects.get_for_models( + *self.item._feincms_content_types + ) for idx, fct in enumerate(self.item._feincms_content_types): - dct = ContentType.objects.get_for_model(fct) + dct = model_to_contenttype[fct] # Rely on non-negative primary keys - map[-dct.id] = idx # From-inventory map - map[idx] = dct.id # To-inventory map + tmap[-dct.id] = idx # From-inventory map + tmap[idx] = dct.id # To-inventory map - _translation_map_cache[cls] = map + _translation_map_cache[cls] = tmap return _translation_map_cache[cls] def _from_inventory(self, inventory): @@ -99,22 +100,22 @@ def _from_inventory(self, inventory): ContentProxy counts format. """ - map = self._translation_map() + tmap = self._translation_map() - return dict((region, [ - (pk, map[-ct]) for pk, ct in items - ]) for region, items in inventory.items() if region != '_version_') + return { + region: [(pk, tmap[-ct]) for pk, ct in items] + for region, items in inventory.items() + if region != "_version_" + } def _to_inventory(self, counts): map = self._translation_map() - inventory = dict( - ( - region, - [(pk, map[ct]) for pk, ct in items], - ) for region, items in counts.items() - ) - inventory['_version_'] = INVENTORY_VERSION + inventory = { + region: [(pk, map[ct]) for pk, ct in items] + for region, items in counts.items() + } + inventory["_version_"] = INVENTORY_VERSION return inventory @@ -151,12 +152,15 @@ def single_pre_save_handler(sender, instance, **kwargs): # ------------------------------------------------------------------------ class Extension(extensions.Extension): def handle_model(self): - self.model.add_to_class('_ct_inventory', JSONField( - _('content types'), editable=False, blank=True, null=True)) + self.model.add_to_class( + "_ct_inventory", + JSONField(_("content types"), editable=False, blank=True, null=True), + ) self.model.content_proxy_class = TrackerContentProxy pre_save.connect(single_pre_save_handler, sender=self.model) - if hasattr(self.model, 'get_descendants'): + if hasattr(self.model, "get_descendants"): post_save.connect(tree_post_save_handler, sender=self.model) + # ------------------------------------------------------------------------ diff --git a/feincms/extensions/datepublisher.py b/feincms/extensions/datepublisher.py index 39c26f6d5..cdf2786a4 100644 --- a/feincms/extensions/datepublisher.py +++ b/feincms/extensions/datepublisher.py @@ -8,23 +8,21 @@ """ # ------------------------------------------------------------------------ -from __future__ import absolute_import, unicode_literals - from datetime import datetime -from pytz.exceptions import AmbiguousTimeError +import django from django.db import models from django.db.models import Q from django.utils import timezone from django.utils.cache import patch_response_headers from django.utils.html import mark_safe -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from feincms import extensions # ------------------------------------------------------------------------ -def format_date(d, if_none=''): +def format_date(d, if_none=""): """ Format a date in a nice human readable way: Omit the year if it's the current year. Also return a default value if no date is passed in. @@ -34,12 +32,12 @@ def format_date(d, if_none=''): return if_none now = timezone.now() - fmt = (d.year == now.year) and '%d.%m' or '%d.%m.%Y' + fmt = (d.year == now.year) and "%d.%m" or "%d.%m.%Y" return d.strftime(fmt) def latest_children(self): - return self.get_children().order_by('-publication_date') + return self.get_children().order_by("-publication_date") # ------------------------------------------------------------------------ @@ -54,26 +52,14 @@ def granular_now(n=None, default_tz=None): if n is None: n = timezone.now() if default_tz is None: - default_tz = n.tzinfo - - # Django 1.9: - # The correct way to resolve the AmbiguousTimeError every dst - # transition is... the is_dst parameter appeared with 1.9 - # make_aware(some_datetime, get_current_timezone(), is_dst=True) + default_tz = timezone.get_current_timezone() rounded_minute = (n.minute // 5) * 5 d = datetime(n.year, n.month, n.day, n.hour, rounded_minute) - try: - retval = timezone.make_aware(d, default_tz) - except AmbiguousTimeError: - try: - retval = timezone.make_aware(d, default_tz, is_dst=False) - except TypeError: # Pre-Django 1.9 - retval = timezone.make_aware( - datetime(n.year, n.month, n.day, n.hour + 1, rounded_minute), - default_tz) - - return retval + if django.VERSION < (5,): + return timezone.make_aware(d, default_tz, is_dst=True) + else: + return timezone.make_aware(d, default_tz) # ------------------------------------------------------------------------ @@ -95,16 +81,19 @@ def datepublisher_response_processor(page, request, response): class Extension(extensions.Extension): def handle_model(self): self.model.add_to_class( - 'publication_date', - models.DateTimeField(_('publication date'), default=granular_now)) + "publication_date", + models.DateTimeField(_("publication date"), default=granular_now), + ) self.model.add_to_class( - 'publication_end_date', + "publication_end_date", models.DateTimeField( - _('publication end date'), - blank=True, null=True, - help_text=_( - 'Leave empty if the entry should stay active forever.'))) - self.model.add_to_class('latest_children', latest_children) + _("publication end date"), + blank=True, + null=True, + help_text=_("Leave empty if the entry should stay active forever."), + ), + ) + self.model.add_to_class("latest_children", latest_children) # Patch in rounding the pub and pub_end dates on save orig_save = self.model.save @@ -113,44 +102,52 @@ def granular_save(obj, *args, **kwargs): if obj.publication_date: obj.publication_date = granular_now(obj.publication_date) if obj.publication_end_date: - obj.publication_end_date = granular_now( - obj.publication_end_date) + obj.publication_end_date = granular_now(obj.publication_end_date) orig_save(obj, *args, **kwargs) + self.model.save = granular_save # Append publication date active check - if hasattr(self.model._default_manager, 'add_to_active_filters'): + if hasattr(self.model._default_manager, "add_to_active_filters"): self.model._default_manager.add_to_active_filters( lambda queryset: queryset.filter( - Q(publication_date__lte=granular_now()) & - (Q(publication_end_date__isnull=True) | - Q(publication_end_date__gt=granular_now()))), - key='datepublisher', + Q(publication_date__lte=granular_now()) + & ( + Q(publication_end_date__isnull=True) + | Q(publication_end_date__gt=granular_now()) + ) + ), + key="datepublisher", ) # Processor to patch up response headers for expiry date - self.model.register_response_processor( - datepublisher_response_processor) + self.model.register_response_processor(datepublisher_response_processor) def handle_modeladmin(self, modeladmin): def datepublisher_admin(self, obj): - return mark_safe('%s – %s' % ( - format_date(obj.publication_date), - format_date(obj.publication_end_date, '∞'), - )) - datepublisher_admin.short_description = _('visible from - to') + return mark_safe( + "%s – %s" + % ( + format_date(obj.publication_date), + format_date(obj.publication_end_date, "∞"), + ) + ) + + datepublisher_admin.short_description = _("visible from - to") modeladmin.__class__.datepublisher_admin = datepublisher_admin try: - pos = modeladmin.list_display.index('is_visible_admin') + pos = modeladmin.list_display.index("is_visible_admin") except ValueError: pos = len(modeladmin.list_display) - modeladmin.list_display.insert(pos + 1, 'datepublisher_admin') + modeladmin.list_display.insert(pos + 1, "datepublisher_admin") + + modeladmin.add_extension_options( + _("Date-based publishing"), + {"fields": ["publication_date", "publication_end_date"]}, + ) - modeladmin.add_extension_options(_('Date-based publishing'), { - 'fields': ['publication_date', 'publication_end_date'], - }) # ------------------------------------------------------------------------ diff --git a/feincms/extensions/featured.py b/feincms/extensions/featured.py index 139edcdda..a15535571 100644 --- a/feincms/extensions/featured.py +++ b/feincms/extensions/featured.py @@ -2,10 +2,8 @@ Add a "featured" field to objects so admins can better direct top content. """ -from __future__ import absolute_import, unicode_literals - from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from feincms import extensions @@ -13,15 +11,10 @@ class Extension(extensions.Extension): def handle_model(self): self.model.add_to_class( - 'featured', - models.BooleanField( - _('featured'), - default=False, - ), + "featured", models.BooleanField(_("featured"), default=False) ) def handle_modeladmin(self, modeladmin): - modeladmin.add_extension_options(_('Featured'), { - 'fields': ('featured',), - 'classes': ('collapse',), - }) + modeladmin.add_extension_options( + _("Featured"), {"fields": ("featured",), "classes": ("collapse",)} + ) diff --git a/feincms/extensions/seo.py b/feincms/extensions/seo.py index f71d9f5f3..3698c00d5 100644 --- a/feincms/extensions/seo.py +++ b/feincms/extensions/seo.py @@ -2,34 +2,39 @@ Add a keyword and a description field which are helpful for SEO optimization. """ -from __future__ import absolute_import, unicode_literals - from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from feincms import extensions class Extension(extensions.Extension): def handle_model(self): - self.model.add_to_class('meta_keywords', models.TextField( - _('meta keywords'), - blank=True, - help_text=_('Keywords are ignored by most search engines.'))) - self.model.add_to_class('meta_description', models.TextField( - _('meta description'), - blank=True, - help_text=_('This text is displayed on the search results page. ' - 'It is however not used for the SEO ranking. ' - 'Text longer than 140 characters is truncated.'))) + self.model.add_to_class( + "meta_keywords", + models.TextField( + _("meta keywords"), + blank=True, + help_text=_("Keywords are ignored by most search engines."), + ), + ) + self.model.add_to_class( + "meta_description", + models.TextField( + _("meta description"), + blank=True, + help_text=_( + "This text is displayed on the search results page. " + "It is however not used for the SEO ranking. " + "Text longer than 140 characters is truncated." + ), + ), + ) def handle_modeladmin(self, modeladmin): - modeladmin.extend_list( - 'search_fields', - ['meta_keywords', 'meta_description'], - ) + modeladmin.extend_list("search_fields", ["meta_keywords", "meta_description"]) - modeladmin.add_extension_options(_('Search engine optimization'), { - 'fields': ('meta_keywords', 'meta_description'), - 'classes': ('collapse',), - }) + modeladmin.add_extension_options( + _("Search engine optimization"), + {"fields": ("meta_keywords", "meta_description"), "classes": ("collapse",)}, + ) diff --git a/feincms/extensions/translations.py b/feincms/extensions/translations.py index 4c37cda1d..7465b1072 100644 --- a/feincms/extensions/translations.py +++ b/feincms/extensions/translations.py @@ -1,5 +1,4 @@ # ------------------------------------------------------------------------ -# coding=utf-8 # ------------------------------------------------------------------------ """ @@ -15,43 +14,46 @@ as Django's administration tool. """ -from __future__ import absolute_import, unicode_literals - # ------------------------------------------------------------------------ import logging +from typing import Optional from django.conf import settings as django_settings from django.db import models -from django.http import HttpResponseRedirect +from django.http import HttpRequest, HttpResponseRedirect from django.utils import translation -from django.utils.html import mark_safe -from django.utils.translation import ugettext_lazy as _ +from django.utils.safestring import mark_safe +from django.utils.translation import gettext_lazy as _ from feincms import extensions, settings -from feincms.translations import is_primary_language from feincms._internal import monkeypatch_method, monkeypatch_property +from feincms.translations import is_primary_language # ------------------------------------------------------------------------ logger = logging.getLogger(__name__) -LANGUAGE_COOKIE_NAME = django_settings.LANGUAGE_COOKIE_NAME -if hasattr(translation, 'LANGUAGE_SESSION_KEY'): +LANGUAGE_COOKIE_NAME: str = django_settings.LANGUAGE_COOKIE_NAME +if hasattr(translation, "LANGUAGE_SESSION_KEY"): LANGUAGE_SESSION_KEY = translation.LANGUAGE_SESSION_KEY else: - # Django 1.6 + # stopgap measure for django >= 4, need to revisit this later LANGUAGE_SESSION_KEY = LANGUAGE_COOKIE_NAME +PRIMARY_LANGUAGE: str = django_settings.LANGUAGES[0][0] + # ------------------------------------------------------------------------ -def user_has_language_set(request): +def user_has_language_set(request: HttpRequest) -> bool: """ Determine whether the user has explicitely set a language earlier on. This is taken later on as an indication that we should not mess with the site's language settings, after all, the user's decision is what counts. """ - if (hasattr(request, 'session') and - request.session.get(LANGUAGE_SESSION_KEY) is not None): + if ( + hasattr(request, "session") + and request.session.get(LANGUAGE_SESSION_KEY) is not None + ): return True if LANGUAGE_COOKIE_NAME in request.COOKIES: return True @@ -59,18 +61,20 @@ def user_has_language_set(request): # ------------------------------------------------------------------------ -def translation_allowed_language(select_language): +def translation_allowed_language(select_language: str) -> str: "Check for feincms specific set of allowed front end languages." if settings.FEINCMS_FRONTEND_LANGUAGES: language = select_language[:2] if language not in settings.FEINCMS_FRONTEND_LANGUAGES: - select_language = django_settings.LANGUAGES[0][0] + select_language = PRIMARY_LANGUAGE return select_language # ------------------------------------------------------------------------ -def translation_set_language(request, select_language): +def translation_set_language( + request: HttpRequest, select_language: str +) -> Optional[HttpResponseRedirect]: """ Set and activate a language, if that language is available. """ @@ -85,25 +89,28 @@ def translation_set_language(request, select_language): # other messages and other applications. It is *highly* recommended to # create a new django.po for the language instead of # using this behaviour. - select_language = django_settings.LANGUAGES[0][0] + select_language = PRIMARY_LANGUAGE fallback = True translation.activate(select_language) - request.LANGUAGE_CODE = translation.get_language() + request.LANGUAGE_CODE = translation.get_language() # type: ignore - if hasattr(request, 'session'): + if hasattr(request, "session"): # User has a session, then set this language there - if select_language != request.session.get(LANGUAGE_SESSION_KEY): + current_session_language = request.session.get( + LANGUAGE_SESSION_KEY, PRIMARY_LANGUAGE + ) + + if select_language != current_session_language: request.session[LANGUAGE_SESSION_KEY] = select_language - elif request.method == 'GET' and not fallback: + elif request.method == "GET" and not fallback: # No session is active. We need to set a cookie for the language # so that it persists when users change their location to somewhere # not under the control of the CMS. # Only do this when request method is GET (mainly, do not abort # POST requests) response = HttpResponseRedirect(request.get_full_path()) - response.set_cookie( - str(LANGUAGE_COOKIE_NAME), select_language) + response.set_cookie(str(LANGUAGE_COOKIE_NAME), select_language, samesite="Lax") return response @@ -118,8 +125,8 @@ def translations_request_processor_explicit(page, request): desired_language = page.language # ...except if the user explicitely wants to switch language - if 'set_language' in request.GET: - desired_language = request.GET['set_language'] + if "set_language" in request.GET: + desired_language = request.GET["set_language"] # ...or the user already has explicitely set a language, bail out and # don't change it for them behind their back elif user_has_language_set(request): @@ -131,7 +138,7 @@ def translations_request_processor_explicit(page, request): # ------------------------------------------------------------------------ def translations_request_processor_standard(page, request): # If this page is just a redirect, don't do any language specific setup - if getattr(page, 'redirect_to', None): + if getattr(page, "redirect_to", None): return if page.language == translation.get_language(): @@ -142,51 +149,54 @@ def translations_request_processor_standard(page, request): # ------------------------------------------------------------------------ def get_current_language_code(request): - language_code = getattr(request, 'LANGUAGE_CODE', None) + language_code = getattr(request, "LANGUAGE_CODE", None) if language_code is None: logger.warning( "Could not access request.LANGUAGE_CODE. Is 'django.middleware." - "locale.LocaleMiddleware' in MIDDLEWARE_CLASSES?") + "locale.LocaleMiddleware' in MIDDLEWARE_CLASSES?" + ) return language_code # ------------------------------------------------------------------------ class Extension(extensions.Extension): - def handle_model(self): cls = self.model cls.add_to_class( - 'language', + "language", models.CharField( - _('language'), + _("language"), max_length=10, choices=django_settings.LANGUAGES, - default=django_settings.LANGUAGES[0][0])) + default=PRIMARY_LANGUAGE, + ), + ) cls.add_to_class( - 'translation_of', + "translation_of", models.ForeignKey( - 'self', + "self", on_delete=models.CASCADE, - blank=True, null=True, verbose_name=_('translation of'), - related_name='translations', - limit_choices_to={'language': django_settings.LANGUAGES[0][0]}, - help_text=_( - 'Leave this empty for entries in the primary language.'), - ) + blank=True, + null=True, + verbose_name=_("translation of"), + related_name="translations", + limit_choices_to={"language": PRIMARY_LANGUAGE}, + help_text=_("Leave this empty for entries in the primary language."), + ), ) - if hasattr(cls, 'register_request_processor'): + if hasattr(cls, "register_request_processor"): if settings.FEINCMS_TRANSLATION_POLICY == "EXPLICIT": cls.register_request_processor( - translations_request_processor_explicit, - key='translations') + translations_request_processor_explicit, key="translations" + ) else: # STANDARD cls.register_request_processor( - translations_request_processor_standard, - key='translations') + translations_request_processor_standard, key="translations" + ) - if hasattr(cls, 'get_redirect_to_target'): + if hasattr(cls, "get_redirect_to_target"): original_get_redirect_to_target = cls.get_redirect_to_target @monkeypatch_method(cls) @@ -199,7 +209,7 @@ def get_redirect_to_target(self, request=None): redirection. """ target = original_get_redirect_to_target(self, request) - if target and target.find('//') == -1: + if target and target.find("//") == -1: # Not an offsite link http://bla/blubb try: page = cls.objects.page_for_path(target) @@ -217,9 +227,10 @@ def available_translations(self): if not self.id: # New, unsaved pages have no translations return [] - if hasattr(cls.objects, 'apply_active_filters'): + if hasattr(cls.objects, "apply_active_filters"): filter_active = cls.objects.apply_active_filters else: + def filter_active(queryset): return queryset @@ -228,9 +239,10 @@ def filter_active(queryset): elif self.translation_of: # reuse prefetched queryset, do not filter it res = [ - t for t - in filter_active(self.translation_of.translations.all()) - if t.language != self.language] + t + for t in filter_active(self.translation_of.translations.all()) + if t.language != self.language + ] res.insert(0, self.translation_of) return res else: @@ -244,7 +256,10 @@ def get_original_translation(self, *args, **kwargs): return self.translation_of logger.debug( "Page pk=%d (%s) has no primary language translation (%s)", - self.pk, self.language, django_settings.LANGUAGES[0][0]) + self.pk, + self.language, + PRIMARY_LANGUAGE, + ) return self @monkeypatch_property(cls) @@ -253,12 +268,12 @@ def original_translation(self): @monkeypatch_method(cls) def get_translation(self, language): - return self.original_translation.translations.get( - language=language) + return self.original_translation.translations.get(language=language) def handle_modeladmin(self, modeladmin): extensions.prefetch_modeladmin_get_queryset( - modeladmin, 'translation_of__translations', 'translations') + modeladmin, "translation_of__translations", "translations" + ) def available_translations_admin(self, page): # Do not use available_translations() because we don't care @@ -268,10 +283,7 @@ def available_translations_admin(self, page): if page.translation_of: translations.append(page.translation_of) translations.extend(page.translation_of.translations.all()) - translations = { - p.language: p.id - for p in translations - } + translations = {p.language: p.id for p in translations} links = [] @@ -280,33 +292,30 @@ def available_translations_admin(self, page): continue if key in translations: - links.append('%s' % ( - translations[key], _('Edit translation'), key.upper())) + links.append( + '%s' + % (translations[key], _("Edit translation"), key.upper()) + ) else: links.append( '%s' % ( - page.id, - key, - _('Create translation'), - key.upper() - ) + '%s&language=%s" title="%s">%s' + % (page.id, key, _("Create translation"), key.upper()) ) - return mark_safe(' | '.join(links)) + return mark_safe(" | ".join(links)) - available_translations_admin.short_description = _('translations') - modeladmin.__class__.available_translations_admin =\ - available_translations_admin + available_translations_admin.short_description = _("translations") + modeladmin.__class__.available_translations_admin = available_translations_admin - if hasattr(modeladmin, 'add_extension_options'): - modeladmin.add_extension_options('language', 'translation_of') + if hasattr(modeladmin, "add_extension_options"): + modeladmin.add_extension_options("language", "translation_of") modeladmin.extend_list( - 'list_display', - ['language', 'available_translations_admin'], + "list_display", ["language", "available_translations_admin"] ) - modeladmin.extend_list('list_filter', ['language']) - modeladmin.extend_list('raw_id_fields', ['translation_of']) + modeladmin.extend_list("list_filter", ["language"]) + modeladmin.extend_list("raw_id_fields", ["translation_of"]) + # ------------------------------------------------------------------------ diff --git a/feincms/locale/pt/LC_MESSAGES/djangojs.po b/feincms/locale/pt/LC_MESSAGES/djangojs.po index 77851f198..f15fdb2b2 100644 --- a/feincms/locale/pt/LC_MESSAGES/djangojs.po +++ b/feincms/locale/pt/LC_MESSAGES/djangojs.po @@ -2,7 +2,7 @@ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. -# +# msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" diff --git a/feincms/management/commands/medialibrary_orphans.py b/feincms/management/commands/medialibrary_orphans.py index 7a27a56de..208d47213 100644 --- a/feincms/management/commands/medialibrary_orphans.py +++ b/feincms/management/commands/medialibrary_orphans.py @@ -1,22 +1,24 @@ -from __future__ import absolute_import, unicode_literals - import os -from django.core.management.base import NoArgsCommand -from django.utils.encoding import force_text +from django.conf import settings +from django.core.management.base import BaseCommand from feincms.module.medialibrary.models import MediaFile -class Command(NoArgsCommand): +class Command(BaseCommand): help = "Prints all orphaned files in the `media/medialibrary` folder" - def handle_noargs(self, **options): - mediafiles = list(MediaFile.objects.values_list('file', flat=True)) + def handle(self, **options): + mediafiles = list(MediaFile.objects.values_list("file", flat=True)) + + root_len = len(settings.MEDIA_ROOT) + medialib_path = os.path.join(settings.MEDIA_ROOT, "medialibrary") - # TODO make this smarter, and take MEDIA_ROOT into account - for base, dirs, files in os.walk('media/medialibrary'): + for base, dirs, files in os.walk(medialib_path): for f in files: - full = os.path.join(base[6:], f) - if force_text(full) not in mediafiles: - self.stdout.write(os.path.join(base, f)) + if base.startswith(settings.MEDIA_ROOT): + base = base[root_len:] + full = os.path.join(base, f) + if full not in mediafiles: + self.stdout.write(full) diff --git a/feincms/management/commands/medialibrary_to_filer.py b/feincms/management/commands/medialibrary_to_filer.py index ff34c8d41..de0961e01 100644 --- a/feincms/management/commands/medialibrary_to_filer.py +++ b/feincms/management/commands/medialibrary_to_filer.py @@ -1,47 +1,44 @@ -from __future__ import absolute_import, unicode_literals - -from django.core.files import File as DjangoFile -from django.core.management.base import NoArgsCommand from django.contrib.auth.models import User +from django.core.files import File as DjangoFile +from django.core.management.base import BaseCommand +from filer.models import File, Image from feincms.contents import FilerFileContent, FilerImageContent from feincms.module.medialibrary.contents import MediaFileContent from feincms.module.medialibrary.models import MediaFile from feincms.module.page.models import Page -from filer.models import File, Image - PageMediaFileContent = Page.content_type_for(MediaFileContent) PageFilerFileContent = Page.content_type_for(FilerFileContent) PageFilerImageContent = Page.content_type_for(FilerImageContent) -assert all(( - PageMediaFileContent, - PageFilerFileContent, - PageFilerImageContent)), 'Not all required models available' +assert all((PageMediaFileContent, PageFilerFileContent, PageFilerImageContent)), ( + "Not all required models available" +) -class Command(NoArgsCommand): +class Command(BaseCommand): help = "Migrate the medialibrary and contents to django-filer" - def handle_noargs(self, **options): - user = User.objects.order_by('pk')[0] + def handle(self, **options): + user = User.objects.order_by("pk")[0] count = MediaFile.objects.count() - for i, mediafile in enumerate(MediaFile.objects.order_by('pk')): - model = Image if mediafile.type == 'image' else File - content_model = PageFilerImageContent if mediafile.type == 'image' else PageFilerFileContent # noqa + for i, mediafile in enumerate(MediaFile.objects.order_by("pk")): + model = Image if mediafile.type == "image" else File + content_model = ( + PageFilerImageContent + if mediafile.type == "image" + else PageFilerFileContent + ) # noqa filerfile = model.objects.create( owner=user, original_filename=mediafile.file.name, - file=DjangoFile( - mediafile.file.file, - name=mediafile.file.name, - ), + file=DjangoFile(mediafile.file.file, name=mediafile.file.name), ) contents = PageMediaFileContent.objects.filter(mediafile=mediafile) @@ -58,6 +55,6 @@ def handle_noargs(self, **options): content.delete() if not i % 10: - self.stdout.write('%s / %s files\n' % (i, count)) + self.stdout.write(f"{i} / {count} files\n") - self.stdout.write('%s / %s files\n' % (count, count)) + self.stdout.write(f"{count} / {count} files\n") diff --git a/feincms/management/commands/rebuild_mptt.py b/feincms/management/commands/rebuild_mptt.py index 5e2b2c2cb..7c292758f 100644 --- a/feincms/management/commands/rebuild_mptt.py +++ b/feincms/management/commands/rebuild_mptt.py @@ -1,5 +1,4 @@ # ------------------------------------------------------------------------ -# coding=utf-8 # ------------------------------------------------------------------------ """ ``rebuild_mptt`` @@ -8,18 +7,17 @@ ``rebuild_mptt`` rebuilds your mptt pointers. Only use in emergencies. """ -from __future__ import absolute_import, unicode_literals - -from django.core.management.base import NoArgsCommand +from django.core.management.base import BaseCommand from feincms.module.page.models import Page -class Command(NoArgsCommand): - help = ( - "Run this manually to rebuild your mptt pointers. Only use in" - " emergencies.") +class Command(BaseCommand): + help = "Run this manually to rebuild your mptt pointers. Only use in emergencies." def handle_noargs(self, **options): + self.handle(**options) + + def handle(self, **options): self.stdout.write("Rebuilding MPTT pointers for Page") Page._tree_manager.rebuild() diff --git a/feincms/models.py b/feincms/models.py index c99c295cb..5e65b2475 100644 --- a/feincms/models.py +++ b/feincms/models.py @@ -2,32 +2,28 @@ This is the core of FeinCMS All models defined here are abstract, which means no tables are created in -the feincms\_ namespace. +the feincms namespace. """ -from __future__ import absolute_import, unicode_literals - -from collections import OrderedDict -from functools import reduce -import sys import operator +import sys import warnings +from collections import OrderedDict +from functools import reduce from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ImproperlyConfigured from django.db import connections, models from django.db.models import Q from django.forms.widgets import Media -from django.utils.encoding import force_text, python_2_unicode_compatible -from django.utils.translation import ugettext_lazy as _ +from django.utils.encoding import force_str +from django.utils.translation import gettext_lazy as _ -from feincms import ensure_completely_loaded from feincms.extensions import ExtensionsMixin -from feincms.utils import copy_model_instance +from feincms.utils import ChoicesCharField, copy_model_instance -@python_2_unicode_compatible -class Region(object): +class Region: """ This class represents a region inside a template. Example regions might be 'main' and 'sidebar'. @@ -36,11 +32,11 @@ class Region(object): def __init__(self, key, title, *args): self.key = key self.title = title - self.inherited = args and args[0] == 'inherited' or False + self.inherited = args and args[0] == "inherited" or False self._content_types = [] def __str__(self): - return force_text(self.title) + return force_str(self.title) @property def content_types(self): @@ -50,20 +46,17 @@ def content_types(self): """ return [ - (ct.__name__.lower(), ct._meta.verbose_name) - for ct in self._content_types + (ct.__name__.lower(), ct._meta.verbose_name) for ct in self._content_types ] -@python_2_unicode_compatible -class Template(object): +class Template: """ A template is a standard Django template which is used to render a CMS object, most commonly a page. """ - def __init__(self, title, path, regions, key=None, preview_image=None, - **kwargs): + def __init__(self, title, path, regions, key=None, preview_image=None, **kwargs): # The key is what will be stored in the database. If key is undefined # use the template path as fallback. if not key: @@ -73,10 +66,10 @@ def __init__(self, title, path, regions, key=None, preview_image=None, self.title = title self.path = path self.preview_image = preview_image - self.singleton = kwargs.get('singleton', False) - self.child_template = kwargs.get('child_template', None) - self.enforce_leaf = kwargs.get('enforce_leaf', False) - self.urlconf = kwargs.get('urlconf', None) + self.singleton = kwargs.get("singleton", False) + self.child_template = kwargs.get("child_template", None) + self.enforce_leaf = kwargs.get("enforce_leaf", False) + self.urlconf = kwargs.get("urlconf", None) def _make_region(data): if isinstance(data, Region): @@ -84,13 +77,13 @@ def _make_region(data): return Region(*data) self.regions = [_make_region(row) for row in regions] - self.regions_dict = dict((r.key, r) for r in self.regions) + self.regions_dict = {r.key: r for r in self.regions} def __str__(self): - return force_text(self.title) + return force_str(self.title) -class ContentProxy(object): +class ContentProxy: """ The ``ContentProxy`` is responsible for loading the content blocks for all regions (including content blocks in inherited regions) and assembling @@ -105,9 +98,7 @@ def __init__(self, item): item._needs_content_types() self.item = item self.db = item._state.db - self._cache = { - 'cts': {}, - } + self._cache = {"cts": {}} def _inherit_from(self): """ @@ -118,8 +109,7 @@ def _inherit_from(self): is good enough (tm) for pages. """ - return self.item.get_ancestors(ascending=True).values_list( - 'pk', flat=True) + return self.item.get_ancestors(ascending=True).values_list("pk", flat=True) def _fetch_content_type_counts(self): """ @@ -138,7 +128,7 @@ def _fetch_content_type_counts(self): } """ - if 'counts' not in self._cache: + if "counts" not in self._cache: counts = self._fetch_content_type_count_helper(self.item.pk) empty_inherited_regions = set() @@ -149,7 +139,8 @@ def _fetch_content_type_counts(self): if empty_inherited_regions: for parent in self._inherit_from(): parent_counts = self._fetch_content_type_count_helper( - parent, regions=tuple(empty_inherited_regions)) + parent, regions=tuple(empty_inherited_regions) + ) counts.update(parent_counts) for key in parent_counts.keys(): @@ -158,36 +149,34 @@ def _fetch_content_type_counts(self): if not empty_inherited_regions: break - self._cache['counts'] = counts - return self._cache['counts'] + self._cache["counts"] = counts + return self._cache["counts"] def _fetch_content_type_count_helper(self, pk, regions=None): - tmpl = [ - 'SELECT %d AS ct_idx, region, COUNT(id) FROM %s WHERE parent_id=%s' - ] + tmpl = ["SELECT %d AS ct_idx, region, COUNT(id) FROM %s WHERE parent_id=%s"] args = [] if regions: - tmpl.append( - 'AND region IN (' + ','.join(['%%s'] * len(regions)) + ')') + tmpl.append("AND region IN (" + ",".join(["%%s"] * len(regions)) + ")") args.extend(regions * len(self.item._feincms_content_types)) - tmpl.append('GROUP BY region') - tmpl = ' '.join(tmpl) - - sql = ' UNION '.join([ - tmpl % (idx, cls._meta.db_table, pk) - for idx, cls in enumerate(self.item._feincms_content_types) - ]) - sql = 'SELECT * FROM ( ' + sql + ' ) AS ct ORDER BY ct_idx' + tmpl.append("GROUP BY region") + tmpl = " ".join(tmpl) - cursor = connections[self.db].cursor() - cursor.execute(sql, args) + sql = " UNION ".join( + [ + tmpl % (idx, cls._meta.db_table, pk) + for idx, cls in enumerate(self.item._feincms_content_types) + ] + ) + sql = "SELECT * FROM ( " + sql + " ) AS ct ORDER BY ct_idx" _c = {} - for ct_idx, region, count in cursor.fetchall(): - if count: - _c.setdefault(region, []).append((pk, ct_idx)) + with connections[self.db].cursor() as cursor: + cursor.execute(sql, args) + for ct_idx, region, count in cursor.fetchall(): + if count: + _c.setdefault(region, []).append((pk, ct_idx)) return _c @@ -200,55 +189,61 @@ def _populate_content_type_caches(self, types): for region, counts in self._fetch_content_type_counts().items(): for pk, ct_idx in counts: counts_by_type.setdefault( - self.item._feincms_content_types[ct_idx], - [], + self.item._feincms_content_types[ct_idx], [] ).append((region, pk)) # Resolve abstract to concrete content types - content_types = ( - cls for cls in self.item._feincms_content_types - if issubclass(cls, tuple(types)) - ) + if types is self.item._feincms_content_types: + # If we come from _fetch_regions, we don't need to do + # any type resolving + content_types = self.item._feincms_content_types + else: + content_types = ( + cls + for cls in self.item._feincms_content_types + if issubclass(cls, tuple(types)) + ) for cls in content_types: counts = counts_by_type.get(cls) - if cls not in self._cache['cts']: + if cls not in self._cache["cts"]: if counts: - self._cache['cts'][cls] = list(cls.get_queryset(reduce( - operator.or_, - (Q(region=r[0], parent=r[1]) for r in counts) - ))) + self._cache["cts"][cls] = list( + cls.get_queryset().filter( + reduce( + operator.or_, + (Q(region=r[0], parent=r[1]) for r in counts), + ) + ) + ) else: - self._cache['cts'][cls] = [] + self._cache["cts"][cls] = [] # share this content proxy object between all content items # so that each can use obj.parent.content to determine its # relationship to its siblings, etc. - for cls, objects in self._cache['cts'].items(): + for objects in self._cache["cts"].values(): for obj in objects: - setattr(obj.parent, '_content_proxy', self) + setattr(obj.parent, "_content_proxy", self) def _fetch_regions(self): """ Fetches all content types and group content types into regions """ - if 'regions' not in self._cache: - self._populate_content_type_caches( - self.item._feincms_content_types) + if "regions" not in self._cache: + self._populate_content_type_caches(self.item._feincms_content_types) contents = {} - for cls, content_list in self._cache['cts'].items(): + for content_list in self._cache["cts"].values(): for instance in content_list: contents.setdefault(instance.region, []).append(instance) - self._cache['regions'] = dict( - ( - region, - sorted(instances, key=lambda c: c.ordering), - ) for region, instances in contents.items() - ) + self._cache["regions"] = { + region: sorted(instances, key=lambda c: c.ordering) + for region, instances in contents.items() + } - return self._cache['regions'] + return self._cache["regions"] def all_of_type(self, type_or_tuple): """ @@ -262,11 +257,11 @@ def all_of_type(self, type_or_tuple): """ content_list = [] - if not hasattr(type_or_tuple, '__iter__'): + if not hasattr(type_or_tuple, "__iter__"): type_or_tuple = (type_or_tuple,) self._populate_content_type_caches(type_or_tuple) - for type, contents in self._cache['cts'].items(): + for type, contents in self._cache["cts"].items(): if any(issubclass(type, t) for t in type_or_tuple): content_list.extend(contents) @@ -277,16 +272,17 @@ def _get_media(self): Collect the media files of all content types of the current object """ - if 'media' not in self._cache: + if "media" not in self._cache: media = Media() for contents in self._fetch_regions().values(): for content in contents: - if hasattr(content, 'media'): + if hasattr(content, "media"): media = media + content.media - self._cache['media'] = media - return self._cache['media'] + self._cache["media"] = media + return self._cache["media"] + media = property(_get_media) def __getattr__(self, attr): @@ -297,7 +293,7 @@ def __getattr__(self, attr): has the inherited flag set, this method will go up the ancestor chain until either some item contents have found or no ancestors are left. """ - if (attr.startswith('__')): + if attr.startswith("__"): raise AttributeError # Do not trigger loading of real content type models if not necessary @@ -337,15 +333,15 @@ def register_regions(cls, *regions): ) """ - if hasattr(cls, 'template'): + if hasattr(cls, "template"): warnings.warn( - 'Ignoring second call to register_regions.', - RuntimeWarning) + "Ignoring second call to register_regions.", RuntimeWarning + ) return # implicitly creates a dummy template object -- the item editor # depends on the presence of a template. - cls.template = Template('', '', regions) + cls.template = Template("", "", regions) cls._feincms_all_regions = cls.template.regions @classmethod @@ -374,7 +370,7 @@ def register_templates(cls, *templates): }) """ - if not hasattr(cls, '_feincms_templates'): + if not hasattr(cls, "_feincms_templates"): cls._feincms_templates = OrderedDict() cls.TEMPLATES_CHOICES = [] @@ -387,46 +383,46 @@ def register_templates(cls, *templates): instances[template.key] = template try: - field = next(iter( - field for field in cls._meta.local_fields - if field.name == 'template_key')) + field = next( + iter( + field + for field in cls._meta.local_fields + if field.name == "template_key" + ) + ) except (StopIteration,): cls.add_to_class( - 'template_key', - models.CharField(_('template'), max_length=255, choices=( - # Dummy choice to trick Django. Cannot be empty, - # otherwise admin.E023 happens. - ('__dummy', '__dummy'), - )) + "template_key", + ChoicesCharField( + _("template"), + max_length=255, + ), + ) + field = next( + iter( + field + for field in cls._meta.local_fields + if field.name == "template_key" + ) ) - field = next(iter( - field for field in cls._meta.local_fields - if field.name == 'template_key')) def _template(self): - ensure_completely_loaded() - try: return self._feincms_templates[self.template_key] except KeyError: # return first template as a fallback if the template # has changed in-between return self._feincms_templates[ - list(self._feincms_templates.keys())[0]] + list(self._feincms_templates.keys())[0] + ] cls.template = property(_template) cls.TEMPLATE_CHOICES = [ - (template_.key, template_.title,) + (template_.key, template_.title) for template_ in cls._feincms_templates.values() ] - try: - # noqa https://github.com/django/django/commit/80e3444eca045799cc40e50c92609e852a299d38 - # Django 1.9 uses this code - field.choices = cls.TEMPLATE_CHOICES - except AttributeError: - # Older versions of Django use that. - field._choices = cls.TEMPLATE_CHOICES + field.choices = cls.TEMPLATE_CHOICES field.default = cls.TEMPLATE_CHOICES[0][0] # Build a set of all regions used anywhere @@ -445,7 +441,7 @@ def content(self): ``content_proxy_class`` member variable. """ - if not hasattr(self, '_content_proxy'): + if not hasattr(self, "_content_proxy"): self._content_proxy = self.content_proxy_class(self) return self._content_proxy @@ -472,19 +468,18 @@ def _create_content_base(cls): class Meta: abstract = True app_label = cls._meta.app_label - ordering = ['ordering'] + ordering = ["ordering"] def __str__(self): - return ( - '%s, region=%s,' - ' ordering=%d>') % ( + return ("%s, region=%s, ordering=%d>") % ( self.__class__.__name__, self.pk, self.parent.__class__.__name__, self.parent.pk, self.parent, self.region, - self.ordering) + self.ordering, + ) def render(self, **kwargs): """ @@ -495,15 +490,18 @@ def render(self, **kwargs): time instead of adding region-specific render methods. """ - render_fn = getattr(self, 'render_%s' % self.region, None) + render_fn = getattr(self, "render_%s" % self.region, None) if render_fn: return render_fn(**kwargs) raise NotImplementedError - def get_queryset(cls, filter_args): - return cls.objects.select_related().filter(filter_args) + def get_queryset(cls, filter_args=None): + qs = cls.objects.select_related() + if filter_args is not None: + return qs.filter(filter_args) + return qs attrs = { # The basic content type is put into @@ -512,31 +510,31 @@ def get_queryset(cls, filter_args): # needs to know where a model comes # from, therefore we ensure that the # module is always known. - '__module__': cls.__module__, - '__str__': __str__, - 'render': render, - 'get_queryset': classmethod(get_queryset), - 'Meta': Meta, - 'parent': models.ForeignKey( - cls, related_name='%(class)s_set', - on_delete=models.CASCADE), - 'region': models.CharField(max_length=255), - 'ordering': models.IntegerField(_('ordering'), default=0), + "__module__": cls.__module__, + "__str__": __str__, + "render": render, + "get_queryset": classmethod(get_queryset), + "Meta": Meta, + "parent": models.ForeignKey( + cls, related_name="%(class)s_set", on_delete=models.CASCADE + ), + "region": models.CharField(max_length=255), + "ordering": models.IntegerField(_("ordering"), default=0), } # create content base type and save reference on CMS class - name = '_Internal%sContentTypeBase' % cls.__name__ + name = "_Internal%sContentTypeBase" % cls.__name__ if hasattr(sys.modules[cls.__module__], name): warnings.warn( - 'The class %s.%s has the same name as the class that ' - 'FeinCMS auto-generates based on %s.%s. To avoid database' - 'errors and import clashes, rename one of these classes.' + "The class %s.%s has the same name as the class that " + "FeinCMS auto-generates based on %s.%s. To avoid database" + "errors and import clashes, rename one of these classes." % (cls.__module__, name, cls.__module__, cls.__name__), - RuntimeWarning) + RuntimeWarning, + ) - cls._feincms_content_model = python_2_unicode_compatible( - type(str(name), (models.Model,), attrs)) + cls._feincms_content_model = type(str(name), (models.Model,), attrs) # list of concrete content types cls._feincms_content_types = [] @@ -557,23 +555,24 @@ def get_queryset(cls, filter_args): # list of item editor context processors, will be extended by # content types - if hasattr(cls, 'feincms_item_editor_context_processors'): + if hasattr(cls, "feincms_item_editor_context_processors"): cls.feincms_item_editor_context_processors = list( - cls.feincms_item_editor_context_processors) + cls.feincms_item_editor_context_processors + ) else: cls.feincms_item_editor_context_processors = [] # list of templates which should be included in the item editor, # will be extended by content types - if hasattr(cls, 'feincms_item_editor_includes'): + if hasattr(cls, "feincms_item_editor_includes"): cls.feincms_item_editor_includes = dict( - cls.feincms_item_editor_includes) + cls.feincms_item_editor_includes + ) else: cls.feincms_item_editor_includes = {} @classmethod - def create_content_type(cls, model, regions=None, class_name=None, - **kwargs): + def create_content_type(cls, model, regions=None, class_name=None, **kwargs): """ This is the method you'll use to create concrete content types. @@ -622,14 +621,19 @@ def create_content_type(cls, model, regions=None, class_name=None, # content types with the same class name because of related_name # clashes try: - getattr(cls, '%s_set' % class_name.lower()) + getattr(cls, "%s_set" % class_name.lower()) warnings.warn( - 'Cannot create content type using %s.%s for %s.%s,' - ' because %s_set is already taken.' % ( - model.__module__, class_name, - cls.__module__, cls.__name__, - class_name.lower()), - RuntimeWarning) + "Cannot create content type using %s.%s for %s.%s," + " because %s_set is already taken." + % ( + model.__module__, + class_name, + cls.__module__, + cls.__name__, + class_name.lower(), + ), + RuntimeWarning, + ) return except AttributeError: # everything ok @@ -637,16 +641,16 @@ def create_content_type(cls, model, regions=None, class_name=None, if not model._meta.abstract: raise ImproperlyConfigured( - 'Cannot create content type from' - ' non-abstract model (yet).') + "Cannot create content type from non-abstract model (yet)." + ) - if not hasattr(cls, '_feincms_content_model'): + if not hasattr(cls, "_feincms_content_model"): cls._create_content_base() feincms_content_base = cls._feincms_content_model class Meta(feincms_content_base.Meta): - db_table = '%s_%s' % (cls._meta.db_table, class_name.lower()) + db_table = f"{cls._meta.db_table}_{class_name.lower()}" verbose_name = model._meta.verbose_name verbose_name_plural = model._meta.verbose_name_plural permissions = model._meta.permissions @@ -659,26 +663,21 @@ class Meta(feincms_content_base.Meta): # content type may be used by several CMS # base models at the same time (f.e. in # the blog and the page module). - '__module__': cls.__module__, - 'Meta': Meta, + "__module__": cls.__module__, + "Meta": Meta, } - new_type = type( - str(class_name), - (model, feincms_content_base,), - attrs, - ) + new_type = type(str(class_name), (model, feincms_content_base), attrs) cls._feincms_content_types.append(new_type) - if hasattr(getattr(new_type, 'process', None), '__call__'): + if hasattr(getattr(new_type, "process", None), "__call__"): cls._feincms_content_types_with_process.append(new_type) - if hasattr(getattr(new_type, 'finalize', None), '__call__'): + if hasattr(getattr(new_type, "finalize", None), "__call__"): cls._feincms_content_types_with_finalize.append(new_type) # content types can be limited to a subset of regions if not regions: - regions = set([ - region.key for region in cls._feincms_all_regions]) + regions = {region.key for region in cls._feincms_all_regions} for region in cls._feincms_all_regions: if region.key in regions: @@ -689,7 +688,7 @@ class Meta(feincms_content_base.Meta): # f.e. for the update_rsscontent management command, which needs to # find all concrete RSSContent types, so that the RSS feeds can be # fetched - if not hasattr(model, '_feincms_content_models'): + if not hasattr(model, "_feincms_content_models"): model._feincms_content_models = [] model._feincms_content_models.append(new_type) @@ -699,36 +698,37 @@ class Meta(feincms_content_base.Meta): # Handle optgroup argument for grouping content types in the item # editor - optgroup = kwargs.pop('optgroup', None) + optgroup = kwargs.pop("optgroup", None) if optgroup: new_type.optgroup = optgroup # customization hook. - if hasattr(new_type, 'initialize_type'): + if hasattr(new_type, "initialize_type"): new_type.initialize_type(**kwargs) else: for k, v in kwargs.items(): setattr(new_type, k, v) # collect item editor context processors from the content type - if hasattr(model, 'feincms_item_editor_context_processors'): + if hasattr(model, "feincms_item_editor_context_processors"): cls.feincms_item_editor_context_processors.extend( - model.feincms_item_editor_context_processors) + model.feincms_item_editor_context_processors + ) # collect item editor includes from the content type - if hasattr(model, 'feincms_item_editor_includes'): + if hasattr(model, "feincms_item_editor_includes"): for key, incls in model.feincms_item_editor_includes.items(): - cls.feincms_item_editor_includes.setdefault( - key, set()).update(incls) + cls.feincms_item_editor_includes.setdefault(key, set()).update( + incls + ) - ensure_completely_loaded(force=True) return new_type @property def _django_content_type(self): - if not getattr(self, '_cached_django_content_type', None): - self.__class__._cached_django_content_type = ( - ContentType.objects.get_for_model(self)) + if not getattr(self, "_cached_django_content_type", None): + ct = ContentType.objects.get_for_model(self) + self.__class__._cached_django_content_type = ct return self.__class__._cached_django_content_type @classmethod @@ -740,8 +740,10 @@ def content_type_for(cls, model): concrete_type = Page.content_type_for(VideoContent) """ - if (not hasattr(cls, '_feincms_content_types') or - not cls._feincms_content_types): + if ( + not hasattr(cls, "_feincms_content_types") + or not cls._feincms_content_types + ): return None for type in cls._feincms_content_types: @@ -752,25 +754,23 @@ def content_type_for(cls, model): @classmethod def _needs_templates(cls): - ensure_completely_loaded() - # helper which can be used to ensure that either register_regions # or register_templates has been executed before proceeding - if not hasattr(cls, 'template'): + if not hasattr(cls, "template"): raise ImproperlyConfigured( - 'You need to register at least one' - ' template or one region on %s.' % cls.__name__) + "You need to register at least one" + " template or one region on %s." % cls.__name__ + ) @classmethod def _needs_content_types(cls): - ensure_completely_loaded() - # Check whether any content types have been created for this base # class - if not getattr(cls, '_feincms_content_types', None): + if not getattr(cls, "_feincms_content_types", None): raise ImproperlyConfigured( - 'You need to create at least one' - ' content type for the %s model.' % cls.__name__) + "You need to create at least one" + " content type for the %s model." % cls.__name__ + ) def copy_content_from(self, obj): """ @@ -781,8 +781,7 @@ def copy_content_from(self, obj): for cls in self._feincms_content_types: for content in cls.objects.filter(parent=obj): - new = copy_model_instance( - content, exclude=('id', 'parent')) + new = copy_model_instance(content, exclude=("id", "parent")) new.parent = self new.save() @@ -799,20 +798,20 @@ def replace_content_with(self, obj): self.copy_content_from(obj) @classmethod - def register_with_reversion(cls): + def register_with_reversion(cls, **kwargs): try: from reversion.revisions import register except ImportError: try: from reversion import register except ImportError: - raise EnvironmentError("django-reversion is not installed") + raise OSError("django-reversion is not installed") follow = [] for content_type in cls._feincms_content_types: - follow.append('%s_set' % content_type.__name__.lower()) - register(content_type) - register(cls, follow=follow) + follow.append("%s_set" % content_type.__name__.lower()) + register(content_type, **kwargs) + register(cls, follow=follow, **kwargs) return Base diff --git a/feincms/module/extensions/changedate.py b/feincms/module/extensions/changedate.py index dd543dbbc..286d144fb 100644 --- a/feincms/module/extensions/changedate.py +++ b/feincms/module/extensions/changedate.py @@ -1,11 +1,11 @@ # flake8: noqa -from __future__ import absolute_import, unicode_literals import warnings -from feincms.extensions.changedate import * warnings.warn( - 'Import %(name)s from feincms.extensions.%(name)s' % { - 'name': __name__.split('.')[-1], - }, DeprecationWarning, stacklevel=2) + "Import %(name)s from feincms.extensions.%(name)s" + % {"name": __name__.split(".")[-1]}, + DeprecationWarning, + stacklevel=2, +) diff --git a/feincms/module/extensions/ct_tracker.py b/feincms/module/extensions/ct_tracker.py index 7b94fd55f..286d144fb 100644 --- a/feincms/module/extensions/ct_tracker.py +++ b/feincms/module/extensions/ct_tracker.py @@ -1,11 +1,11 @@ # flake8: noqa -from __future__ import absolute_import, unicode_literals import warnings -from feincms.extensions.ct_tracker import * warnings.warn( - 'Import %(name)s from feincms.extensions.%(name)s' % { - 'name': __name__.split('.')[-1], - }, DeprecationWarning, stacklevel=2) + "Import %(name)s from feincms.extensions.%(name)s" + % {"name": __name__.split(".")[-1]}, + DeprecationWarning, + stacklevel=2, +) diff --git a/feincms/module/extensions/datepublisher.py b/feincms/module/extensions/datepublisher.py index 44fed5edb..286d144fb 100644 --- a/feincms/module/extensions/datepublisher.py +++ b/feincms/module/extensions/datepublisher.py @@ -1,11 +1,11 @@ # flake8: noqa -from __future__ import absolute_import, unicode_literals import warnings -from feincms.extensions.datepublisher import * warnings.warn( - 'Import %(name)s from feincms.extensions.%(name)s' % { - 'name': __name__.split('.')[-1], - }, DeprecationWarning, stacklevel=2) + "Import %(name)s from feincms.extensions.%(name)s" + % {"name": __name__.split(".")[-1]}, + DeprecationWarning, + stacklevel=2, +) diff --git a/feincms/module/extensions/featured.py b/feincms/module/extensions/featured.py index 05b59a87a..286d144fb 100644 --- a/feincms/module/extensions/featured.py +++ b/feincms/module/extensions/featured.py @@ -1,11 +1,11 @@ # flake8: noqa -from __future__ import absolute_import, unicode_literals import warnings -from feincms.extensions.featured import * warnings.warn( - 'Import %(name)s from feincms.extensions.%(name)s' % { - 'name': __name__.split('.')[-1], - }, DeprecationWarning, stacklevel=2) + "Import %(name)s from feincms.extensions.%(name)s" + % {"name": __name__.split(".")[-1]}, + DeprecationWarning, + stacklevel=2, +) diff --git a/feincms/module/extensions/seo.py b/feincms/module/extensions/seo.py index 8dc6add93..286d144fb 100644 --- a/feincms/module/extensions/seo.py +++ b/feincms/module/extensions/seo.py @@ -1,11 +1,11 @@ # flake8: noqa -from __future__ import absolute_import, unicode_literals import warnings -from feincms.extensions.seo import * warnings.warn( - 'Import %(name)s from feincms.extensions.%(name)s' % { - 'name': __name__.split('.')[-1], - }, DeprecationWarning, stacklevel=2) + "Import %(name)s from feincms.extensions.%(name)s" + % {"name": __name__.split(".")[-1]}, + DeprecationWarning, + stacklevel=2, +) diff --git a/feincms/module/extensions/translations.py b/feincms/module/extensions/translations.py index 7d8842186..286d144fb 100644 --- a/feincms/module/extensions/translations.py +++ b/feincms/module/extensions/translations.py @@ -1,11 +1,11 @@ # flake8: noqa -from __future__ import absolute_import, unicode_literals import warnings -from feincms.extensions.translations import * warnings.warn( - 'Import %(name)s from feincms.extensions.%(name)s' % { - 'name': __name__.split('.')[-1], - }, DeprecationWarning, stacklevel=2) + "Import %(name)s from feincms.extensions.%(name)s" + % {"name": __name__.split(".")[-1]}, + DeprecationWarning, + stacklevel=2, +) diff --git a/feincms/module/medialibrary/__init__.py b/feincms/module/medialibrary/__init__.py index fc7e02a00..bb74687b2 100644 --- a/feincms/module/medialibrary/__init__.py +++ b/feincms/module/medialibrary/__init__.py @@ -1,12 +1,11 @@ # ------------------------------------------------------------------------ -# coding=utf-8 # ------------------------------------------------------------------------ -from __future__ import absolute_import import logging + # ------------------------------------------------------------------------ -logger = logging.getLogger('feincms.medialibrary') +logger = logging.getLogger("feincms.medialibrary") # ------------------------------------------------------------------------ diff --git a/feincms/module/medialibrary/admin.py b/feincms/module/medialibrary/admin.py index 922450af9..b9fa8f33f 100644 --- a/feincms/module/medialibrary/admin.py +++ b/feincms/module/medialibrary/admin.py @@ -1,13 +1,12 @@ # ------------------------------------------------------------------------ -# coding=utf-8 # ------------------------------------------------------------------------ -from __future__ import absolute_import, unicode_literals from django.contrib import admin -from .models import Category, MediaFile from .modeladmins import CategoryAdmin, MediaFileAdmin +from .models import Category, MediaFile + # ------------------------------------------------------------------------ admin.site.register(Category, CategoryAdmin) diff --git a/feincms/module/medialibrary/contents.py b/feincms/module/medialibrary/contents.py index b5b2cd435..63a2153fc 100644 --- a/feincms/module/medialibrary/contents.py +++ b/feincms/module/medialibrary/contents.py @@ -1,18 +1,16 @@ -from __future__ import unicode_literals - from django.contrib import admin from django.core.exceptions import ImproperlyConfigured from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ -from feincms._internal import ct_render_to_string from feincms.admin.item_editor import FeinCMSInline from feincms.module.medialibrary.fields import ContentWithMediaFile +from feincms.utils.tuple import AutoRenderTuple class MediaFileContentInline(FeinCMSInline): - raw_id_fields = ('mediafile',) - radio_fields = {'type': admin.VERTICAL} + raw_id_fields = ("mediafile",) + radio_fields = {"type": admin.VERTICAL} class MediaFileContent(ContentWithMediaFile): @@ -44,36 +42,35 @@ class MediaFileContent(ContentWithMediaFile): class Meta: abstract = True - verbose_name = _('media file') - verbose_name_plural = _('media files') + verbose_name = _("media file") + verbose_name_plural = _("media files") @classmethod def initialize_type(cls, TYPE_CHOICES=None): if TYPE_CHOICES is None: raise ImproperlyConfigured( - 'You have to set TYPE_CHOICES when' - ' creating a %s' % cls.__name__) + "You have to set TYPE_CHOICES when creating a %s" % cls.__name__ + ) cls.add_to_class( - 'type', + "type", models.CharField( - _('type'), + _("type"), max_length=20, choices=TYPE_CHOICES, default=TYPE_CHOICES[0][0], - ) + ), ) def render(self, **kwargs): - return ct_render_to_string( - [ - 'content/mediafile/%s_%s.html' % ( - self.mediafile.type, self.type), - 'content/mediafile/%s.html' % self.mediafile.type, - 'content/mediafile/%s.html' % self.type, - 'content/mediafile/default.html', - ], - {'content': self}, - request=kwargs.get('request'), - context=kwargs.get('context'), + return AutoRenderTuple( + ( + [ + f"content/mediafile/{self.mediafile.type}_{self.type}.html", + "content/mediafile/%s.html" % self.mediafile.type, + "content/mediafile/%s.html" % self.type, + "content/mediafile/default.html", + ], + {"content": self}, + ) ) diff --git a/feincms/module/medialibrary/fields.py b/feincms/module/medialibrary/fields.py index 71d8073d9..0910ca540 100644 --- a/feincms/module/medialibrary/fields.py +++ b/feincms/module/medialibrary/fields.py @@ -1,24 +1,20 @@ # ------------------------------------------------------------------------ -# coding=utf-8 # ------------------------------------------------------------------------ -from __future__ import absolute_import, unicode_literals -from django.contrib.admin.widgets import AdminFileWidget -from django.contrib.admin.widgets import ForeignKeyRawIdWidget +from django.contrib.admin.widgets import AdminFileWidget, ForeignKeyRawIdWidget from django.db import models -from django.utils import six -from django.utils.html import escape -from django.utils.safestring import mark_safe -from django.utils.translation import ugettext_lazy as _ +from django.utils.html import escape, mark_safe +from django.utils.translation import gettext_lazy as _ from feincms.admin.item_editor import FeinCMSInline from feincms.utils import shorten_string + from .models import MediaFile from .thumbnail import admin_thumbnail -__all__ = ('MediaFileForeignKey', 'ContentWithMediaFile') +__all__ = ("MediaFileForeignKey", "ContentWithMediaFile") # ------------------------------------------------------------------------ @@ -26,23 +22,26 @@ class MediaFileForeignKeyRawIdWidget(ForeignKeyRawIdWidget): def __init__(self, original): self.__dict__ = original.__dict__ - def label_for_value(self, value): - key = self.rel.get_related_field().name + def label_and_url_for_value(self, value): + label, url = super().label_and_url_for_value(value) + key = "pk" try: - obj = self.rel.to._default_manager.using(self.db).get( - **{key: value}) - label = [' %s' % escape( - shorten_string(six.text_type(obj)))] + obj = ( + self.rel.model._default_manager.using(self.db) + .filter(**{key: value}) + .first() + ) + label = [" %s" % escape(shorten_string(str(obj)))] image = admin_thumbnail(obj) if image: label.append( - '
' % image) + '
' % image + ) - return ''.join(label) - except (ValueError, self.rel.to.DoesNotExist): - return '' + return mark_safe("".join(label)), url + except (ValueError, self.rel.model.DoesNotExist): + return label, url class MediaFileForeignKey(models.ForeignKey): @@ -53,24 +52,25 @@ class MediaFileForeignKey(models.ForeignKey): """ def __init__(self, *args, **kwargs): - if not args and 'to' not in kwargs: + if not args and "to" not in kwargs: args = (MediaFile,) - super(MediaFileForeignKey, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def formfield(self, **kwargs): - if 'widget' in kwargs and isinstance( - kwargs['widget'], ForeignKeyRawIdWidget): - kwargs['widget'] = MediaFileForeignKeyRawIdWidget(kwargs['widget']) - return super(MediaFileForeignKey, self).formfield(**kwargs) + if "widget" in kwargs and isinstance(kwargs["widget"], ForeignKeyRawIdWidget): + kwargs["widget"] = MediaFileForeignKeyRawIdWidget(kwargs["widget"]) + return super().formfield(**kwargs) class ContentWithMediaFile(models.Model): class feincms_item_editor_inline(FeinCMSInline): - raw_id_fields = ('mediafile',) + raw_id_fields = ("mediafile",) mediafile = MediaFileForeignKey( - MediaFile, verbose_name=_('media file'), related_name='+', - on_delete=models.PROTECT + MediaFile, + verbose_name=_("media file"), + related_name="+", + on_delete=models.PROTECT, ) class Meta: @@ -83,16 +83,20 @@ class AdminFileWithPreviewWidget(AdminFileWidget): Simple AdminFileWidget, but detects if the file is an image and tries to render a small thumbnail besides the input field. """ - def render(self, name, value, attrs=None): - r = super(AdminFileWithPreviewWidget, self).render( - name, value, attrs=attrs) - if value and getattr(value, 'instance', None): + def render(self, name, value, attrs=None, *args, **kwargs): + r = super().render(name, value, attrs=attrs, *args, **kwargs) + + if value and getattr(value, "instance", None): image = admin_thumbnail(value.instance) if image: - r = mark_safe(( - '' % image) + r) + r = mark_safe( + ( + '" % image + ) + + r + ) return r diff --git a/feincms/module/medialibrary/forms.py b/feincms/module/medialibrary/forms.py index e592bd591..c2f094322 100644 --- a/feincms/module/medialibrary/forms.py +++ b/feincms/module/medialibrary/forms.py @@ -1,60 +1,60 @@ # ------------------------------------------------------------------------ -# coding=utf-8 # ------------------------------------------------------------------------ -from __future__ import absolute_import, unicode_literals import os from django import forms -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from feincms import settings from . import logger -from .models import Category, MediaFile from .fields import AdminFileWithPreviewWidget +from .models import Category, MediaFile # ------------------------------------------------------------------------ class MediaCategoryAdminForm(forms.ModelForm): class Meta: model = Category - fields = '__all__' + fields = "__all__" def clean_parent(self): - data = self.cleaned_data['parent'] + data = self.cleaned_data["parent"] if data is not None and self.instance in data.path_list(): - raise forms.ValidationError( - _("This would create a loop in the hierarchy")) + raise forms.ValidationError(_("This would create a loop in the hierarchy")) return data def __init__(self, *args, **kwargs): - super(MediaCategoryAdminForm, self).__init__(*args, **kwargs) - self.fields['parent'].queryset =\ - self.fields['parent'].queryset.exclude(pk=self.instance.pk) + super().__init__(*args, **kwargs) + self.fields["parent"].queryset = self.fields["parent"].queryset.exclude( + pk=self.instance.pk + ) # ------------------------------------------------------------------------ class MediaFileAdminForm(forms.ModelForm): class Meta: model = MediaFile - widgets = {'file': AdminFileWithPreviewWidget} - fields = '__all__' + widgets = {"file": AdminFileWithPreviewWidget} + fields = "__all__" def __init__(self, *args, **kwargs): - super(MediaFileAdminForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) if settings.FEINCMS_MEDIAFILE_OVERWRITE and self.instance.id: field = self.instance.file.field - if not hasattr(field, '_feincms_generate_filename_patched'): + if not hasattr(field, "_feincms_generate_filename_patched"): original_generate = field.generate_filename def _gen_fname(instance, filename): - if instance.id and hasattr(instance, 'original_name'): - logger.info("Overwriting file %s with new data" % ( - instance.original_name)) + if instance.id and hasattr(instance, "original_name"): + logger.info( + "Overwriting file %s with new data" + % (instance.original_name) + ) instance.file.storage.delete(instance.original_name) return instance.original_name @@ -65,18 +65,21 @@ def _gen_fname(instance, filename): def clean_file(self): if settings.FEINCMS_MEDIAFILE_OVERWRITE and self.instance.id: - new_base, new_ext = os.path.splitext( - self.cleaned_data['file'].name) + new_base, new_ext = os.path.splitext(self.cleaned_data["file"].name) old_base, old_ext = os.path.splitext(self.instance.file.name) if new_ext.lower() != old_ext.lower(): - raise forms.ValidationError(_( - "Cannot overwrite with different file type (attempt to" - " overwrite a %(old_ext)s with a %(new_ext)s)" - ) % {'old_ext': old_ext, 'new_ext': new_ext}) + raise forms.ValidationError( + _( + "Cannot overwrite with different file type (attempt to" + " overwrite a %(old_ext)s with a %(new_ext)s)" + ) + % {"old_ext": old_ext, "new_ext": new_ext} + ) self.instance.original_name = self.instance.file.name - return self.cleaned_data['file'] + return self.cleaned_data["file"] + # ------------------------------------------------------------------------ diff --git a/feincms/module/medialibrary/modeladmins.py b/feincms/module/medialibrary/modeladmins.py index d52e9fb2c..721b97c04 100644 --- a/feincms/module/medialibrary/modeladmins.py +++ b/feincms/module/medialibrary/modeladmins.py @@ -1,35 +1,30 @@ # ------------------------------------------------------------------------ -# coding=utf-8 # ------------------------------------------------------------------------ -from __future__ import absolute_import, unicode_literals import os from django import forms from django.conf import settings as django_settings -from django.contrib import admin -from django.contrib import messages +from django.contrib import admin, messages +from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME from django.contrib.auth.decorators import permission_required from django.contrib.sites.shortcuts import get_current_site from django.core.files.images import get_image_dimensions from django.http import HttpResponseRedirect from django.shortcuts import render from django.template.defaultfilters import filesizeformat +from django.urls import reverse from django.utils.safestring import mark_safe -from django.utils.translation import ungettext, ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _, ngettext from django.views.decorators.csrf import csrf_protect -try: - from django.urls import reverse -except ImportError: - from django.core.urlresolvers import reverse from feincms.extensions import ExtensionModelAdmin from feincms.translations import admin_translationinline, lookup_translations from feincms.utils import shorten_string -from .models import Category, MediaFileTranslation from .forms import MediaCategoryAdminForm, MediaFileAdminForm +from .models import Category, MediaFileTranslation from .thumbnail import admin_thumbnail from .zip import import_zipfile @@ -37,56 +32,55 @@ # ----------------------------------------------------------------------- class CategoryAdmin(admin.ModelAdmin): form = MediaCategoryAdminForm - list_display = ['path'] - list_filter = ['parent'] + list_display = ["path"] + list_filter = ["parent"] list_per_page = 25 - search_fields = ['title'] - prepopulated_fields = {'slug': ('title',)} + search_fields = ["title"] + prepopulated_fields = {"slug": ("title",)} # ------------------------------------------------------------------------ +@admin.action(description=_("Add selected media files to category")) def assign_category(modeladmin, request, queryset): class AddCategoryForm(forms.Form): _selected_action = forms.CharField(widget=forms.MultipleHiddenInput) category = forms.ModelChoiceField(Category.objects.all()) form = None - if 'apply' in request.POST: + if "apply" in request.POST: form = AddCategoryForm(request.POST) if form.is_valid(): - category = form.cleaned_data['category'] + category = form.cleaned_data["category"] count = 0 for mediafile in queryset: category.mediafile_set.add(mediafile) count += 1 - message = ungettext( - 'Successfully added %(count)d media file to %(category)s.', - 'Successfully added %(count)d media files to %(category)s.', - count) % {'count': count, 'category': category} + message = ngettext( + "Successfully added %(count)d media file to %(category)s.", + "Successfully added %(count)d media files to %(category)s.", + count, + ) % {"count": count, "category": category} modeladmin.message_user(request, message) return HttpResponseRedirect(request.get_full_path()) - if 'cancel' in request.POST: + if "cancel" in request.POST: return HttpResponseRedirect(request.get_full_path()) if not form: - form = AddCategoryForm(initial={ - '_selected_action': request.POST.getlist( - admin.ACTION_CHECKBOX_NAME), - }) - - return render(request, 'admin/medialibrary/add_to_category.html', { - 'mediafiles': queryset, - 'category_form': form, - 'opts': modeladmin.model._meta, - }) + form = AddCategoryForm( + initial={"_selected_action": request.POST.getlist(ACTION_CHECKBOX_NAME)} + ) - -assign_category.short_description = _('Add selected media files to category') + return render( + request, + "admin/medialibrary/add_to_category.html", + {"mediafiles": queryset, "category_form": form, "opts": modeladmin.model._meta}, + ) # ------------------------------------------------------------------------- +@admin.action(description=_("Export selected media files as zip file")) def save_as_zipfile(modeladmin, request, queryset): from .zip import export_zipfile @@ -98,12 +92,7 @@ def save_as_zipfile(modeladmin, request, queryset): messages.error(request, _("ZIP file export failed: %s") % str(e)) return - return HttpResponseRedirect( - os.path.join(django_settings.MEDIA_URL, zip_name)) - - -save_as_zipfile.short_description = _( - 'Export selected media files as zip file') + return HttpResponseRedirect(os.path.join(django_settings.MEDIA_URL, zip_name)) # ------------------------------------------------------------------------ @@ -111,62 +100,68 @@ class MediaFileAdmin(ExtensionModelAdmin): form = MediaFileAdminForm save_on_top = True - date_hierarchy = 'created' + date_hierarchy = "created" inlines = [admin_translationinline(MediaFileTranslation)] - list_display = [ - 'admin_thumbnail', '__str__', 'file_info', 'formatted_created'] - list_display_links = ['__str__'] - list_filter = ['type', 'categories'] + list_display = ["admin_thumbnail", "__str__", "file_info", "formatted_created"] + list_display_links = ["__str__"] + list_filter = ["type", "categories"] list_per_page = 25 - search_fields = ['copyright', 'file', 'translations__caption'] + search_fields = ["copyright", "file", "translations__caption"] filter_horizontal = ("categories",) actions = [assign_category, save_as_zipfile] def get_urls(self): - from django.conf.urls import url + from django.urls import path return [ - url( - r'^mediafile-bulk-upload/$', + path( + "mediafile-bulk-upload/", self.admin_site.admin_view(MediaFileAdmin.bulk_upload), {}, - name='mediafile_bulk_upload', - ), - ] + super(MediaFileAdmin, self).get_urls() + name="mediafile_bulk_upload", + ) + ] + super().get_urls() def changelist_view(self, request, extra_context=None): if extra_context is None: extra_context = {} - extra_context['categories'] = Category.objects.order_by('title') - return super(MediaFileAdmin, self).changelist_view( - request, extra_context=extra_context) + extra_context["categories"] = Category.objects.order_by("title") + return super().changelist_view(request, extra_context=extra_context) + @admin.display(description=_("Preview")) def admin_thumbnail(self, obj): image = admin_thumbnail(obj) if image: - return mark_safe(""" + return mark_safe( + """ - """ % { - 'url': obj.file.url, - 'image': image} + """ + % {"url": obj.file.url, "image": image} ) - return '' - admin_thumbnail.short_description = _('Preview') + return "" + @admin.display( + description=_("file size"), + ordering="file_size", + ) def formatted_file_size(self, obj): return filesizeformat(obj.file_size) - formatted_file_size.short_description = _("file size") - formatted_file_size.admin_order_field = 'file_size' + @admin.display( + description=_("created"), + ordering="created", + ) def formatted_created(self, obj): return obj.created.strftime("%Y-%m-%d") - formatted_created.short_description = _("created") - formatted_created.admin_order_field = 'created' + @admin.display( + description=_("file type"), + ordering="type", + ) def file_type(self, obj): t = obj.filetypes_dict[obj.type] - if obj.type == 'image': + if obj.type == "image": # get_image_dimensions is expensive / slow if the storage is not # local filesystem (indicated by availability the path property) try: @@ -174,15 +169,17 @@ def file_type(self, obj): except NotImplementedError: return t try: - d = get_image_dimensions(obj.file.file) + d = get_image_dimensions(obj.file.file, close=True) if d: t += " %d×%d" % (d[0], d[1]) - except (IOError, TypeError, ValueError) as e: + except (OSError, TypeError, ValueError) as e: t += " (%s)" % e return mark_safe(t) - file_type.admin_order_field = 'type' - file_type.short_description = _('file type') + @admin.display( + description=_("file info"), + ordering="file", + ) def file_info(self, obj): """ Method for showing the file name in admin. @@ -191,47 +188,48 @@ def file_info(self, obj): the file name later on, this can be used to access the file name from JS, like for example a TinyMCE connector shim. """ - return mark_safe(( - '' - ' %s
%s, %s' - ) % ( - obj.id, - obj.file.name, - obj.id, - shorten_string(os.path.basename(obj.file.name), max_length=40), - self.file_type(obj), - self.formatted_file_size(obj), - )) - file_info.admin_order_field = 'file' - file_info.short_description = _('file info') + return mark_safe( + ( + '' + " %s
%s, %s" + ) + % ( + obj.id, + obj.file.name, + obj.id, + shorten_string(os.path.basename(obj.file.name), max_length=40), + self.file_type(obj), + self.formatted_file_size(obj), + ) + ) @staticmethod @csrf_protect - @permission_required('medialibrary.add_mediafile') + @permission_required("medialibrary.add_mediafile") def bulk_upload(request): - if request.method == 'POST' and 'data' in request.FILES: + if request.method == "POST" and "data" in request.FILES: try: count = import_zipfile( - request.POST.get('category'), - request.POST.get('overwrite', False), - request.FILES['data']) + request.POST.get("category"), + request.POST.get("overwrite", False), + request.FILES["data"], + ) messages.info(request, _("%d files imported") % count) except Exception as e: messages.error(request, _("ZIP import failed: %s") % e) else: messages.error(request, _("No input file given")) - return HttpResponseRedirect( - reverse('admin:medialibrary_mediafile_changelist')) + return HttpResponseRedirect(reverse("admin:medialibrary_mediafile_changelist")) def get_queryset(self, request): - return super(MediaFileAdmin, self).get_queryset(request).transform( - lookup_translations()) + return super().get_queryset(request).transform(lookup_translations()) def save_model(self, request, obj, form, change): - obj.purge_translation_cache() - return super(MediaFileAdmin, self).save_model( - request, obj, form, change) + if obj.id: + obj.purge_translation_cache() + return super().save_model(request, obj, form, change) + # ------------------------------------------------------------------------ diff --git a/feincms/module/medialibrary/models.py b/feincms/module/medialibrary/models.py index 66fd1d069..056eb2852 100644 --- a/feincms/module/medialibrary/models.py +++ b/feincms/module/medialibrary/models.py @@ -1,8 +1,6 @@ # ------------------------------------------------------------------------ -# coding=utf-8 # ------------------------------------------------------------------------ -from __future__ import absolute_import, unicode_literals import os import re @@ -12,13 +10,15 @@ from django.dispatch.dispatcher import receiver from django.template.defaultfilters import slugify from django.utils import timezone -from django.utils.encoding import python_2_unicode_compatible -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from feincms import settings from feincms.models import ExtensionsMixin from feincms.translations import ( - TranslatedObjectMixin, Translation, TranslatedObjectManager) + TranslatedObjectManager, + TranslatedObjectMixin, + Translation, +) from . import logger @@ -29,39 +29,42 @@ class CategoryManager(models.Manager): Simple manager which exists only to supply ``.select_related("parent")`` on querysets since we can't even __str__ efficiently without it. """ + def get_queryset(self): - return super(CategoryManager, self).get_queryset().select_related( - "parent") + return super().get_queryset().select_related("parent") # ------------------------------------------------------------------------ -@python_2_unicode_compatible class Category(models.Model): """ These categories are meant primarily for organizing media files in the library. """ - title = models.CharField(_('title'), max_length=200) + title = models.CharField(_("title"), max_length=200) parent = models.ForeignKey( - 'self', blank=True, null=True, + "self", + blank=True, + null=True, on_delete=models.CASCADE, - related_name='children', limit_choices_to={'parent__isnull': True}, - verbose_name=_('parent')) + related_name="children", + limit_choices_to={"parent__isnull": True}, + verbose_name=_("parent"), + ) - slug = models.SlugField(_('slug'), max_length=150) + slug = models.SlugField(_("slug"), max_length=150) class Meta: - ordering = ['parent__title', 'title'] - verbose_name = _('category') - verbose_name_plural = _('categories') - app_label = 'medialibrary' + ordering = ["parent__title", "title"] + verbose_name = _("category") + verbose_name_plural = _("categories") + app_label = "medialibrary" objects = CategoryManager() def __str__(self): if self.parent_id: - return '%s - %s' % (self.parent.title, self.title) + return f"{self.parent.title} - {self.title}" return self.title @@ -69,7 +72,8 @@ def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.title) - super(Category, self).save(*args, **kwargs) + super().save(*args, **kwargs) + save.alters_data = True def path_list(self): @@ -80,11 +84,10 @@ def path_list(self): return p def path(self): - return ' - '.join((f.title for f in self.path_list())) + return " - ".join(f.title for f in self.path_list()) # ------------------------------------------------------------------------ -@python_2_unicode_compatible class MediaFileBase(models.Model, ExtensionsMixin, TranslatedObjectMixin): """ Abstract media file class. Includes the @@ -93,26 +96,25 @@ class MediaFileBase(models.Model, ExtensionsMixin, TranslatedObjectMixin): """ file = models.FileField( - _('file'), max_length=255, - upload_to=settings.FEINCMS_MEDIALIBRARY_UPLOAD_TO) - type = models.CharField( - _('file type'), max_length=12, editable=False, - choices=()) - created = models.DateTimeField( - _('created'), editable=False, default=timezone.now) - copyright = models.CharField(_('copyright'), max_length=200, blank=True) + _("file"), max_length=255, upload_to=settings.FEINCMS_MEDIALIBRARY_UPLOAD_TO + ) + type = models.CharField(_("file type"), max_length=12, editable=False, choices=()) + created = models.DateTimeField(_("created"), editable=False, default=timezone.now) + copyright = models.CharField(_("copyright"), max_length=200, blank=True) file_size = models.IntegerField( - _("file size"), blank=True, null=True, editable=False) + _("file size"), blank=True, null=True, editable=False + ) categories = models.ManyToManyField( - Category, verbose_name=_('categories'), blank=True) + Category, verbose_name=_("categories"), blank=True + ) categories.category_filter = True class Meta: abstract = True - ordering = ['-created'] - verbose_name = _('media file') - verbose_name_plural = _('media files') + ordering = ["-created"] + verbose_name = _("media file") + verbose_name_plural = _("media files") objects = TranslatedObjectManager() @@ -121,7 +123,7 @@ class Meta: @classmethod def reconfigure(cls, upload_to=None, storage=None): - f = cls._meta.get_field('file') + f = cls._meta.get_field("file") # Ugh. Copied relevant parts from django/db/models/fields/files.py # FileField.__init__ (around line 225) if storage: @@ -136,27 +138,29 @@ def register_filetypes(cls, *types): cls.filetypes[0:0] = types choices = [t[0:2] for t in cls.filetypes] cls.filetypes_dict = dict(choices) - cls._meta.get_field('type').choices[:] = choices + cls._meta.get_field("type").choices = choices def __init__(self, *args, **kwargs): - super(MediaFileBase, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) if self.file: self._original_file_name = self.file.name def __str__(self): - trans = None + if settings.FEINCMS_MEDIAFILE_TRANSLATIONS: + trans = None + + try: + trans = self.translation + except models.ObjectDoesNotExist: + pass + except AttributeError: + pass + + if trans: + trans = "%s" % trans + if trans.strip(): + return trans - try: - trans = self.translation - except models.ObjectDoesNotExist: - pass - except AttributeError: - pass - - if trans: - trans = '%s' % trans - if trans.strip(): - return trans return os.path.basename(self.file.name) def get_absolute_url(self): @@ -164,16 +168,15 @@ def get_absolute_url(self): def determine_file_type(self, name): """ - >>> t = MediaFileBase() - >>> str(t.determine_file_type('foobar.jpg')) + >>> str(MediaFile().determine_file_type('foobar.jpg')) 'image' - >>> str(t.determine_file_type('foobar.PDF')) + >>> str(MediaFile().determine_file_type('foobar.PDF')) 'pdf' - >>> str(t.determine_file_type('foobar.jpg.pdf')) + >>> str(MediaFile().determine_file_type('foobar.jpg.pdf')) 'pdf' - >>> str(t.determine_file_type('foobar.jgp')) + >>> str(MediaFile().determine_file_type('foobar.jgp')) 'other' - >>> str(t.determine_file_type('foobar-jpg')) + >>> str(MediaFile().determine_file_type('foobar-jpg')) 'other' """ for type_key, type_name, type_test in self.filetypes: @@ -189,21 +192,24 @@ def save(self, *args, **kwargs): if self.file: try: self.file_size = self.file.size - except (OSError, IOError, ValueError) as e: - logger.error("Unable to read file size for %s: %s" % (self, e)) + except (OSError, ValueError) as e: + logger.error(f"Unable to read file size for {self}: {e}") - super(MediaFileBase, self).save(*args, **kwargs) + super().save(*args, **kwargs) - logger.info("Saved mediafile %d (%s, type %s, %d bytes)" % ( - self.id, self.file.name, self.type, self.file_size or 0)) + logger.info( + "Saved mediafile %d (%s, type %s, %d bytes)" + % (self.id, self.file.name, self.type, self.file_size or 0) + ) # User uploaded a new file. Try to get rid of the old file in # storage, to avoid having orphaned files hanging around. - if getattr(self, '_original_file_name', None): + if getattr(self, "_original_file_name", None): if self.file.name != self._original_file_name: self.delete_mediafile(self._original_file_name) self.purge_translation_cache() + save.alters_data = True def delete_mediafile(self, name=None): @@ -212,62 +218,83 @@ def delete_mediafile(self, name=None): try: self.file.storage.delete(name) except Exception as e: - logger.warn("Cannot delete media file %s: %s" % (name, e)) + logger.warn(f"Cannot delete media file {name}: {e}") # ------------------------------------------------------------------------ MediaFileBase.register_filetypes( # Should we be using imghdr.what instead of extension guessing? - ('image', _('Image'), lambda f: re.compile( - r'\.(bmp|jpe?g|jp2|jxr|gif|png|tiff?)$', re.IGNORECASE).search(f)), - ('video', _('Video'), lambda f: re.compile( - r'\.(mov|m[14]v|mp4|avi|mpe?g|qt|ogv|wmv|flv)$', - re.IGNORECASE).search(f)), - ('audio', _('Audio'), lambda f: re.compile( - r'\.(au|mp3|m4a|wma|oga|ram|wav)$', re.IGNORECASE).search(f)), - ('pdf', _('PDF document'), lambda f: f.lower().endswith('.pdf')), - ('swf', _('Flash'), lambda f: f.lower().endswith('.swf')), - ('txt', _('Text'), lambda f: f.lower().endswith('.txt')), - ('rtf', _('Rich Text'), lambda f: f.lower().endswith('.rtf')), - ('zip', _('Zip archive'), lambda f: f.lower().endswith('.zip')), - ('doc', _('Microsoft Word'), lambda f: re.compile( - r'\.docx?$', re.IGNORECASE).search(f)), - ('xls', _('Microsoft Excel'), lambda f: re.compile( - r'\.xlsx?$', re.IGNORECASE).search(f)), - ('ppt', _('Microsoft PowerPoint'), lambda f: re.compile( - r'\.pptx?$', re.IGNORECASE).search(f)), - ('other', _('Binary'), lambda f: True), # Must be last + ( + "image", + _("Image"), + lambda f: re.compile( + r"\.(bmp|jpe?g|jp2|jxr|gif|png|tiff?|webp)$", re.IGNORECASE + ).search(f), + ), + ( + "video", + _("Video"), + lambda f: re.compile( + r"\.(mov|m[14]v|mp4|avi|mpe?g|qt|ogv|wmv|flv)$", re.IGNORECASE + ).search(f), + ), + ( + "audio", + _("Audio"), + lambda f: re.compile(r"\.(au|mp3|m4a|wma|oga|ram|wav)$", re.IGNORECASE).search( + f + ), + ), + ("pdf", _("PDF document"), lambda f: f.lower().endswith(".pdf")), + ("swf", _("Flash"), lambda f: f.lower().endswith(".swf")), + ("txt", _("Text"), lambda f: f.lower().endswith(".txt")), + ("rtf", _("Rich Text"), lambda f: f.lower().endswith(".rtf")), + ("zip", _("Zip archive"), lambda f: f.lower().endswith(".zip")), + ( + "doc", + _("Microsoft Word"), + lambda f: re.compile(r"\.docx?$", re.IGNORECASE).search(f), + ), + ( + "xls", + _("Microsoft Excel"), + lambda f: re.compile(r"\.xlsx?$", re.IGNORECASE).search(f), + ), + ( + "ppt", + _("Microsoft PowerPoint"), + lambda f: re.compile(r"\.pptx?$", re.IGNORECASE).search(f), + ), + ("other", _("Binary"), lambda f: True), # Must be last ) # ------------------------------------------------------------------------ class MediaFile(MediaFileBase): class Meta: - app_label = 'medialibrary' + app_label = "medialibrary" @receiver(post_delete, sender=MediaFile) def _mediafile_post_delete(sender, instance, **kwargs): instance.delete_mediafile() - logger.info("Deleted mediafile %d (%s)" % ( - instance.id, instance.file.name)) + logger.info("Deleted mediafile %d (%s)" % (instance.id, instance.file.name)) # ------------------------------------------------------------------------ -@python_2_unicode_compatible class MediaFileTranslation(Translation(MediaFile)): """ Translated media file caption and description. """ - caption = models.CharField(_('caption'), max_length=1000) - description = models.TextField(_('description'), blank=True) + caption = models.CharField(_("caption"), max_length=1000) + description = models.TextField(_("description"), blank=True) class Meta: - verbose_name = _('media file translation') - verbose_name_plural = _('media file translations') - unique_together = ('parent', 'language_code') - app_label = 'medialibrary' + verbose_name = _("media file translation") + verbose_name_plural = _("media file translations") + unique_together = ("parent", "language_code") + app_label = "medialibrary" def __str__(self): return self.caption diff --git a/feincms/module/medialibrary/thumbnail.py b/feincms/module/medialibrary/thumbnail.py index a70c2dc27..f833ee913 100644 --- a/feincms/module/medialibrary/thumbnail.py +++ b/feincms/module/medialibrary/thumbnail.py @@ -1,12 +1,10 @@ -from __future__ import absolute_import, unicode_literals - from feincms import settings from feincms.templatetags import feincms_thumbnail from feincms.utils import get_object -def default_admin_thumbnail(mediafile, dimensions='100x100', **kwargs): - if mediafile.type != 'image': +def default_admin_thumbnail(mediafile, dimensions="100x100", **kwargs): + if mediafile.type != "image": return None return feincms_thumbnail.thumbnail(mediafile.file, dimensions) @@ -15,9 +13,8 @@ def default_admin_thumbnail(mediafile, dimensions='100x100', **kwargs): _cached_thumbnailer = None -def admin_thumbnail(mediafile, dimensions='100x100'): +def admin_thumbnail(mediafile, dimensions="100x100"): global _cached_thumbnailer if not _cached_thumbnailer: - _cached_thumbnailer = get_object( - settings.FEINCMS_MEDIALIBRARY_THUMBNAIL) + _cached_thumbnailer = get_object(settings.FEINCMS_MEDIALIBRARY_THUMBNAIL) return _cached_thumbnailer(mediafile, dimensions=dimensions) diff --git a/feincms/module/medialibrary/zip.py b/feincms/module/medialibrary/zip.py index 557e82566..182a43ac1 100644 --- a/feincms/module/medialibrary/zip.py +++ b/feincms/module/medialibrary/zip.py @@ -1,5 +1,4 @@ # ------------------------------------------------------------------------ -# coding=utf-8 # ------------------------------------------------------------------------ # # Created by Martin J. Laubach on 2011-12-07 @@ -7,12 +6,11 @@ # # ------------------------------------------------------------------------ -from __future__ import absolute_import, unicode_literals import json -import zipfile import os import time +import zipfile from django.conf import settings as django_settings from django.core.files.base import ContentFile @@ -23,7 +21,7 @@ # ------------------------------------------------------------------------ -export_magic = 'feincms-export-01' +export_magic = "feincms-export-01" # ------------------------------------------------------------------------ @@ -47,7 +45,7 @@ def import_zipfile(category_id, overwrite, data): info = {} try: info = json.loads(z.comment) - if info['export_magic'] == export_magic: + if info["export_magic"] == export_magic: is_export_file = True except Exception: pass @@ -57,21 +55,21 @@ def import_zipfile(category_id, overwrite, data): category_id_map = {} if is_export_file: for cat in sorted( - info.get('categories', []), - key=lambda k: k.get('level', 999)): + info.get("categories", []), key=lambda k: k.get("level", 999) + ): new_cat, created = Category.objects.get_or_create( - slug=cat['slug'], - title=cat['title']) - category_id_map[cat['id']] = new_cat - if created and cat.get('parent', 0): - parent_cat = category_id_map.get(cat.get('parent', 0), None) + slug=cat["slug"], title=cat["title"] + ) + category_id_map[cat["id"]] = new_cat + if created and cat.get("parent", 0): + parent_cat = category_id_map.get(cat.get("parent", 0), None) if parent_cat: new_cat.parent = parent_cat new_cat.save() count = 0 for zi in z.infolist(): - if not zi.filename.endswith('/'): + if not zi.filename.endswith("/"): bname = os.path.basename(zi.filename) if bname and not bname.startswith(".") and "." in bname: fname, ext = os.path.splitext(bname) @@ -95,36 +93,34 @@ def import_zipfile(category_id, overwrite, data): mf = MediaFile() if overwrite: mf.file.field.upload_to = wanted_dir - mf.copyright = info.get('copyright', '') - mf.file.save( - target_fname, - ContentFile(z.read(zi.filename)), - save=False) + mf.copyright = info.get("copyright", "") + mf.file.save(target_fname, ContentFile(z.read(zi.filename)), save=False) mf.save() found_metadata = False if is_export_file: try: - for tr in info['translations']: + for tr in info["translations"]: found_metadata = True - mt, mt_created =\ - MediaFileTranslation.objects.get_or_create( - parent=mf, language_code=tr['lang']) - mt.caption = tr['caption'] - mt.description = tr.get('description', None) + mt, mt_created = MediaFileTranslation.objects.get_or_create( + parent=mf, language_code=tr["lang"] + ) + mt.caption = tr["caption"] + mt.description = tr.get("description", None) mt.save() # Add categories mf.categories = ( category_id_map[cat_id] - for cat_id in info.get('categories', [])) + for cat_id in info.get("categories", []) + ) except Exception: pass if not found_metadata: mt = MediaFileTranslation() mt.parent = mf - mt.caption = fname.replace('_', ' ') + mt.caption = fname.replace("_", " ") mt.save() if category: @@ -139,10 +135,14 @@ def import_zipfile(category_id, overwrite, data): def export_zipfile(site, queryset): now = timezone.now() zip_name = "export_%s_%04d%02d%02d.zip" % ( - slugify(site.domain), now.year, now.month, now.day) + slugify(site.domain), + now.year, + now.month, + now.day, + ) zip_data = open(os.path.join(django_settings.MEDIA_ROOT, zip_name), "w") - zip_file = zipfile.ZipFile(zip_data, 'w', allowZip64=True) + zip_file = zipfile.ZipFile(zip_data, "w", allowZip64=True) # Save the used categories in the zip file's global comment used_categories = set() @@ -151,30 +151,38 @@ def export_zipfile(site, queryset): used_categories.update(cat.path_list()) info = { - 'export_magic': export_magic, - 'categories': [{ - 'id': cat.id, - 'title': cat.title, - 'slug': cat.slug, - 'parent': cat.parent_id or 0, - 'level': len(cat.path_list()), - } for cat in used_categories], + "export_magic": export_magic, + "categories": [ + { + "id": cat.id, + "title": cat.title, + "slug": cat.slug, + "parent": cat.parent_id or 0, + "level": len(cat.path_list()), + } + for cat in used_categories + ], } zip_file.comment = json.dumps(info) for mf in queryset: ctime = time.localtime(os.stat(mf.file.path).st_ctime) - info = json.dumps({ - 'copyright': mf.copyright, - 'categories': [cat.id for cat in mf.categories.all()], - 'translations': [{ - 'lang': t.language_code, - 'caption': t.caption, - 'description': t.description, - } for t in mf.translations.all()], - }) - - with open(mf.file.path, "r") as file_data: + info = json.dumps( + { + "copyright": mf.copyright, + "categories": [cat.id for cat in mf.categories.all()], + "translations": [ + { + "lang": t.language_code, + "caption": t.caption, + "description": t.description, + } + for t in mf.translations.all() + ], + } + ) + + with open(mf.file.path) as file_data: zip_info = zipfile.ZipInfo( filename=mf.file.name, date_time=( @@ -183,10 +191,13 @@ def export_zipfile(site, queryset): ctime.tm_mday, ctime.tm_hour, ctime.tm_min, - ctime.tm_sec)) + ctime.tm_sec, + ), + ) zip_info.comment = info zip_file.writestr(zip_info, file_data.read()) return zip_name + # ------------------------------------------------------------------------ diff --git a/feincms/module/mixins.py b/feincms/module/mixins.py index ed873334d..63f5f89c7 100644 --- a/feincms/module/mixins.py +++ b/feincms/module/mixins.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, unicode_literals - from collections import OrderedDict from django.http import Http404 @@ -9,10 +7,10 @@ from django.views.generic.base import TemplateResponseMixin from feincms import settings -from feincms.apps import standalone +from feincms.content.application.models import standalone -class ContentModelMixin(object): +class ContentModelMixin: """ Mixin for ``feincms.models.Base`` subclasses which need need some degree of additional control over the request-response cycle. @@ -79,7 +77,7 @@ class ContentObjectMixin(TemplateResponseMixin): context_object_name = None def handler(self, request, *args, **kwargs): - if not hasattr(self.request, '_feincms_extra_context'): + if not hasattr(self.request, "_feincms_extra_context"): self.request._feincms_extra_context = {} r = self.run_request_processors() @@ -117,13 +115,13 @@ def get_template_names(self): # Hopefully someone else has a usable get_template_names() # implementation... - return super(ContentObjectMixin, self).get_template_names() + return super().get_template_names() def get_context_data(self, **kwargs): context = self.request._feincms_extra_context - context[self.context_object_name or 'feincms_object'] = self.object + context[self.context_object_name or "feincms_object"] = self.object context.update(kwargs) - return super(ContentObjectMixin, self).get_context_data(**context) + return super().get_context_data(**context) @property def __name__(self): @@ -140,7 +138,7 @@ def run_request_processors(self): also return a ``HttpResponse`` for shortcutting the rendering and returning that response immediately to the client. """ - if not getattr(self.object, 'request_processors', None): + if not getattr(self.object, "request_processors", None): return for fn in reversed(list(self.object.request_processors.values())): @@ -154,7 +152,7 @@ def run_response_processors(self, response): processors are called to modify the response, eg. for setting cache or expiration headers, keeping statistics, etc. """ - if not getattr(self.object, 'response_processors', None): + if not getattr(self.object, "response_processors", None): return for fn in self.object.response_processors.values(): @@ -172,9 +170,9 @@ def process_content_types(self): # did any content type successfully end processing? successful = False - for content in self.object.content.all_of_type(tuple( - self.object._feincms_content_types_with_process)): - + for content in self.object.content.all_of_type( + tuple(self.object._feincms_content_types_with_process) + ): try: r = content.process(self.request, view=self) if r in (True, False): @@ -191,15 +189,17 @@ def process_content_types(self): extra_context = self.request._feincms_extra_context - if (not settings.FEINCMS_ALLOW_EXTRA_PATH and - extra_context.get('extra_path', '/') != '/' and - # XXX Already inside application content. I'm not sure - # whether this fix is really correct... - not extra_context.get('app_config')): - raise Http404(str('Not found (extra_path %r on %r)') % ( - extra_context.get('extra_path', '/'), - self.object, - )) + if ( + not settings.FEINCMS_ALLOW_EXTRA_PATH + and extra_context.get("extra_path", "/") != "/" + # XXX Already inside application content. I'm not sure + # whether this fix is really correct... + and not extra_context.get("app_config") + ): + raise Http404( + "Not found (extra_path %r on %r)" + % (extra_context.get("extra_path", "/"), self.object) + ) def finalize_content_types(self, response): """ @@ -207,9 +207,9 @@ def finalize_content_types(self, response): returns the final response. """ - for content in self.object.content.all_of_type(tuple( - self.object._feincms_content_types_with_finalize)): - + for content in self.object.content.all_of_type( + tuple(self.object._feincms_content_types_with_finalize) + ): r = content.finalize(self.request, response) if r: return r @@ -229,4 +229,4 @@ def dispatch(self, request, *args, **kwargs): class StandaloneView(generic.View): @method_decorator(standalone) def dispatch(self, request, *args, **kwargs): - return super(StandaloneView, self).dispatch(request, *args, **kwargs) + return super().dispatch(request, *args, **kwargs) diff --git a/feincms/module/page/admin.py b/feincms/module/page/admin.py index 4abea4668..a4404b3cb 100644 --- a/feincms/module/page/admin.py +++ b/feincms/module/page/admin.py @@ -1,23 +1,21 @@ # ------------------------------------------------------------------------ -# coding=utf-8 # ------------------------------------------------------------------------ -from __future__ import absolute_import, unicode_literals from django.contrib import admin -from django.core.exceptions import ImproperlyConfigured -from django.db.models import FieldDoesNotExist +from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured + +from feincms import settings -from feincms import ensure_completely_loaded, settings -from .models import Page from .modeladmins import PageAdmin +from .models import Page + # ------------------------------------------------------------------------ if settings.FEINCMS_USE_PAGE_ADMIN: - ensure_completely_loaded() try: - Page._meta.get_field('template_key') + Page._meta.get_field("template_key") except FieldDoesNotExist: raise ImproperlyConfigured( "The page module requires a 'Page.register_templates()' call " diff --git a/feincms/module/page/extensions/excerpt.py b/feincms/module/page/extensions/excerpt.py index 25fe9004a..2212f2c50 100644 --- a/feincms/module/page/extensions/excerpt.py +++ b/feincms/module/page/extensions/excerpt.py @@ -2,10 +2,8 @@ Add an excerpt field to the page. """ -from __future__ import absolute_import, unicode_literals - from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from feincms import extensions @@ -13,16 +11,17 @@ class Extension(extensions.Extension): def handle_model(self): self.model.add_to_class( - 'excerpt', + "excerpt", models.TextField( - _('excerpt'), + _("excerpt"), blank=True, help_text=_( - 'Add a brief excerpt summarizing the content' - ' of this page.'))) + "Add a brief excerpt summarizing the content of this page." + ), + ), + ) def handle_modeladmin(self, modeladmin): - modeladmin.add_extension_options(_('Excerpt'), { - 'fields': ('excerpt',), - 'classes': ('collapse',), - }) + modeladmin.add_extension_options( + _("Excerpt"), {"fields": ("excerpt",), "classes": ("collapse",)} + ) diff --git a/feincms/module/page/extensions/navigation.py b/feincms/module/page/extensions/navigation.py index f44e8f22b..915bd3863 100644 --- a/feincms/module/page/extensions/navigation.py +++ b/feincms/module/page/extensions/navigation.py @@ -7,19 +7,16 @@ be they real Page instances or extended navigation entries. """ -from __future__ import absolute_import, unicode_literals - -from collections import OrderedDict import types +from collections import OrderedDict from django.db import models -from django.utils import six from django.utils.functional import cached_property -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from feincms import extensions -from feincms.utils import get_object, shorten_string from feincms._internal import monkeypatch_method +from feincms.utils import get_object, shorten_string class TypeRegistryMetaClass(type): @@ -30,13 +27,13 @@ class TypeRegistryMetaClass(type): """ def __init__(cls, name, bases, attrs): - if not hasattr(cls, 'types'): + if not hasattr(cls, "types"): cls.types = [] else: cls.types.append(cls) -class PagePretender(object): +class PagePretender: """ A PagePretender pretends to be a page, but in reality is just a shim layer that implements enough functionality to inject fake pages eg. into the @@ -46,11 +43,12 @@ class PagePretender(object): parameters on creation: title, url, level. If using the translation extension, also add language. """ + pk = None # emulate mptt properties to get the template tags working class _mptt_meta: - level_attr = 'level' + level_attr = "level" def __init__(self, **kwargs): for k, v in kwargs.items(): @@ -66,7 +64,7 @@ def get_level(self): return self.level def get_children(self): - """ overwrite this if you want nested extensions using recursetree """ + """overwrite this if you want nested extensions using recursetree""" return [] def available_translations(self): @@ -79,14 +77,14 @@ def short_title(self): return shorten_string(self.title) -class NavigationExtension(six.with_metaclass(TypeRegistryMetaClass)): +class NavigationExtension(metaclass=TypeRegistryMetaClass): """ Base class for all navigation extensions. The name attribute is shown to the website administrator. """ - name = _('navigation extension') + name = _("navigation extension") def children(self, page, **kwargs): """ @@ -103,51 +101,56 @@ def children(self, page, **kwargs): def navigation_extension_choices(): for ext in NavigationExtension.types: - if (issubclass(ext, NavigationExtension) and - ext is not NavigationExtension): - yield ('%s.%s' % (ext.__module__, ext.__name__), ext.name) + if issubclass(ext, NavigationExtension) and ext is not NavigationExtension: + yield (f"{ext.__module__}.{ext.__name__}", ext.name) def get_extension_class(extension): extension = get_object(extension) if isinstance(extension, types.ModuleType): - return getattr(extension, 'Extension') + return getattr(extension, "Extension") return extension class Extension(extensions.Extension): - ident = 'navigation' # TODO actually use this + ident = "navigation" # TODO actually use this navigation_extensions = None @cached_property def _extensions(self): if self.navigation_extensions is None: return OrderedDict( - ('%s.%s' % (ext.__module__, ext.__name__), ext) + (f"{ext.__module__}.{ext.__name__}", ext) for ext in NavigationExtension.types if ( - issubclass(ext, NavigationExtension) and - ext is not NavigationExtension)) + issubclass(ext, NavigationExtension) + and ext is not NavigationExtension + ) + ) else: return OrderedDict( - ('%s.%s' % (ext.__module__, ext.__name__), ext) - for ext - in map(get_extension_class, self.navigation_extensions)) + (f"{ext.__module__}.{ext.__name__}", ext) + for ext in map(get_extension_class, self.navigation_extensions) + ) def handle_model(self): - choices = [ - (path, ext.name) for path, ext in self._extensions.items()] + choices = [(path, ext.name) for path, ext in self._extensions.items()] self.model.add_to_class( - 'navigation_extension', + "navigation_extension", models.CharField( - _('navigation extension'), + _("navigation extension"), choices=choices, - blank=True, null=True, max_length=200, + blank=True, + null=True, + max_length=200, help_text=_( - 'Select the module providing subpages for this page if' - ' you need to customize the navigation.'))) + "Select the module providing subpages for this page if" + " you need to customize the navigation." + ), + ), + ) extension = self @@ -169,7 +172,7 @@ def extended_navigation(self, **kwargs): return self.children.in_navigation() def handle_modeladmin(self, modeladmin): - modeladmin.add_extension_options(_('Navigation extension'), { - 'fields': ('navigation_extension',), - 'classes': ('collapse',), - }) + modeladmin.add_extension_options( + _("Navigation extension"), + {"fields": ("navigation_extension",), "classes": ("collapse",)}, + ) diff --git a/feincms/module/page/extensions/navigationgroups.py b/feincms/module/page/extensions/navigationgroups.py index fc47ec033..67a0c4f53 100644 --- a/feincms/module/page/extensions/navigationgroups.py +++ b/feincms/module/page/extensions/navigationgroups.py @@ -3,33 +3,30 @@ such as header, footer and what else. """ -from __future__ import absolute_import, unicode_literals - from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from feincms import extensions class Extension(extensions.Extension): - ident = 'navigationgroups' - groups = [ - ('default', _('Default')), - ('footer', _('Footer')), - ] + ident = "navigationgroups" + groups = [("default", _("Default")), ("footer", _("Footer"))] def handle_model(self): self.model.add_to_class( - 'navigation_group', + "navigation_group", models.CharField( - _('navigation group'), + _("navigation group"), choices=self.groups, default=self.groups[0][0], max_length=20, blank=True, - db_index=True)) + db_index=True, + ), + ) def handle_modeladmin(self, modeladmin): - modeladmin.add_extension_options('navigation_group') - modeladmin.extend_list('list_display', ['navigation_group']) - modeladmin.extend_list('list_filter', ['navigation_group']) + modeladmin.add_extension_options("navigation_group") + modeladmin.extend_list("list_display", ["navigation_group"]) + modeladmin.extend_list("list_filter", ["navigation_group"]) diff --git a/feincms/module/page/extensions/relatedpages.py b/feincms/module/page/extensions/relatedpages.py index 2ddfb0ef7..e0b4a9340 100644 --- a/feincms/module/page/extensions/relatedpages.py +++ b/feincms/module/page/extensions/relatedpages.py @@ -2,27 +2,27 @@ Add a many-to-many relationship field to relate this page to other pages. """ -from __future__ import absolute_import, unicode_literals - from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from feincms import extensions, settings class Extension(extensions.Extension): def handle_model(self): - self.model.add_to_class('related_pages', models.ManyToManyField( - settings.FEINCMS_DEFAULT_PAGE_MODEL, - blank=True, - related_name='%(app_label)s_%(class)s_related', - help_text=_( - 'Select pages that should be listed as related content.'))) + self.model.add_to_class( + "related_pages", + models.ManyToManyField( + settings.FEINCMS_DEFAULT_PAGE_MODEL, + blank=True, + related_name="%(app_label)s_%(class)s_related", + help_text=_("Select pages that should be listed as related content."), + ), + ) def handle_modeladmin(self, modeladmin): - modeladmin.extend_list('filter_horizontal', ['related_pages']) + modeladmin.extend_list("filter_horizontal", ["related_pages"]) - modeladmin.add_extension_options(_('Related pages'), { - 'fields': ('related_pages',), - 'classes': ('collapse',), - }) + modeladmin.add_extension_options( + _("Related pages"), {"fields": ("related_pages",), "classes": ("collapse",)} + ) diff --git a/feincms/module/page/extensions/sites.py b/feincms/module/page/extensions/sites.py index b0b25d245..4a4da11e8 100644 --- a/feincms/module/page/extensions/sites.py +++ b/feincms/module/page/extensions/sites.py @@ -1,9 +1,7 @@ -from __future__ import absolute_import, unicode_literals - from django.conf import settings from django.contrib.sites.models import Site from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from feincms import extensions from feincms.module.page.models import PageManager @@ -16,14 +14,18 @@ def current_site(queryset): class Extension(extensions.Extension): def handle_model(self): self.model.add_to_class( - 'site', + "site", models.ForeignKey( - Site, verbose_name=_('Site'), default=settings.SITE_ID, - on_delete=models.CASCADE)) + Site, + verbose_name=_("Site"), + default=settings.SITE_ID, + on_delete=models.CASCADE, + ), + ) - PageManager.add_to_active_filters(current_site, key='current_site') + PageManager.add_to_active_filters(current_site, key="current_site") def handle_modeladmin(self, modeladmin): - modeladmin.extend_list('list_display', ['site']) - modeladmin.extend_list('list_filter', ['site']) - modeladmin.add_extension_options('site') + modeladmin.extend_list("list_display", ["site"]) + modeladmin.extend_list("list_filter", ["site"]) + modeladmin.add_extension_options("site") diff --git a/feincms/module/page/extensions/symlinks.py b/feincms/module/page/extensions/symlinks.py index c5e4c095b..52433da37 100644 --- a/feincms/module/page/extensions/symlinks.py +++ b/feincms/module/page/extensions/symlinks.py @@ -3,10 +3,8 @@ all content from the linked page. """ -from __future__ import absolute_import, unicode_literals - from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from feincms import extensions from feincms._internal import monkeypatch_property @@ -14,26 +12,29 @@ class Extension(extensions.Extension): def handle_model(self): - self.model.add_to_class('symlinked_page', models.ForeignKey( - 'self', - blank=True, - null=True, - on_delete=models.CASCADE, - related_name='%(app_label)s_%(class)s_symlinks', - verbose_name=_('symlinked page'), - help_text=_('All content is inherited from this page if given.'))) + self.model.add_to_class( + "symlinked_page", + models.ForeignKey( + "self", + blank=True, + null=True, + on_delete=models.CASCADE, + related_name="%(app_label)s_%(class)s_symlinks", + verbose_name=_("symlinked page"), + help_text=_("All content is inherited from this page if given."), + ), + ) @monkeypatch_property(self.model) def content(self): - if not hasattr(self, '_content_proxy'): + if not hasattr(self, "_content_proxy"): if self.symlinked_page: - self._content_proxy = self.content_proxy_class( - self.symlinked_page) + self._content_proxy = self.content_proxy_class(self.symlinked_page) else: self._content_proxy = self.content_proxy_class(self) return self._content_proxy def handle_modeladmin(self, modeladmin): - modeladmin.extend_list('raw_id_fields', ['symlinked_page']) - modeladmin.add_extension_options('symlinked_page') + modeladmin.extend_list("raw_id_fields", ["symlinked_page"]) + modeladmin.add_extension_options("symlinked_page") diff --git a/feincms/module/page/extensions/titles.py b/feincms/module/page/extensions/titles.py index 68529b113..7269c3564 100644 --- a/feincms/module/page/extensions/titles.py +++ b/feincms/module/page/extensions/titles.py @@ -4,10 +4,8 @@ you do that. """ -from __future__ import absolute_import, unicode_literals - from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from feincms import extensions from feincms._internal import monkeypatch_property @@ -15,20 +13,30 @@ class Extension(extensions.Extension): def handle_model(self): - self.model.add_to_class('_content_title', models.TextField( - _('content title'), - blank=True, - help_text=_( - 'The first line is the main title, the following' - ' lines are subtitles.'))) - - self.model.add_to_class('_page_title', models.CharField( - _('page title'), - max_length=69, - blank=True, - help_text=_( - 'Page title for browser window. Same as title by' - ' default. Must be 69 characters or fewer.'))) + self.model.add_to_class( + "_content_title", + models.TextField( + _("content title"), + blank=True, + help_text=_( + "The first line is the main title, the following" + " lines are subtitles." + ), + ), + ) + + self.model.add_to_class( + "_page_title", + models.CharField( + _("page title"), + max_length=69, + blank=True, + help_text=_( + "Page title for browser window. Same as title by" + " default. Must be 69 characters or fewer." + ), + ), + ) @monkeypatch_property(self.model) def page_title(self): @@ -54,10 +62,10 @@ def content_title(self): @monkeypatch_property(self.model) def content_subtitle(self): - return '\n'.join(self._content_title.splitlines()[1:]) + return "\n".join(self._content_title.splitlines()[1:]) def handle_modeladmin(self, modeladmin): - modeladmin.add_extension_options(_('Titles'), { - 'fields': ('_content_title', '_page_title'), - 'classes': ('collapse',), - }) + modeladmin.add_extension_options( + _("Titles"), + {"fields": ("_content_title", "_page_title"), "classes": ("collapse",)}, + ) diff --git a/feincms/module/page/forms.py b/feincms/module/page/forms.py index 1e7f3abd8..8e9415888 100644 --- a/feincms/module/page/forms.py +++ b/feincms/module/page/forms.py @@ -1,8 +1,6 @@ # ------------------------------------------------------------------------ -# coding=utf-8 # ------------------------------------------------------------------------ -from __future__ import absolute_import, unicode_literals import re @@ -10,10 +8,7 @@ from django.contrib.admin.widgets import ForeignKeyRawIdWidget from django.forms.models import model_to_dict from django.utils.safestring import mark_safe -from django.utils.translation import ugettext_lazy as _ - -from feincms import ensure_completely_loaded - +from django.utils.translation import gettext_lazy as _ from mptt.forms import MPTTAdminForm @@ -21,28 +16,38 @@ class RedirectToWidget(ForeignKeyRawIdWidget): def label_for_value(self, value): match = re.match( # XXX this regex would be available as .models.REDIRECT_TO_RE - r'^(?P\w+).(?P\w+):(?P\d+)$', - value) + r"^(?P\w+).(?P\w+):(?P\d+)$", + value, + ) if match: matches = match.groupdict() - model = apps.get_model(matches['app_label'], matches['model_name']) + model = apps.get_model(matches["app_label"], matches["model_name"]) try: - instance = model._default_manager.get(pk=int(matches['pk'])) - return ' %s (%s)' % ( - instance, instance.get_absolute_url()) + instance = model._default_manager.get(pk=int(matches["pk"])) + return " {} ({})".format( + instance, + instance.get_absolute_url(), + ) except model.DoesNotExist: pass - return '' + return "" # ------------------------------------------------------------------------ class PageAdminForm(MPTTAdminForm): never_copy_fields = ( - 'title', 'slug', 'parent', 'active', 'override_url', - 'translation_of', '_content_title', '_page_title') + "title", + "slug", + "parent", + "active", + "override_url", + "translation_of", + "_content_title", + "_page_title", + ) @property def page_model(self): @@ -53,14 +58,11 @@ def page_manager(self): return self.page_model._default_manager def __init__(self, *args, **kwargs): - ensure_completely_loaded() - - if 'initial' in kwargs: - if 'parent' in kwargs['initial']: + if "initial" in kwargs: + if "parent" in kwargs["initial"]: # Prefill a few form values from the parent page try: - page = self.page_manager.get( - pk=kwargs['initial']['parent']) + page = self.page_manager.get(pk=kwargs["initial"]["parent"]) data = model_to_dict(page) @@ -73,79 +75,81 @@ def __init__(self, *args, **kwargs): if field in data: del data[field] - data.update(kwargs['initial']) + data.update(kwargs["initial"]) if page.template.child_template: - data['template_key'] = page.template.child_template - kwargs['initial'] = data + data["template_key"] = page.template.child_template + kwargs["initial"] = data except self.page_model.DoesNotExist: pass - elif 'translation_of' in kwargs['initial']: + elif "translation_of" in kwargs["initial"]: # Only if translation extension is active try: - page = self.page_manager.get( - pk=kwargs['initial']['translation_of']) + page = self.page_manager.get(pk=kwargs["initial"]["translation_of"]) original = page.original_translation data = { - 'translation_of': original.id, - 'template_key': original.template_key, - 'active': original.active, - 'in_navigation': original.in_navigation, + "translation_of": original.id, + "template_key": original.template_key, + "active": original.active, + "in_navigation": original.in_navigation, } if original.parent: try: - data['parent'] = original.parent.get_translation( - kwargs['initial']['language'] + data["parent"] = original.parent.get_translation( + kwargs["initial"]["language"] ).id except self.page_model.DoesNotExist: # ignore this -- the translation does not exist pass - data.update(kwargs['initial']) - kwargs['initial'] = data + data.update(kwargs["initial"]) + kwargs["initial"] = data except (AttributeError, self.page_model.DoesNotExist): pass # Not required, only a nice-to-have for the `redirect_to` field - modeladmin = kwargs.pop('modeladmin', None) - super(PageAdminForm, self).__init__(*args, **kwargs) - if modeladmin and 'redirect_to' in self.fields: + modeladmin = kwargs.pop("modeladmin", None) + super().__init__(*args, **kwargs) + if modeladmin and "redirect_to" in self.fields: # Note: Using `parent` is not strictly correct, but we can be # sure that `parent` always points to another page instance, # and that's good enough for us. - field = self.page_model._meta.get_field('parent') - self.fields['redirect_to'].widget = RedirectToWidget( - field.remote_field if hasattr(field, 'remote_field') else field.rel, # noqa - modeladmin.admin_site) + field = self.page_model._meta.get_field("parent") + self.fields["redirect_to"].widget = RedirectToWidget( + field.remote_field if hasattr(field, "remote_field") else field.rel, # noqa + modeladmin.admin_site, + ) - if 'template_key' in self.fields: + if "template_key" in self.fields: choices = [] for key, template_name in self.page_model.TEMPLATE_CHOICES: template = self.page_model._feincms_templates[key] pages_for_template = self.page_model._default_manager.filter( - template_key=key) - pk = kwargs['instance'].pk if kwargs.get('instance') else None + template_key=key + ) + pk = kwargs["instance"].pk if kwargs.get("instance") else None other_pages_for_template = pages_for_template.exclude(pk=pk) if template.singleton and other_pages_for_template.exists(): continue # don't allow selection of singleton if in use if template.preview_image: - choices.append(( - template.key, - mark_safe('%s %s' % ( - template.preview_image, + choices.append( + ( template.key, - template.title, - )) - )) + mark_safe( + '%s %s' + % (template.preview_image, template.key, template.title) + ), + ) + ) else: choices.append((template.key, template.title)) - self.fields['template_key'].choices = choices + self.fields["template_key"].choices = choices def clean(self): - cleaned_data = super(PageAdminForm, self).clean() + cleaned_data = super().clean() # No need to think further, let the user correct errors first if self._errors: @@ -160,18 +164,21 @@ def clean(self): current_id = self.instance.id active_pages = active_pages.exclude(id=current_id) - sites_is_installed = apps.is_installed('django.contrib.sites') - if sites_is_installed and 'site' in cleaned_data: - active_pages = active_pages.filter(site=cleaned_data['site']) + sites_is_installed = apps.is_installed("django.contrib.sites") + if sites_is_installed and "site" in cleaned_data: + active_pages = active_pages.filter(site=cleaned_data["site"]) # Convert PK in redirect_to field to something nicer for the future - redirect_to = cleaned_data.get('redirect_to') - if redirect_to and re.match(r'^\d+$', redirect_to): + redirect_to = cleaned_data.get("redirect_to") + if redirect_to and re.match(r"^\d+$", redirect_to): opts = self.page_model._meta - cleaned_data['redirect_to'] = '%s.%s:%s' % ( - opts.app_label, opts.model_name, redirect_to) + cleaned_data["redirect_to"] = "{}.{}:{}".format( + opts.app_label, + opts.model_name, + redirect_to, + ) - if 'active' in cleaned_data and not cleaned_data['active']: + if "active" in cleaned_data and not cleaned_data["active"]: # If the current item is inactive, we do not need to conduct # further validation. Note that we only check for the flag, not # for any other active filters. This is because we do not want @@ -179,12 +186,12 @@ def clean(self): # really won't be active at the same time. return cleaned_data - if 'override_url' in cleaned_data and cleaned_data['override_url']: - if active_pages.filter( - _cached_url=cleaned_data['override_url']).count(): - self._errors['override_url'] = self.error_class([ - _('This URL is already taken by an active page.')]) - del cleaned_data['override_url'] + if "override_url" in cleaned_data and cleaned_data["override_url"]: + if active_pages.filter(_cached_url=cleaned_data["override_url"]).count(): + self._errors["override_url"] = self.error_class( + [_("This URL is already taken by an active page.")] + ) + del cleaned_data["override_url"] return cleaned_data @@ -193,24 +200,27 @@ def clean(self): parent = self.page_manager.get(pk=current_id).parent else: # The user tries to create a new page - parent = cleaned_data['parent'] + parent = cleaned_data["parent"] if parent: - new_url = '%s%s/' % (parent._cached_url, cleaned_data['slug']) + new_url = "{}{}/".format(parent._cached_url, cleaned_data["slug"]) else: - new_url = '/%s/' % cleaned_data['slug'] + new_url = "/%s/" % cleaned_data["slug"] if active_pages.filter(_cached_url=new_url).count(): - self._errors['active'] = self.error_class([ - _('This URL is already taken by another active page.')]) - del cleaned_data['active'] + self._errors["active"] = self.error_class( + [_("This URL is already taken by another active page.")] + ) + del cleaned_data["active"] if parent and parent.template.enforce_leaf: - self._errors['parent'] = self.error_class( - [_('This page does not allow attachment of child pages')]) - del cleaned_data['parent'] + self._errors["parent"] = self.error_class( + [_("This page does not allow attachment of child pages")] + ) + del cleaned_data["parent"] return cleaned_data + # ------------------------------------------------------------------------ # ------------------------------------------------------------------------ diff --git a/feincms/module/page/modeladmins.py b/feincms/module/page/modeladmins.py index a913c0db8..2855a1c5f 100644 --- a/feincms/module/page/modeladmins.py +++ b/feincms/module/page/modeladmins.py @@ -1,26 +1,19 @@ # ------------------------------------------------------------------------ -# coding=utf-8 # ------------------------------------------------------------------------ -from __future__ import absolute_import, unicode_literals +from functools import partial from threading import local from django.conf import settings as django_settings -from django.core.exceptions import PermissionDenied +from django.contrib import admin, messages from django.contrib.contenttypes.models import ContentType -from django.contrib.staticfiles.templatetags.staticfiles import static -from django.contrib import admin -from django.contrib import messages +from django.core.exceptions import PermissionDenied from django.http import HttpResponseRedirect -from django.utils.functional import curry -from django.utils.translation import ugettext_lazy as _ -try: - from django.urls import reverse -except ImportError: - from django.core.urlresolvers import reverse - -from feincms import ensure_completely_loaded +from django.templatetags.static import static +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + from feincms import settings from feincms.admin import item_editor, tree_editor @@ -41,70 +34,70 @@ class Media: fieldset_insertion_index = 2 fieldsets = [ - (None, { - 'fields': [ - ('title', 'slug'), - ('active', 'in_navigation'), - ], - }), - (_('Other options'), { - 'classes': ['collapse'], - 'fields': [ - 'template_key', 'parent', 'override_url', 'redirect_to'], - }), + (None, {"fields": [("title", "slug"), ("active", "in_navigation")]}), + ( + _("Other options"), + { + "classes": ["collapse"], + "fields": ["template_key", "parent", "override_url", "redirect_to"], + }, + ), # <-- insertion point, extensions appear here, see insertion_index # above item_editor.FEINCMS_CONTENT_FIELDSET, ] readonly_fields = [] list_display = [ - 'short_title', 'is_visible_admin', 'in_navigation_toggle', 'template'] - list_filter = ['active', 'in_navigation', 'template_key', 'parent'] - search_fields = ['title', 'slug'] - prepopulated_fields = {'slug': ('title',)} + "short_title", + "is_visible_admin", + "in_navigation_toggle", + "template", + ] + list_filter = ["active", "in_navigation", "template_key", "parent"] + search_fields = ["title", "slug"] + prepopulated_fields = {"slug": ("title",)} - raw_id_fields = ['parent'] - radio_fields = {'template_key': admin.HORIZONTAL} + raw_id_fields = ["parent"] + radio_fields = {"template_key": admin.HORIZONTAL} @classmethod def add_extension_options(cls, *f): - if isinstance(f[-1], dict): # called with a fieldset + if isinstance(f[-1], dict): # called with a fieldset cls.fieldsets.insert(cls.fieldset_insertion_index, f) - f[1]['classes'] = list(f[1].get('classes', [])) - f[1]['classes'].append('collapse') - else: # assume called with "other" fields - cls.fieldsets[1][1]['fields'].extend(f) + f[1]["classes"] = list(f[1].get("classes", [])) + f[1]["classes"].append("collapse") + else: # assume called with "other" fields + cls.fieldsets[1][1]["fields"].extend(f) def __init__(self, model, admin_site): - ensure_completely_loaded() - - if len(model._feincms_templates) > 4 and \ - 'template_key' in self.radio_fields: - del(self.radio_fields['template_key']) + if len(model._feincms_templates) > 4 and "template_key" in self.radio_fields: + del self.radio_fields["template_key"] - super(PageAdmin, self).__init__(model, admin_site) + super().__init__(model, admin_site) in_navigation_toggle = tree_editor.ajax_editable_boolean( - 'in_navigation', _('in navigation')) + "in_navigation", _("in navigation") + ) def get_readonly_fields(self, request, obj=None): - readonly = super(PageAdmin, self).get_readonly_fields(request, obj=obj) + readonly = super().get_readonly_fields(request, obj=obj) if not settings.FEINCMS_SINGLETON_TEMPLATE_CHANGE_ALLOWED: if obj and obj.template and obj.template.singleton: - return tuple(readonly) + ('template_key',) + return tuple(readonly) + ("template_key",) return readonly def get_form(self, *args, **kwargs): - form = super(PageAdmin, self).get_form(*args, **kwargs) - return curry(form, modeladmin=self) + form = super().get_form(*args, **kwargs) + return partial(form, modeladmin=self) def _actions_column(self, page): - addable = getattr(page, 'feincms_addable', True) + addable = getattr(page, "feincms_addable", True) - preview_url = "../../r/%s/%s/" % ( + preview_url = "../../r/{}/{}/".format( ContentType.objects.get_for_model(self.model).id, - page.id) - actions = super(PageAdmin, self)._actions_column(page) + page.id, + ) + actions = super()._actions_column(page) if addable: if not page.template.enforce_leaf: @@ -112,61 +105,63 @@ def _actions_column(self, page): 0, '' '%s' - '' % ( + "" + % ( page.pk, - _('Add child page'), - static('feincms/img/icon_addlink.gif'), - _('Add child page'), - ) + _("Add child page"), + static("feincms/img/icon_addlink.gif"), + _("Add child page"), + ), ) actions.insert( 0, '' '%s' - '' % ( + "" + % ( preview_url, - _('View on site'), - static('feincms/img/selector-search.gif'), - _('View on site'), - ) + _("View on site"), + static("feincms/img/selector-search.gif"), + _("View on site"), + ), ) return actions def add_view(self, request, **kwargs): - kwargs['form_url'] = request.get_full_path() # Preserve GET parameters - if 'translation_of' in request.GET and 'language' in request.GET: + kwargs["form_url"] = request.get_full_path() # Preserve GET parameters + if "translation_of" in request.GET and "language" in request.GET: try: original = self.model._tree_manager.get( - pk=request.GET.get('translation_of')) + pk=request.GET.get("translation_of") + ) except (AttributeError, self.model.DoesNotExist): pass else: - language_code = request.GET['language'] - language = dict( - django_settings.LANGUAGES).get(language_code, '') - kwargs['extra_context'] = { - 'adding_translation': True, - 'title': _( - 'Add %(language)s translation of "%(page)s"') % { - 'language': language, - 'page': original, - }, - 'language_name': language, - 'translation_of': original, + language_code = request.GET["language"] + language = dict(django_settings.LANGUAGES).get(language_code, "") + kwargs["extra_context"] = { + "adding_translation": True, + "title": _('Add %(language)s translation of "%(page)s"') + % {"language": language, "page": original}, + "language_name": language, + "translation_of": original, } - return super(PageAdmin, self).add_view(request, **kwargs) + return super().add_view(request, **kwargs) def response_add(self, request, obj, *args, **kwargs): - response = super(PageAdmin, self).response_add( - request, obj, *args, **kwargs) - if ('parent' in request.GET and - '_addanother' in request.POST and - response.status_code in (301, 302)): + response = super().response_add(request, obj, *args, **kwargs) + if ( + "parent" in request.GET + and "_addanother" in request.POST + and response.status_code in (301, 302) + ): # Preserve GET parameters if we are about to add another page - response['Location'] += '?parent=%s' % request.GET['parent'] + response["Location"] += "?parent=%s" % request.GET["parent"] - if ('translation_of' in request.GET and - '_copy_content_from_original' in request.POST): + if ( + "translation_of" in request.GET + and "_copy_content_from_original" in request.POST + ): # Copy all contents for content_type in obj._feincms_content_types: if content_type.objects.filter(parent=obj).exists(): @@ -176,14 +171,19 @@ def response_add(self, request, obj, *args, **kwargs): try: original = self.model._tree_manager.get( - pk=request.GET.get('translation_of')) + pk=request.GET.get("translation_of") + ) original = original.original_translation obj.copy_content_from(original) obj.save() - self.message_user(request, _( - 'The content from the original translation has been copied' - ' to the newly created page.')) + self.message_user( + request, + _( + "The content from the original translation has been copied" + " to the newly created page." + ), + ) except (AttributeError, self.model.DoesNotExist): pass @@ -191,30 +191,28 @@ def response_add(self, request, obj, *args, **kwargs): def change_view(self, request, object_id, **kwargs): try: - return super(PageAdmin, self).change_view( - request, object_id, **kwargs) + return super().change_view(request, object_id, **kwargs) except PermissionDenied: messages.add_message( request, messages.ERROR, - _( - "You don't have the necessary permissions to edit this" - " object" - ) + _("You don't have the necessary permissions to edit this object"), ) - return HttpResponseRedirect(reverse('admin:page_page_changelist')) + return HttpResponseRedirect(reverse("admin:page_page_changelist")) def has_delete_permission(self, request, obj=None): if not settings.FEINCMS_SINGLETON_TEMPLATE_DELETION_ALLOWED: if obj and obj.template.singleton: return False - return super(PageAdmin, self).has_delete_permission(request, obj=obj) + return super().has_delete_permission(request, obj=obj) def changelist_view(self, request, *args, **kwargs): _local.visible_pages = list( - self.model.objects.active().values_list('id', flat=True)) - return super(PageAdmin, self).changelist_view(request, *args, **kwargs) + self.model.objects.active().values_list("id", flat=True) + ) + return super().changelist_view(request, *args, **kwargs) + @admin.display(description=_("is active")) def is_visible_admin(self, page): """ Instead of just showing an on/off boolean, also indicate whether this @@ -225,30 +223,35 @@ def is_visible_admin(self, page): if page.id in _local.visible_pages: _local.visible_pages.remove(page.id) return tree_editor.ajax_editable_boolean_cell( - page, 'active', override=False, text=_('inherited')) + page, "active", override=False, text=_("inherited") + ) if page.active and page.id not in _local.visible_pages: # is active but should not be shown, so visibility limited by # extension: show a "not active" return tree_editor.ajax_editable_boolean_cell( - page, 'active', override=False, text=_('extensions')) + page, "active", override=False, text=_("extensions") + ) - return tree_editor.ajax_editable_boolean_cell(page, 'active') - is_visible_admin.short_description = _('is active') - is_visible_admin.editable_boolean_field = 'active' + return tree_editor.ajax_editable_boolean_cell(page, "active") + + is_visible_admin.editable_boolean_field = "active" # active toggle needs more sophisticated result function def is_visible_recursive(self, page): # Have to refresh visible_pages here, because TreeEditor.toggle_boolean # will have changed the value when inside this code path. _local.visible_pages = list( - self.model.objects.active().values_list('id', flat=True)) + self.model.objects.active().values_list("id", flat=True) + ) retval = [] for c in page.get_descendants(include_self=True): retval.append(self.is_visible_admin(c)) return retval + is_visible_admin.editable_boolean_result = is_visible_recursive + # ------------------------------------------------------------------------ # ------------------------------------------------------------------------ diff --git a/feincms/module/page/models.py b/feincms/module/page/models.py index 1f6f5262f..b269b9867 100644 --- a/feincms/module/page/models.py +++ b/feincms/module/page/models.py @@ -1,30 +1,21 @@ # ------------------------------------------------------------------------ -# coding=utf-8 # ------------------------------------------------------------------------ -from __future__ import absolute_import, unicode_literals from django.core.exceptions import PermissionDenied -from django.db import models +from django.db import models, transaction from django.db.models import Q from django.http import Http404 -from django.utils.encoding import python_2_unicode_compatible -from django.utils.translation import ugettext_lazy as _ -try: - from django.urls import reverse -except ImportError: - from django.core.urlresolvers import reverse - +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ from mptt.models import MPTTModel, TreeManager from feincms import settings from feincms.models import create_base_model from feincms.module.mixins import ContentModelMixin from feincms.module.page import processors +from feincms.utils import get_model_instance, match_model_string, shorten_string from feincms.utils.managers import ActiveAwareContentManagerMixin -from feincms.utils import ( - shorten_string, match_model_string, get_model_instance -) # ------------------------------------------------------------------------ @@ -35,8 +26,7 @@ class BasePageManager(ActiveAwareContentManagerMixin, TreeManager): """ # The fields which should be excluded when creating a copy. - exclude_from_copy = [ - 'id', 'tree_id', 'lft', 'rght', 'level', 'redirect_to'] + exclude_from_copy = ["id", "tree_id", "lft", "rght", "level", "redirect_to"] def page_for_path(self, path, raise404=False): """ @@ -47,14 +37,13 @@ def page_for_path(self, path, raise404=False): Page.objects.page_for_path(request.path) """ - stripped = path.strip('/') + stripped = path.strip("/") try: - page = self.active().get( - _cached_url='/%s/' % stripped if stripped else '/') + page = self.active().get(_cached_url="/%s/" % stripped if stripped else "/") if not page.are_ancestors_active(): - raise self.model.DoesNotExist('Parents are inactive.') + raise self.model.DoesNotExist("Parents are inactive.") return page @@ -75,22 +64,23 @@ def best_match_for_path(self, path, raise404=False): page with url '/photos/album/'. """ - paths = ['/'] - path = path.strip('/') + paths = ["/"] + path = path.strip("/") if path: - tokens = path.split('/') - paths += [ - '/%s/' % '/'.join(tokens[:i]) - for i in range(1, len(tokens) + 1)] + tokens = path.split("/") + paths += ["/%s/" % "/".join(tokens[:i]) for i in range(1, len(tokens) + 1)] try: - page = self.active().filter(_cached_url__in=paths).extra( - select={'_url_length': 'LENGTH(_cached_url)'} - ).order_by('-_url_length')[0] + page = ( + self.active() + .filter(_cached_url__in=paths) + .extra(select={"_url_length": "LENGTH(_cached_url)"}) + .order_by("-_url_length")[0] + ) if not page.are_ancestors_active(): - raise IndexError('Parents are inactive.') + raise IndexError("Parents are inactive.") return page @@ -114,8 +104,7 @@ def toplevel_navigation(self): return self.in_navigation().filter(parent__isnull=True) - def for_request(self, request, raise404=False, best_match=False, - path=None): + def for_request(self, request, raise404=False, best_match=False, path=None): """ Return a page for the request @@ -129,15 +118,15 @@ def for_request(self, request, raise404=False, best_match=False, could be determined. """ - if not hasattr(request, '_feincms_page'): + if not hasattr(request, "_feincms_page"): path = path or request.path_info or request.path if best_match: request._feincms_page = self.best_match_for_path( - path, raise404=raise404) + path, raise404=raise404 + ) else: - request._feincms_page = self.page_for_path( - path, raise404=raise404) + request._feincms_page = self.page_for_path(path, raise404=raise404) return request._feincms_page @@ -147,45 +136,62 @@ class PageManager(BasePageManager): pass -PageManager.add_to_active_filters(Q(active=True), key='is_active') +PageManager.add_to_active_filters(Q(active=True), key="is_active") # ------------------------------------------------------------------------ -@python_2_unicode_compatible class BasePage(create_base_model(MPTTModel), ContentModelMixin): - active = models.BooleanField(_('active'), default=True) + active = models.BooleanField(_("active"), default=True) # structure and navigation - title = models.CharField(_('title'), max_length=200, help_text=_( - 'This title is also used for navigation menu items.')) + title = models.CharField( + _("title"), + max_length=200, + help_text=_("This title is also used for navigation menu items."), + ) slug = models.SlugField( - _('slug'), max_length=150, - help_text=_('This is used to build the URL for this page')) + _("slug"), + max_length=150, + help_text=_("This is used to build the URL for this page"), + ) parent = models.ForeignKey( - 'self', verbose_name=_('Parent'), blank=True, + "self", + verbose_name=_("Parent"), + blank=True, on_delete=models.CASCADE, - null=True, related_name='children') + null=True, + related_name="children", + ) # Custom list_filter - see admin/filterspecs.py parent.parent_filter = True - in_navigation = models.BooleanField(_('in navigation'), default=False) + in_navigation = models.BooleanField(_("in navigation"), default=False) override_url = models.CharField( - _('override URL'), max_length=255, - blank=True, help_text=_( - 'Override the target URL. Be sure to include slashes at the ' - 'beginning and at the end if it is a local URL. This ' - 'affects both the navigation and subpages\' URLs.')) - redirect_to = models.CharField( - _('redirect to'), max_length=255, + _("override URL"), + max_length=255, blank=True, help_text=_( - 'Target URL for automatic redirects' - ' or the primary key of a page.')) + "Override the target URL. Be sure to include slashes at the " + "beginning and at the end if it is a local URL. This " + "affects both the navigation and subpages' URLs." + ), + ) + redirect_to = models.CharField( + _("redirect to"), + max_length=255, + blank=True, + help_text=_("Target URL for automatic redirects or the primary key of a page."), + ) _cached_url = models.CharField( - _('Cached URL'), max_length=255, blank=True, - editable=False, default='', db_index=True) + _("Cached URL"), + max_length=255, + blank=True, + editable=False, + default="", + db_index=True, + ) class Meta: - ordering = ['tree_id', 'lft'] + ordering = ["tree_id", "lft"] abstract = True objects = PageManager() @@ -206,11 +212,11 @@ def is_active(self): return False pages = self.__class__.objects.active().filter( - tree_id=self.tree_id, - lft__lte=self.lft, - rght__gte=self.rght) + tree_id=self.tree_id, lft__lte=self.lft, rght__gte=self.rght + ) return pages.count() > self.level - is_active.short_description = _('is active') + + is_active.short_description = _("is active") def are_ancestors_active(self): """ @@ -228,11 +234,12 @@ def short_title(self): Title shortened for display. """ return shorten_string(self.title) - short_title.admin_order_field = 'title' - short_title.short_description = _('title') + + short_title.admin_order_field = "title" + short_title.short_description = _("title") def __init__(self, *args, **kwargs): - super(BasePage, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) # Cache a copy of the loaded _cached_url value so we can reliably # determine whether it has been changed in the save handler: self._original_cached_url = self._cached_url @@ -250,43 +257,49 @@ def save(self, *args, **kwargs): if self.override_url: self._cached_url = self.override_url elif self.is_root_node(): - self._cached_url = '/%s/' % self.slug + self._cached_url = "/%s/" % self.slug else: - self._cached_url = '%s%s/' % (self.parent._cached_url, self.slug) + self._cached_url = f"{self.parent._cached_url}{self.slug}/" cached_page_urls[self.id] = self._cached_url - super(BasePage, self).save(*args, **kwargs) - - # If our cached URL changed we need to update all descendants to - # reflect the changes. Since this is a very expensive operation - # on large sites we'll check whether our _cached_url actually changed - # or if the updates weren't navigation related: - if self._cached_url == self._original_cached_url: - return - pages = self.get_descendants().order_by('lft') + with transaction.atomic(): + super().save(*args, **kwargs) + + # If our cached URL changed we need to update all descendants to + # reflect the changes. Since this is a very expensive operation + # on large sites we'll check whether our _cached_url actually changed + # or if the updates weren't navigation related: + if self._cached_url != self._original_cached_url: + pages = self.get_descendants().order_by("lft") + + for page in pages: + if page.override_url: + page._cached_url = page.override_url + else: + # cannot be root node by definition + page._cached_url = "{}{}/".format( + cached_page_urls[page.parent_id], + page.slug, + ) + + cached_page_urls[page.id] = page._cached_url + super(BasePage, page).save() # do not recurse - for page in pages: - if page.override_url: - page._cached_url = page.override_url - else: - # cannot be root node by definition - page._cached_url = '%s%s/' % ( - cached_page_urls[page.parent_id], - page.slug) - - cached_page_urls[page.id] = page._cached_url - super(BasePage, page).save() # do not recurse save.alters_data = True def delete(self, *args, **kwargs): if not settings.FEINCMS_SINGLETON_TEMPLATE_DELETION_ALLOWED: if self.template.singleton: - raise PermissionDenied(_( - 'This %(page_class)s uses a singleton template, and ' - 'FEINCMS_SINGLETON_TEMPLATE_DELETION_ALLOWED=False' % { - 'page_class': self._meta.verbose_name})) - super(BasePage, self).delete(*args, **kwargs) + raise PermissionDenied( + _( + "This %(page_class)s uses a singleton template, and " + "FEINCMS_SINGLETON_TEMPLATE_DELETION_ALLOWED=False" + % {"page_class": self._meta.verbose_name} + ) + ) + super().delete(*args, **kwargs) + delete.alters_data = True def get_absolute_url(self): @@ -294,10 +307,10 @@ def get_absolute_url(self): Return the absolute URL of this page. """ # result url never begins or ends with a slash - url = self._cached_url.strip('/') + url = self._cached_url.strip("/") if url: - return reverse('feincms_handler', args=(url,)) - return reverse('feincms_home') + return reverse("feincms_handler", args=(url,)) + return reverse("feincms_home") def get_navigation_url(self): """ @@ -360,18 +373,20 @@ def register_default_processors(cls): Page experience. """ cls.register_request_processor( - processors.redirect_request_processor, key='redirect') + processors.redirect_request_processor, key="redirect" + ) cls.register_request_processor( - processors.extra_context_request_processor, key='extra_context') + processors.extra_context_request_processor, key="extra_context" + ) # ------------------------------------------------------------------------ class Page(BasePage): class Meta: - ordering = ['tree_id', 'lft'] - verbose_name = _('page') - verbose_name_plural = _('pages') - app_label = 'page' + ordering = ["tree_id", "lft"] + verbose_name = _("page") + verbose_name_plural = _("pages") + app_label = "page" # not yet # permissions = (("edit_page", _("Can edit page metadata")),) diff --git a/feincms/module/page/processors.py b/feincms/module/page/processors.py index 44441e824..6b8016b77 100644 --- a/feincms/module/page/processors.py +++ b/feincms/module/page/processors.py @@ -1,11 +1,10 @@ -from __future__ import absolute_import, print_function, unicode_literals - import logging import re import sys from django.conf import settings as django_settings from django.http import Http404, HttpResponseRedirect +from django.views.decorators.http import condition logger = logging.getLogger(__name__) @@ -18,12 +17,14 @@ def redirect_request_processor(page, request): """ target = page.get_redirect_to_target(request) if target: - extra_path = request._feincms_extra_context.get('extra_path', '/') - if extra_path == '/': + extra_path = request._feincms_extra_context.get("extra_path", "/") + if extra_path == "/": return HttpResponseRedirect(target) logger.debug( "Page redirect on '%s' not taken because extra path '%s' present", - page.get_absolute_url(), extra_path) + page.get_absolute_url(), + extra_path, + ) raise Http404() @@ -31,22 +32,38 @@ def extra_context_request_processor(page, request): """ Fills ``request._feincms_extra_context`` with a few useful variables. """ - request._feincms_extra_context.update({ - # XXX This variable name isn't accurate anymore. - 'in_appcontent_subpage': False, - 'extra_path': '/', - }) + request._feincms_extra_context.update( + { + # XXX This variable name isn't accurate anymore. + "in_appcontent_subpage": False, + "extra_path": "/", + } + ) url = page.get_absolute_url() if request.path != url: - request._feincms_extra_context.update({ - 'in_appcontent_subpage': True, - 'extra_path': re.sub( - '^' + re.escape(url.rstrip('/')), - '', - request.path, - ), - }) + request._feincms_extra_context.update( + { + "in_appcontent_subpage": True, + "extra_path": re.sub( + "^" + re.escape(url.rstrip("/")), "", request.path + ), + } + ) + + +class __DummyResponse(dict): + """ + This is a dummy class with enough behaviour of HttpResponse so we + can use the condition decorator without too much pain. + """ + + @property + def headers(self): + return self + + def has_header(self, what): + return False def etag_request_processor(page, request): @@ -54,19 +71,8 @@ def etag_request_processor(page, request): Short-circuits the request-response cycle if the ETag matches. """ - # XXX is this a performance concern? Does it create a new class - # every time the processor is called or is this optimized to a static - # class?? - class DummyResponse(dict): - """ - This is a dummy class with enough behaviour of HttpResponse so we - can use the condition decorator without too much pain. - """ - def has_header(page, what): - return False - def dummy_response_handler(*args, **kwargs): - return DummyResponse() + return __DummyResponse() def etagger(request, page, *args, **kwargs): etag = page.etag(request) @@ -76,20 +82,17 @@ def lastmodifier(request, page, *args, **kwargs): lm = page.last_modified() return lm - # Unavailable in Django 1.0 -- the current implementation of ETag support - # requires Django 1.1 unfortunately. - from django.views.decorators.http import condition - # Now wrap the condition decorator around our dummy handler: # the net effect is that we will be getting a DummyResponse from # the handler if processing is to continue and a non-DummyResponse # (should be a "304 not modified") if the etag matches. rsp = condition(etag_func=etagger, last_modified_func=lastmodifier)( - dummy_response_handler)(request, page) + dummy_response_handler + )(request, page) # If dummy then don't do anything, if a real response, return and # thus shortcut the request processing. - if not isinstance(rsp, DummyResponse): + if not isinstance(rsp, __DummyResponse): return rsp @@ -101,7 +104,7 @@ def etag_response_processor(page, request, response): """ etag = page.etag(request) if etag is not None: - response['ETag'] = '"' + etag + '"' + response["ETag"] = '"' + etag + '"' def debug_sql_queries_response_processor(verbose=False, file=sys.stderr): @@ -127,9 +130,10 @@ def processor(page, request, response): import sqlparse def print_sql(x): - return sqlparse.format( - x, reindent=True, keyword_case='upper') + return sqlparse.format(x, reindent=True, keyword_case="upper") + except Exception: + def print_sql(x): return x @@ -140,9 +144,10 @@ def print_sql(x): for q in connection.queries: i += 1 if verbose: - print("%d : [%s]\n%s\n" % ( - i, q['time'], print_sql(q['sql'])), file=file) - time += float(q['time']) + print( + "%d : [%s]\n%s\n" % (i, q["time"], print_sql(q["sql"])), file=file + ) + time += float(q["time"]) print("-" * 60, file=file) print("Total: %d queries, %.3f ms" % (i, time), file=file) diff --git a/feincms/module/page/sitemap.py b/feincms/module/page/sitemap.py index 40f2853d4..babe80f4b 100644 --- a/feincms/module/page/sitemap.py +++ b/feincms/module/page/sitemap.py @@ -1,12 +1,10 @@ # ------------------------------------------------------------------------ -# coding=utf-8 # ------------------------------------------------------------------------ -from __future__ import absolute_import, unicode_literals from django.apps import apps -from django.db.models import Max from django.contrib.sitemaps import Sitemap +from django.db.models import Max from feincms import settings @@ -17,10 +15,19 @@ class PageSitemap(Sitemap): The PageSitemap can be used to automatically generate sitemap.xml files for submission to index engines. See http://www.sitemaps.org/ for details. """ - def __init__(self, navigation_only=False, max_depth=0, changefreq=None, - queryset=None, filter=None, extended_navigation=False, - page_model=settings.FEINCMS_DEFAULT_PAGE_MODEL, - *args, **kwargs): + + def __init__( + self, + navigation_only=False, + max_depth=0, + changefreq=None, + queryset=None, + filter=None, + extended_navigation=False, + page_model=settings.FEINCMS_DEFAULT_PAGE_MODEL, + *args, + **kwargs, + ): """ The PageSitemap accepts the following parameters for customisation of the resulting sitemap.xml output: @@ -29,7 +36,7 @@ def __init__(self, navigation_only=False, max_depth=0, changefreq=None, will appear in the site map. * max_depth -- if set to a non-negative integer, will limit the sitemap generated to this page hierarchy depth. - * changefreq -- should be a string or callable specifiying the page + * changefreq -- should be a string or callable specifying the page update frequency, according to the sitemap protocol. * queryset -- pass in a query set to restrict the Pages to include in the site map. @@ -39,7 +46,7 @@ def __init__(self, navigation_only=False, max_depth=0, changefreq=None, extensions. If using PagePretender, make sure to include title, url, level, in_navigation and optionally modification_date. """ - super(PageSitemap, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.depth_cutoff = max_depth self.navigation_only = navigation_only self.changefreq = changefreq @@ -48,7 +55,7 @@ def __init__(self, navigation_only=False, max_depth=0, changefreq=None, if queryset is not None: self.queryset = queryset else: - Page = apps.get_model(*page_model.split('.')) + Page = apps.get_model(*page_model.split(".")) self.queryset = Page.objects.active() def items(self): @@ -60,7 +67,7 @@ def items(self): if callable(base_qs): base_qs = base_qs() - self.max_depth = base_qs.aggregate(Max('level'))['level__max'] or 0 + self.max_depth = base_qs.aggregate(Max("level"))["level__max"] or 0 if self.depth_cutoff > 0: self.max_depth = min(self.depth_cutoff, self.max_depth) @@ -78,14 +85,13 @@ def items(self): for idx, page in enumerate(pages): if self.depth_cutoff > 0 and page.level == self.max_depth: continue - if getattr(page, 'navigation_extension', None): + if getattr(page, "navigation_extension", None): cnt = 0 for p in page.extended_navigation(): depth_too_deep = ( - self.depth_cutoff > 0 and - p.level > self.depth_cutoff) - not_in_nav = ( - self.navigation_only and not p.in_navigation) + self.depth_cutoff > 0 and p.level > self.depth_cutoff + ) + not_in_nav = self.navigation_only and not p.in_navigation if depth_too_deep or not_in_nav: continue cnt += 1 @@ -97,7 +103,7 @@ def items(self): return pages def lastmod(self, obj): - return getattr(obj, 'modification_date', None) + return getattr(obj, "modification_date", None) # the priority is computed of the depth in the tree of a page # may we should make an extension to give control to the user for priority @@ -107,7 +113,7 @@ def priority(self, obj): the site. Top level get highest priority, then each level is decreased by per_level. """ - if getattr(obj, 'override_url', '') == '/': + if getattr(obj, "override_url", "") == "/": prio = 1.0 else: prio = 1.0 - (obj.level + 1) * self.per_level @@ -119,4 +125,5 @@ def priority(self, obj): return "%0.2g" % min(1.0, prio) + # ------------------------------------------------------------------------ diff --git a/feincms/shortcuts.py b/feincms/shortcuts.py index 936460915..1d2afb882 100644 --- a/feincms/shortcuts.py +++ b/feincms/shortcuts.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, unicode_literals - from django.shortcuts import render from feincms.module.page.models import Page @@ -11,6 +9,6 @@ def render_to_response_best_match(request, template_name, dictionary=None): """ dictionary = dictionary or {} - dictionary['feincms_page'] = Page.objects.best_match_for_request(request) + dictionary["feincms_page"] = Page.objects.best_match_for_request(request) return render(request, template_name, dictionary) diff --git a/feincms/signals.py b/feincms/signals.py index 2d4fca135..f75fec0b6 100644 --- a/feincms/signals.py +++ b/feincms/signals.py @@ -1,5 +1,4 @@ # ------------------------------------------------------------------------ -# coding=utf-8 # ------------------------------------------------------------------------ # # Created by Martin J. Laubach on 2011-08-01 @@ -7,14 +6,14 @@ # # ------------------------------------------------------------------------ -from __future__ import absolute_import, unicode_literals from django.dispatch import Signal + # ------------------------------------------------------------------------ # This signal is sent when an item editor managed object is completely # saved, especially including all foreign or manytomany dependencies. -itemeditor_post_save_related = Signal(providing_args=["instance", "created"]) +itemeditor_post_save_related = Signal() # ------------------------------------------------------------------------ diff --git a/feincms/static/feincms/item_editor.css b/feincms/static/feincms/item_editor.css index 260e73d1a..0d2e47680 100644 --- a/feincms/static/feincms/item_editor.css +++ b/feincms/static/feincms/item_editor.css @@ -1,278 +1,308 @@ .navi_tab { - float:left; - padding: 8px 10px; - cursor:pointer; - margin-top:3px; - font-weight: bold; - font-size: 11px; - color: #666; - background: #f6f6f6; - border: 1px solid #eee; - text-transform: uppercase; + float: left; + padding: 8px 10px; + cursor: pointer; + margin-top: 3px; + font-weight: bold; + font-size: 11px; + color: #666; + background: #f6f6f6; + border: 1px solid #eee; + text-transform: uppercase; } .tab_active { - background: #79aec8; - color: white; - border-color: #79aec8; + background: #79aec8; + color: white; + border-color: #79aec8; } -#main { - clear:both; - padding: 10px 10px 10px 10px; - border: 1px solid #eee; - margin: 0 0 10px 0; +#feincmsmain { + clear: both; + padding: 10px 10px 10px 10px; + border: 1px solid #eee; + margin: 0 0 10px 0; } .panel { - display:none; - position:relative; - padding-bottom: 39px; + display: none; + position: relative; + padding-bottom: 39px; } .order-item { - margin: 0 0 10px 0; - position:relative; - + margin: 0 0 10px 0; + position: relative; } .order-item h2 { - background-image: url('img/arrow-move.png'); - background-repeat: no-repeat; - background-position: 6px 9px; + background-image: url("img/arrow-move.png"); + background-repeat: no-repeat; + background-position: 6px 9px; } .order-item .handle { - display: inline-block; - height: 14px; - width: 15px; - cursor: move; + display: inline-block; + height: 14px; + width: 15px; + cursor: move; } .order-item .collapse { - cursor: pointer; - /*color: #444;*/ - font-weight: normal; + cursor: pointer; + /*color: #444;*/ + font-weight: normal; - border-bottom: 1px solid rgba(255, 255, 255, 0.25); + border-bottom: 1px solid rgba(255, 255, 255, 0.25); } .order-item .collapse:hover { - opacity: 0.7; + opacity: 0.7; } .item-delete { - cursor:pointer; - float:right; - margin: 4px 3px 0px 0; + cursor: pointer; + float: right; + margin: 4px 3px 0px 0; } -.highlight, .helper { - height: 34px; - margin: 0 0 10px 0; - border: none; - opacity: 0.3; - background: #79aec8; +.highlight, +.helper { + height: 34px; + margin: 0 0 10px 0; + border: none; + opacity: 0.3; + background: #79aec8; } -.helper{ - height: 25px !important; - opacity: 1; +.helper { + height: 25px !important; + opacity: 1; } .button { - margin:5px; padding:5px; - font-weight: bold; - cursor:pointer; - border: 1px solid #678; + margin: 5px; + padding: 5px; + font-weight: bold; + cursor: pointer; + border: 1px solid #678; } select { - max-width: 580px; + max-width: 580px; } -#main_wrapper { - margin: 10px 0 30px 0; +#feincmsmain_wrapper { + margin: 10px 0 30px 0; } -.clearfix { *zoom:1; } -.clearfix:before, .clearfix:after { content: " "; display: table; } -.clearfix:after { clear: both; } +.clearfix:before, +.clearfix:after { + content: " "; + display: table; +} +.clearfix:after { + clear: both; +} textarea { - width: 580px; - margin-top:5px; - margin-bottom:5px; + width: 580px; + margin-top: 5px; + margin-bottom: 5px; } .inline-group .tabular textarea { - width: auto; + width: auto; } .item-controls { - position: absolute; - top: 2px; - right: 32px; + position: absolute; + top: 2px; + right: 32px; } .item-control-unit { - float:left; + float: left; } .item-controls select { - margin-left: 7px; + margin-left: 7px; } .machine-control { - padding: 5px 10px 5px 10px; - border: 1px solid #ccc; - background-color: #edf3fe; - position:absolute; - left:-11px; - bottom:-30px; - width: 100%; + padding: 5px 10px 5px 10px; + border: 1px solid #eee; + background-color: #f8f8f8; + position: absolute; + left: -11px; + bottom: -30px; + width: 100%; - background: #f8f8f8; - border: 1px solid #eee; - height: 55px; + height: 55px; } .machine-control .button { - padding-top: 6px; - padding-bottom: 6px; + padding-top: 6px; + padding-bottom: 6px; } .control-unit { - float:left; - padding: 0 20px 0 5px; - border-left: 1px solid #eee; + float: left; + padding: 0 20px 0 5px; + border-left: 1px solid #eee; } .control-unit:first-child { - border-left: none; + border-left: none; } .control-unit span { - font-weight:bold; + font-weight: bold; } a.actionbutton { - display: block; - background-repeat: no-repeat; - width:50px; - height:50px; - float:left; - margin: 5px 0 0 20px; - text-indent:-7000px; + display: block; + background-repeat: no-repeat; + width: 50px; + height: 50px; + float: left; + margin: 5px 0 0 20px; + text-indent: -7000px; + transition: none; } -a.richtextcontent { background: url(img/contenttypes.png) no-repeat 0 0; } -a.richtextcontent:hover { background-position: 0 -70px; } +a.richtextcontent { + background: url(img/contenttypes.png) no-repeat 0 0; +} +a.richtextcontent:hover { + background-position: 0 -70px; +} -a.imagecontent { background: url(img/contenttypes.png) no-repeat -70px 0; } -a.imagecontent:hover { background-position: -70px -70px; } +a.imagecontent { + background: url(img/contenttypes.png) no-repeat -70px 0; +} +a.imagecontent:hover { + background-position: -70px -70px; +} -a.gallerycontent { background: url(img/contenttypes.png) no-repeat -140px 0; } -a.gallerycontent:hover { background-position: -140px -70px; } +a.gallerycontent { + background: url(img/contenttypes.png) no-repeat -140px 0; +} +a.gallerycontent:hover { + background-position: -140px -70px; +} -a.oembedcontent { background: url(img/contenttypes.png) no-repeat -280px 0; } -a.oembedcontent:hover { background-position: -280px -70px; } +a.oembedcontent { + background: url(img/contenttypes.png) no-repeat -280px 0; +} +a.oembedcontent:hover { + background-position: -280px -70px; +} -a.pdfcontent { background: url(img/contenttypes.png) no-repeat -210px 0; } -a.pdfcontent:hover { background-position: -210px -70px; } +a.pdfcontent { + background: url(img/contenttypes.png) no-repeat -210px 0; +} +a.pdfcontent:hover { + background-position: -210px -70px; +} -a.audiocontent { background: url(img/contenttypes.png) no-repeat -350px 0; } -a.audiocontent:hover { background-position: -350px -70px; } +a.audiocontent { + background: url(img/contenttypes.png) no-repeat -350px 0; +} +a.audiocontent:hover { + background-position: -350px -70px; +} .control-unit select { - float: left; - position: relative; - top: 13px; + float: left; + position: relative; + top: 13px; } .empty-machine-msg { - margin:10px 0px 20px 20px; - font-size:14px; + margin: 10px 0px 20px 20px; + font-size: 14px; } td span select { - width:600px; + width: 600px; } - .change-template-button { - margin-left: 7em; - padding-left: 30px; + margin-left: 7em; + padding-left: 30px; } /* Allow nested lists in error items */ ul.errorlist ul { - margin-left: 1em; - padding-left: 0; - list-style-type: square; + margin-left: 1em; + padding-left: 0; + list-style-type: square; } ul.errorlist li li { - /* Avoid repeating the warning image every time*/ - background-image:none; - padding: 0; + /* Avoid repeating the warning image every time*/ + background-image: none; + padding: 0; } -div.order-machine div.inline-related > h3{ - display: none; +div.order-machine div.inline-related > h3 { + display: none; } .hidden-form-row { - display: none; + display: none; } #extension_options_wrapper { border-bottom: 1px solid #eee; } -#extension_options>.module.aligned { +#extension_options > .module.aligned { border-top: 1px solid #eee; margin-bottom: -1px; } - /* various overrides */ -#id_redirect_to { width: 20em; } /* raw_id_fields act-a-like for redirect_to */ +#id_redirect_to { + width: 20em; +} /* raw_id_fields act-a-like for redirect_to */ /* overwrite flat theme default label width because of problems with the CKEditor */ -.aligned .text label { width: auto; } - +.aligned .text label { + width: auto; +} /* django suit hacks */ /*********************/ -#suit-center #main { - clear:none; +#suit-center #feincmsmain { + clear: none; } #suit-center .panel { - padding-top: 15px; + padding-top: 15px; } #suit-center .form-horizontal .inline-related fieldset { - margin-top: 10px; + margin-top: 10px; } #suit-center .panel h2 { - color: white; - font-size: 13px; - line-height: 12px; - margin-left: 0; - text-shadow: none; + color: white; + font-size: 13px; + line-height: 12px; + margin-left: 0; + text-shadow: none; } #suit-center .item-delete { - margin: 3px 5px 0 0; + margin: 3px 5px 0 0; } #suit-center .order-item .handle { - height: 36px; + height: 36px; } #suit-center .order-machine .order-item { - margin-top: 10px; + margin-top: 10px; } diff --git a/feincms/static/feincms/item_editor.js b/feincms/static/feincms/item_editor.js index 91fc5706c..e8dd30dd7 100644 --- a/feincms/static/feincms/item_editor.js +++ b/feincms/static/feincms/item_editor.js @@ -1,602 +1,673 @@ -// IE<9 lacks Array.prototype.indexOf -if (!Array.prototype.indexOf) { - Array.prototype.indexOf = function(needle) { - for (i=0, l=this.length; i { + // Patch up urlify maps to generate nicer slugs in german + if (typeof Downcoder !== "undefined") { + Downcoder.Initialize() + Downcoder.map.ö = Downcoder.map.Ö = "oe" + Downcoder.map.ä = Downcoder.map.Ä = "ae" + Downcoder.map.ü = Downcoder.map.Ü = "ue" + } + + function feincms_gettext(s) { + // Unfortunately, we cannot use Django's jsi18n view for this + // because it only sends translations from the package + // "django.conf" -- our own djangojs domain strings won't be + // picked up + + if (FEINCMS_ITEM_EDITOR_GETTEXT[s]) return FEINCMS_ITEM_EDITOR_GETTEXT[s] + return s + } + + function create_new_item_from_form(form, modname, modvar) { + const fieldset = $("
").addClass( + `module aligned order-item item-wrapper-${modvar}`, + ) + const original_id_id = `#id_${form.attr("id")}-id` + + const wrp = ["

"] + // If original has delete checkbox or this is a freshly added CT? Add delete link! + if ($(".delete", form).length || !$(original_id_id, form).val()) { + wrp.push(``) } - - function create_new_item_from_form(form, modname, modvar){ - - var fieldset = $("
").addClass("module aligned order-item item-wrapper-" + modvar); - var original_id_id = '#id_' + form.attr('id') + '-id'; - - var wrp = ['

']; - // If original has delete checkbox or this is a freshly added CT? Add delete link! - if($('.delete', form).length || !$(original_id_id, form).val()) { - wrp.push(''); - } - wrp.push(' '+modname+'

'); - wrp.push('
'); - fieldset.append(wrp.join("")); - - fieldset.children(".item-content").append(form); //relocates, not clone - - $("
").addClass("item-controls").appendTo(fieldset); - - return fieldset; - } - - - SELECTS = {}; - function save_content_type_selects() { - $('#main>.panel').each(function() { - SELECTS[this.id.replace(/_body$/, '')] = $("select[name=order-machine-add-select]", this).clone().removeAttr("name"); - }); - } - - function update_item_controls(item, target_region_id){ - var item_controls = item.find(".item-controls"); - item_controls.empty(); - - // Insert control unit - var insert_control = $("
").addClass("item-control-unit"); - var select_content = SELECTS[REGION_MAP[target_region_id]].clone(); - - select_content.change(function() { - var modvar = select_content.val(); - var modname = select_content.find("option:selected").html(); - var new_fieldset = create_new_fieldset_from_module(modvar, modname); - add_fieldset(target_region_id, new_fieldset, {where:'insertBefore', relative_to:item, animate:true}); - update_item_controls(new_fieldset, target_region_id); - - select_content.val(''); - }); - insert_control.append(select_content); - item_controls.append(insert_control); - - // Move control unit - if (REGION_MAP.length > 1) { - var wrp = []; - wrp.push('
'); - - var move_control = $(wrp.join("")); - move_control.find("select").change(function(){ - var move_to = $(this).val(); - move_item(REGION_MAP.indexOf(move_to), item); - }); - item_controls.append(move_control); // Add new one + wrp.push( + ` ${modname}

`, + ) + wrp.push('
') + fieldset.append(wrp.join("")) + + fieldset.children(".item-content").append(form) //relocates, not clone + + $("
").addClass("item-controls").appendTo(fieldset) + + return fieldset + } + + const SELECTS = {} + function save_content_type_selects() { + $("#feincmsmain>.panel").each(function () { + SELECTS[this.id.replace(/_body$/, "")] = $( + "select[name=order-machine-add-select]", + this, + ) + .clone() + .removeAttr("name") + }) + } + + function update_item_controls(item, target_region_id) { + const item_controls = item.find(".item-controls") + item_controls.empty() + + // Insert control unit + const insert_control = $("
").addClass("item-control-unit") + const select_content = SELECTS[REGION_MAP[target_region_id]].clone() + + select_content.change(() => { + const modvar = select_content.val() + const modname = select_content.find("option:selected").html() + const new_fieldset = create_new_fieldset_from_module(modvar, modname) + add_fieldset(target_region_id, new_fieldset, { + where: "insertBefore", + relative_to: item, + animate: true, + }) + update_item_controls(new_fieldset, target_region_id) + + select_content.val("") + }) + insert_control.append(select_content) + item_controls.append(insert_control) + + // Move control unit + if (REGION_MAP.length > 1) { + const wrp = [] + wrp.push( + '
") + + const move_control = $(wrp.join("")) + move_control.find("select").change(function () { + const move_to = $(this).val() + move_item(REGION_MAP.indexOf(move_to), item) + }) + item_controls.append(move_control) // Add new one } + } + function create_new_fieldset_from_module(modvar, modname) { + const new_form = create_new_spare_form(modvar) + return create_new_item_from_form(new_form, modname, modvar) + } - function create_new_fieldset_from_module(modvar, modname) { - var new_form = create_new_spare_form(modvar); - return create_new_item_from_form(new_form, modname, modvar); - } - - function add_fieldset(region_id, item, how){ - /* `how` should be an object. + function add_fieldset(region_id, item, how) { + /* `how` should be an object. `how.where` should be one of: - 'append' -- last region - 'prepend' -- first region - 'insertBefore' -- insert before relative_to - 'insertAfter' -- insert after relative_to */ - // Default parameters - if (how) $.extend({ - where: 'append', - relative_to: undefined, - animate: false - }, how); - - item.hide(); - if(how.where == 'append' || how.where == 'prepend'){ - $("#"+ REGION_MAP[region_id] +"_body").children("div.order-machine")[how.where](item); - } - else if(how.where == 'insertBefore' || how.where == 'insertAfter'){ - if(how.relative_to){ - item[how.where](how.relative_to); - } - else{ - window.alert('DEBUG: invalid add_fieldset usage'); - return; - } - } - else{ - window.alert('DEBUG: invalid add_fieldset usage'); - return; - } - set_item_field_value(item, "region-choice-field", region_id); - init_contentblocks(); - - if (how.animate) { - item.fadeIn(800); - } - else { - item.show(); - } - } - - function create_new_spare_form(modvar) { - var old_form_count = parseInt($('#id_'+modvar+'_set-TOTAL_FORMS').val(), 10); - // **** UGLY CODE WARNING, avert your gaze! **** - // for some unknown reason, the add-button click handler function - // fails on the first triggerHandler call in some rare cases; - // we can detect this here and retry: - for(var i = 0; i < 2; i++){ - // Use Django's built-in inline spawing mechanism (Django 1.2+) - // must use django.jQuery since the bound function lives there: - django.jQuery('#'+modvar+'_set-group').find( - 'div.add-row > a').triggerHandler('click'); - var new_form_count = parseInt($('#id_'+modvar+'_set-TOTAL_FORMS').val(), 10); - if(new_form_count > old_form_count){ - return $('#'+modvar+'_set-'+(new_form_count-1)); - } - } - } - - function set_item_field_value(item, field, value) { - // item: DOM object for the item's fieldset. - // field: "order-field" | "delete-field" | "region-choice-field" - if (field=="delete-field") - item.find("."+field).attr("checked",value); - else if (field=="region-choice-field") { - var old_region_id = REGION_MAP.indexOf(item.find("."+field).val()); - item.find("."+field).val(REGION_MAP[value]); - - // show/hide the empty machine message in the source and - // target region. - old_region_item = $("#"+REGION_MAP[old_region_id]+"_body"); - if (old_region_item.children("div.order-machine").children().length == 0) - old_region_item.children("div.empty-machine-msg").show(); - else - old_region_item.children("div.empty-machine-msg").hide(); - - new_region_item = $("#"+REGION_MAP[value]+"_body"); - new_region_item.children("div.empty-machine-msg").hide(); - } - else - item.find("."+field).val(value); - } - - function move_item(region_id, item) { - poorify_rich(item); - item.fadeOut(800, function() { - add_fieldset(region_id, item, {where:'append'}); - richify_poor(item); - update_item_controls(item, region_id); - item.show(); - }); - } - - function poorify_rich(item){ - item.children(".item-content").hide(); - - for (var i=0; i v2 ? 1 : -1; + if (how.animate) { + item.fadeIn(800) + } else { + item.show() } - - function give_ordering_to_content_types() { - for (var i=0; i old_form_count) { + return $(`#${modvar}_set-${new_form_count - 1}`) } } - - function order_content_types_in_regions() { - for (var i=0; i { + add_fieldset(region_id, item, { where: "append" }) + richify_poor(item) + update_item_controls(item, region_id) + item.show() + }) + } + + function poorify_rich(item) { + item.children(".item-content").hide() + + for (let i = 0; i < contentblock_move_handlers.poorify.length; i++) + contentblock_move_handlers.poorify[i](item) + } + + function richify_poor(item) { + item.children(".item-content").show() + + for (let i = 0; i < contentblock_move_handlers.richify.length; i++) + contentblock_move_handlers.richify[i](item) + } + + function sort_by_ordering(e1, e2) { + const v1 = parseInt($(".order-field", e1).val(), 10) || 0 + const v2 = parseInt($(".order-field", e2).val(), 10) || 0 + return v1 > v2 ? 1 : -1 + } + + function give_ordering_to_content_types() { + for (let i = 0; i < REGION_MAP.length; i++) { + const container = $(`#${REGION_MAP[i]}_body div.order-machine`) + for (let j = 0; j < container.children().length; j++) { + set_item_field_value( + container.find(`fieldset.order-item:eq(${j})`), + "order-field", + j, + ) } } - - function init_contentblocks() { - for(var i=0; i