├── .coveragerc ├── .github └── workflows │ └── tests.yaml ├── .gitignore ├── .tx └── config ├── AUTHORS ├── CHANGES.rst ├── LICENSE.rst ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── _ext │ └── djangodummy │ │ ├── __init__.py │ │ ├── requirements.txt │ │ ├── settings.py │ │ └── templates │ │ └── .gitignore ├── api │ ├── adminui.rst │ ├── adminui.utils.rst │ ├── extensions.rst │ ├── index.rst │ ├── integration │ │ └── fluent_contents.rst │ ├── models.navigation.rst │ ├── models.rst │ ├── pagetypes.fluentpage.admin.rst │ ├── pagetypes.fluentpage.models.rst │ ├── sitemaps.rst │ ├── templatetags │ │ ├── appurl_tags.rst │ │ └── fluent_pages_tags.rst │ ├── urlresolvers.rst │ └── views.rst ├── changelog.rst ├── conf.py ├── configuration.rst ├── dependencies.rst ├── images │ ├── newpagetypes │ │ ├── shoppage-add.png │ │ └── shoppage-admin.png │ └── pagetypes │ │ ├── flatpage-admin.png │ │ ├── fluentpage-admin.png │ │ ├── redirectnode-admin.png │ │ └── textfile-admin.png ├── index.rst ├── lowlevel.rst ├── make.bat ├── management.rst ├── multilingual.rst ├── newpagetypes │ ├── admin.rst │ ├── fluent_contents.rst │ ├── index.rst │ ├── models.rst │ ├── rendering.rst │ └── urls.rst ├── pagetypes │ ├── flatpage.rst │ ├── fluentpage.rst │ ├── index.rst │ ├── others.rst │ ├── redirectnode.rst │ └── textfile.rst ├── quickstart.rst ├── sitemaps.rst └── templatetags.rst ├── example ├── README.rst ├── __init__.py ├── fixtures │ ├── create.sh │ ├── shop_example.json │ └── welcome.json ├── manage.py ├── requirements.txt ├── settings.py ├── simpleshop │ ├── __init__.py │ ├── admin.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── page_type_plugins.py │ ├── templates │ │ └── products │ │ │ ├── product_details.html │ │ │ └── productcategorypage.html │ └── views.py ├── theme1 │ ├── __init__.py │ ├── static │ │ └── theme1 │ │ │ └── site.css │ └── templates │ │ └── theme1 │ │ ├── base.html │ │ ├── pages │ │ ├── standard-twocols.html │ │ └── standard.html │ │ └── parts │ │ └── sidebar.html └── urls.py ├── fluent_pages ├── __init__.py ├── admin │ ├── __init__.py │ └── utils.py ├── adminui │ ├── __init__.py │ ├── htmlpageadmin.py │ ├── overrides.py │ ├── pageadmin.py │ ├── pagelayoutadmin.py │ ├── urlnodechildadmin.py │ ├── urlnodeparentadmin.py │ └── utils.py ├── apps.py ├── appsettings.py ├── extensions │ ├── __init__.py │ ├── pagetypebase.py │ └── pagetypepool.py ├── forms │ ├── __init__.py │ └── fields.py ├── integration │ ├── __init__.py │ └── fluent_contents │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── models.py │ │ ├── page_type_plugins.py │ │ └── tests.py ├── locale │ ├── en │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── es │ │ └── LC_MESSAGES │ │ │ └── django.po │ └── nl │ │ └── LC_MESSAGES │ │ └── django.po ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── make_language_redirects.py │ │ ├── rebuild_page_tree.py │ │ └── remove_stale_pages.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_add_htmlpage_meta_image.py │ ├── 0003_set_htmlpage_defaults.py │ ├── 0004_add_htmlpage_not_null.py │ ├── 0005_author_on_delete_set_null.py │ ├── 0006_remove_mptt_indices.py │ ├── 0007_use_parler_transactionsfk.py │ └── __init__.py ├── models │ ├── __init__.py │ ├── db.py │ ├── fields.py │ ├── managers.py │ ├── navigation.py │ └── utils.py ├── pagetypes │ ├── __init__.py │ ├── flatpage │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── appsettings.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── page_type_plugins.py │ │ ├── templates │ │ │ ├── admin │ │ │ │ └── fluent_pages │ │ │ │ │ └── pagetypes │ │ │ │ │ └── flatpage │ │ │ │ │ └── change_form.html │ │ │ └── fluent_pages │ │ │ │ └── pagetypes │ │ │ │ └── flatpage │ │ │ │ └── default.html │ │ └── tests.py │ ├── fluentpage │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── page_type_plugins.py │ │ ├── static │ │ │ └── fluent_pages │ │ │ │ └── fluentpage │ │ │ │ └── fluent_layouts.js │ │ ├── templates │ │ │ └── admin │ │ │ │ └── fluentpage │ │ │ │ └── change_form.html │ │ ├── tests.py │ │ └── widgets.py │ ├── redirectnode │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── page_type_plugins.py │ │ └── tests.py │ └── textfile │ │ ├── __init__.py │ │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_add_translation_model.py │ │ ├── 0003_migrate_translatable_fields.py │ │ ├── 0004_remove_untranslated_fields.py │ │ └── __init__.py │ │ ├── models.py │ │ ├── page_type_plugins.py │ │ └── tests.py ├── sitemaps.py ├── static │ └── fluent_pages │ │ └── admin │ │ └── pagetree.css ├── templates │ ├── admin │ │ └── fluent_pages │ │ │ ├── integration │ │ │ └── fluent_contents │ │ │ │ └── base_change_form.html │ │ │ └── page │ │ │ ├── base_change_form.html │ │ │ ├── change_form.html │ │ │ └── change_list.html │ ├── fluent_pages │ │ ├── base.html │ │ ├── example-base.html │ │ ├── example-cmspage.html │ │ ├── intro_page.html │ │ └── parts │ │ │ ├── breadcrumb.html │ │ │ └── menu.html │ └── robots.txt ├── templatetags │ ├── __init__.py │ ├── appurl_tags.py │ └── fluent_pages_tags.py ├── tests │ ├── __init__.py │ ├── test_admin.py │ ├── test_forms.py │ ├── test_menu.py │ ├── test_modeldata.py │ ├── test_plugins.py │ ├── test_templatetags.py │ ├── test_urldispatcher.py │ ├── testapp │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── page_type_plugins.py │ │ ├── templates │ │ │ ├── 404.html │ │ │ └── testapp │ │ │ │ ├── base.html │ │ │ │ ├── json_menu.html │ │ │ │ └── simpletextpage.html │ │ ├── urls.py │ │ ├── urls_nonroot.py │ │ └── urls_webshop.py │ └── utils.py ├── urlresolvers.py ├── urls.py └── views │ ├── __init__.py │ ├── dispatcher.py │ ├── mixins.py │ └── seo.py ├── makemessages.py ├── pyproject.toml ├── runtests.py ├── setup.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = fluent_pages/ 3 | omit = 4 | */migrations/* 5 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: CI Testing 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | push: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | test: 12 | name: "Python ${{ matrix.python }} Django ${{ matrix.django }}" 13 | runs-on: ubuntu-latest 14 | strategy: 15 | # max-parallel: 8 # default is max available 16 | fail-fast: false 17 | matrix: 18 | include: 19 | # Django 3.2 20 | - django: "3.2" 21 | python: "3.8" 22 | # Django 4.0 23 | - django: "4.2" 24 | python: "3.10" 25 | # Django 5.0 26 | - django: "5.0a1" 27 | python: "3.10" 28 | 29 | steps: 30 | - name: Install gettext 31 | run: sudo apt-get install -y gettext 32 | 33 | - name: Checkout code 34 | uses: actions/checkout@v2 35 | 36 | - name: Setup Python ${{ matrix.python }} 37 | uses: actions/setup-python@v2 38 | with: 39 | python-version: ${{ matrix.python }} 40 | 41 | - name: Install Packages 42 | run: | 43 | python -m pip install -U pip 44 | python -m pip install "Django~=${{ matrix.django }}" codecov -e .[tests] 45 | 46 | - name: Run Tests 47 | run: | 48 | echo "Python ${{ matrix.python }} / Django ${{ matrix.django }}" 49 | coverage run --rcfile=.coveragerc runtests.py 50 | codecov 51 | continue-on-error: ${{ contains(matrix.django, '5.0') }} 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.mo 4 | *.db 5 | *.egg-info/ 6 | *.egg/ 7 | .coverage 8 | .project 9 | .idea/ 10 | .pydevproject 11 | .idea/workspace.xml 12 | .tox/ 13 | .DS_Store 14 | build/ 15 | dist/ 16 | docs/_build/ 17 | htmlcov/ 18 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | 4 | [django-fluent-pages.djangopo] 5 | file_filter = fluent_pages/locale//LC_MESSAGES/django.po 6 | source_file = fluent_pages/locale/en/LC_MESSAGES/django.po 7 | source_lang = en 8 | type = PO 9 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Original authors 2 | ================ 3 | 4 | * Diederik van der Boor 5 | 6 | 7 | Contributions by 8 | ================ 9 | 10 | * Adam Chainz 11 | * Adam Wentz (@floppya) 12 | * Basil Shubin (@bashu) 13 | * Ben Konrath 14 | * Chad Shryock 15 | * David Loaiza 16 | * Evan Borgstrom (@borgstrom) 17 | * James Murty 18 | * Jeroen Dekkers (@dekkers) 19 | * Jonathan Potter 20 | * Maarten Draijer (@maartendraijer) 21 | * Mario Rosa (@vinnyrose) 22 | * Patrick Taylor (@huxley) 23 | * Samuel Bishop (@techdragon) 24 | * Stuart Dines 25 | * Tai Lee 26 | -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | LICENSE 3 | ======= 4 | 5 | Copyright © 2011, Edoburu 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this software except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include README.rst 3 | include LICENSE.rst 4 | recursive-include fluent_pages/locale *.mo *.po 5 | recursive-include fluent_pages/pagetypes/*/static *.js 6 | recursive-include fluent_pages/pagetypes/*/templates *.html 7 | recursive-include fluent_pages/static *.js *.css 8 | recursive-include fluent_pages/templates *.html *.txt 9 | recursive-include fluent_pages/tests/testapp/templates *.html 10 | -------------------------------------------------------------------------------- /docs/_ext/djangodummy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-fluent/django-fluent-pages/621d756542f23d1e2f7679bffe6fcb81b82df005/docs/_ext/djangodummy/__init__.py -------------------------------------------------------------------------------- /docs/_ext/djangodummy/requirements.txt: -------------------------------------------------------------------------------- 1 | # for readthedocs 2 | Django == 4.2.6 3 | django-fluent-contents == 3.0 4 | django-fluent-utils == 3.0.1 5 | django-mptt == 0.14.0 6 | django-parler == 2.3 7 | django-polymorphic-tree == 2.1 8 | django-polymorphic == 3.1.0 9 | django-tag-parser == 3.2 10 | sphinxcontrib-django == 2.5 11 | -------------------------------------------------------------------------------- /docs/_ext/djangodummy/settings.py: -------------------------------------------------------------------------------- 1 | # Settings file to allow parsing API documentation of Django modules, 2 | # and provide defaults to use in the documentation. 3 | # 4 | # This file is placed in a subdirectory, 5 | # so the docs root won't be detected by find_packages() 6 | import os 7 | 8 | # Display sane URLs in the docs: 9 | STATIC_URL = "/static/" 10 | 11 | # Required to pass module tests 12 | FLUENT_PAGES_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "templates/") 13 | 14 | # Required by Django 15 | SECRET_KEY = "foo" 16 | SITE_ID = 1 17 | 18 | INSTALLED_APPS = [ 19 | "fluent_pages", 20 | "django.contrib.admin", 21 | "django.contrib.auth", 22 | "django.contrib.contenttypes", 23 | "django.contrib.sites", 24 | "mptt", 25 | "polymorphic", 26 | "polymorphic_tree", 27 | ] 28 | 29 | MIDDLEWARE = [ 30 | "django.contrib.sessions.middleware.SessionMiddleware", 31 | "django.contrib.auth.middleware.AuthenticationMiddleware", 32 | "django.contrib.messages.middleware.MessageMiddleware", 33 | "django.middleware.locale.LocaleMiddleware", # / will be redirected to // 34 | ] 35 | 36 | TEMPLATES = [ 37 | { 38 | "BACKEND": "django.template.backends.django.DjangoTemplates", 39 | "DIRS": (), 40 | "OPTIONS": { 41 | "loaders": ( 42 | "django.template.loaders.filesystem.Loader", 43 | "django.template.loaders.app_directories.Loader", 44 | ) 45 | }, 46 | } 47 | ] 48 | -------------------------------------------------------------------------------- /docs/_ext/djangodummy/templates/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-fluent/django-fluent-pages/621d756542f23d1e2f7679bffe6fcb81b82df005/docs/_ext/djangodummy/templates/.gitignore -------------------------------------------------------------------------------- /docs/api/adminui.rst: -------------------------------------------------------------------------------- 1 | .. _fluent_pages.adminui: 2 | 3 | fluent_pages.adminui 4 | ==================== 5 | 6 | .. automodule:: fluent_pages.adminui 7 | 8 | The ``PageAdmin`` class 9 | ----------------------- 10 | 11 | .. autoclass:: fluent_pages.adminui.PageAdmin 12 | :members: 13 | :show-inheritance: 14 | 15 | .. autoclass:: fluent_pages.adminui.DefaultPageChildAdmin 16 | :members: 17 | :show-inheritance: 18 | 19 | 20 | The ``PageAdminForm`` class 21 | --------------------------- 22 | 23 | .. autoclass:: fluent_pages.adminui.PageAdminForm 24 | :members: 25 | 26 | 27 | The ``HtmlPageAdmin`` class 28 | --------------------------- 29 | 30 | .. autoclass:: fluent_pages.adminui.HtmlPageAdmin 31 | :members: 32 | :show-inheritance: 33 | -------------------------------------------------------------------------------- /docs/api/adminui.utils.rst: -------------------------------------------------------------------------------- 1 | .. _fluent_pages.adminui.utils: 2 | 3 | fluent_pages.adminui.utils 4 | ========================== 5 | 6 | .. automodule:: fluent_pages.adminui.utils 7 | 8 | .. autofunction:: fluent_pages.adminui.utils.get_page_admin_url 9 | 10 | .. autofunction:: fluent_pages.adminui.utils.get_current_edited_page 11 | -------------------------------------------------------------------------------- /docs/api/extensions.rst: -------------------------------------------------------------------------------- 1 | fluent_pages.extensions 2 | ======================= 3 | 4 | .. automodule:: fluent_pages.extensions 5 | 6 | The ``PageTypePlugin`` class 7 | ---------------------------- 8 | 9 | .. autoclass:: fluent_pages.extensions.PageTypePlugin 10 | :members: 11 | 12 | The ``PageTypePool`` class 13 | -------------------------- 14 | 15 | .. autoclass:: fluent_pages.extensions.PageTypePool 16 | :members: 17 | 18 | The ``page_type_pool`` attribute 19 | -------------------------------- 20 | 21 | .. attribute:: fluent_pages.extensions.page_type_pool 22 | 23 | The global plugin pool, a instance of the :class:`PluginPool` class. 24 | 25 | Other classes 26 | ------------- 27 | 28 | .. autoexception:: fluent_pages.extensions.PageTypeAlreadyRegistered 29 | 30 | .. autoexception:: fluent_pages.extensions.PageTypeNotFound 31 | 32 | -------------------------------------------------------------------------------- /docs/api/index.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | API documentation 4 | ================= 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | adminui 10 | adminui.utils 11 | extensions 12 | integration/fluent_contents 13 | models 14 | models.navigation 15 | templatetags/appurl_tags 16 | templatetags/fluent_pages_tags 17 | sitemaps 18 | urlresolvers 19 | views 20 | 21 | 22 | Deprecated modules 23 | ------------------ 24 | 25 | .. toctree:: 26 | :maxdepth: 2 27 | 28 | pagetypes.fluentpage.admin 29 | pagetypes.fluentpage.models 30 | 31 | .. 32 | Kept out of public API: 33 | - forms 34 | - utils.* 35 | -------------------------------------------------------------------------------- /docs/api/integration/fluent_contents.rst: -------------------------------------------------------------------------------- 1 | .. _fluent_pages.integration.fluent_contents: 2 | 3 | fluent_pages.integration.fluent_contents 4 | ======================================== 5 | 6 | .. automodule:: fluent_pages.integration.fluent_contents 7 | 8 | The ``FluentContentsPageAdmin`` class 9 | ------------------------------------- 10 | 11 | .. autoclass:: fluent_pages.integration.fluent_contents.admin.FluentContentsPageAdmin 12 | :members: 13 | 14 | The ``FluentContentsPage`` class 15 | -------------------------------- 16 | 17 | .. autoclass:: fluent_pages.integration.fluent_contents.models.FluentContentsPage 18 | :members: 19 | 20 | The ``FluentContentsPagePlugin`` class 21 | -------------------------------------- 22 | 23 | .. autoclass:: fluent_pages.integration.fluent_contents.page_type_plugins.FluentContentsPagePlugin 24 | :members: 25 | -------------------------------------------------------------------------------- /docs/api/models.navigation.rst: -------------------------------------------------------------------------------- 1 | .. _fluent_pages.models.navigation: 2 | 3 | fluent_pages.models.navigation 4 | ============================== 5 | 6 | .. automodule:: fluent_pages.models.navigation 7 | 8 | The ``NavigationNode`` class 9 | ---------------------------- 10 | 11 | .. autoclass:: fluent_pages.models.navigation.NavigationNode 12 | :members: 13 | 14 | The ``PageNavigationNode`` class 15 | -------------------------------- 16 | 17 | .. autoclass:: fluent_pages.models.navigation.PageNavigationNode 18 | :members: 19 | 20 | -------------------------------------------------------------------------------- /docs/api/models.rst: -------------------------------------------------------------------------------- 1 | .. _fluent_pages.models: 2 | 3 | fluent_pages.models 4 | ===================== 5 | 6 | .. automodule:: fluent_pages.models 7 | 8 | The ``UrlNode`` class 9 | ------------------------------------ 10 | 11 | .. autoclass:: fluent_pages.models.UrlNode 12 | :members: 13 | 14 | The ``Page`` class 15 | ----------------------------------- 16 | 17 | .. autoclass:: fluent_pages.models.Page 18 | :members: 19 | 20 | The ``HtmlPage`` class 21 | ----------------------------------- 22 | 23 | .. autoclass:: fluent_pages.models.HtmlPage 24 | :members: 25 | 26 | The ``PageLayout`` class 27 | ----------------------------------- 28 | 29 | .. autoclass:: fluent_pages.models.PageLayout 30 | :members: 31 | 32 | The ``UrlNodeManager`` class 33 | ------------------------------------ 34 | 35 | .. autoclass:: fluent_pages.models.UrlNodeManager 36 | :members: 37 | -------------------------------------------------------------------------------- /docs/api/pagetypes.fluentpage.admin.rst: -------------------------------------------------------------------------------- 1 | .. _fluent_pages.pagetypes.fluentpage.admin: 2 | 3 | fluent_pages.pagetypes.fluentpage.admin 4 | ======================================= 5 | 6 | .. automodule:: fluent_pages.pagetypes.fluentpage.admin 7 | 8 | The ``FluentPageAdmin`` class 9 | ----------------------------- 10 | 11 | .. autoclass:: fluent_pages.pagetypes.fluentpage.admin.FluentPageAdmin 12 | :members: 13 | -------------------------------------------------------------------------------- /docs/api/pagetypes.fluentpage.models.rst: -------------------------------------------------------------------------------- 1 | .. _fluent_pages.pagetypes.fluentpage.models: 2 | 3 | fluent_pages.pagetypes.fluentpage.models 4 | ======================================== 5 | 6 | .. automodule:: fluent_pages.pagetypes.fluentpage.models 7 | 8 | The ``AbstractFluentPage`` class 9 | -------------------------------- 10 | 11 | .. autoclass:: fluent_pages.pagetypes.fluentpage.models.AbstractFluentPage 12 | :members: 13 | 14 | The ``FluentPage`` class 15 | ------------------------ 16 | 17 | .. autoclass:: fluent_pages.pagetypes.fluentpage.models.FluentPage 18 | :members: 19 | -------------------------------------------------------------------------------- /docs/api/sitemaps.rst: -------------------------------------------------------------------------------- 1 | .. _fluent_pages.sitemaps: 2 | 3 | fluent_pages.sitemaps 4 | ===================== 5 | 6 | .. automodule:: fluent_pages.sitemaps 7 | 8 | The ``PageSitemap`` class 9 | ------------------------- 10 | 11 | .. autoclass:: fluent_pages.sitemaps.PageSitemap 12 | :members: 13 | 14 | -------------------------------------------------------------------------------- /docs/api/templatetags/appurl_tags.rst: -------------------------------------------------------------------------------- 1 | fluent_pages.templatetags.appurl_tags 2 | ============================================= 3 | 4 | .. automodule:: fluent_pages.templatetags.appurl_tags 5 | -------------------------------------------------------------------------------- /docs/api/templatetags/fluent_pages_tags.rst: -------------------------------------------------------------------------------- 1 | fluent_pages.templatetags.fluent_pages_tags 2 | ============================================= 3 | 4 | Template tags to request fluent page content in the template. 5 | Load this module using: 6 | 7 | .. code-block:: html+django 8 | 9 | {% load fluent_pages_tags %} 10 | 11 | 12 | The ``render_breadcrumb`` tag 13 | --------------------------------- 14 | 15 | Render the breadcrumb of the site, starting at the current ``page``. 16 | This function either uses the default template, 17 | or a custom template if the ``template`` argument is provided. 18 | 19 | .. code-block:: html+django 20 | 21 | {% render_breadcrumb template="fluent_pages/parts/breadcrumb.html" %} 22 | 23 | 24 | The ``render_menu`` tag 25 | ----------------------------------- 26 | 27 | Render the menu of the site. The ``max_depth``, ``parent`` and ``template`` arguments are optional. 28 | 29 | .. code-block:: html+django 30 | 31 | {% render_menu max_depth=1 parent="/documentation/" template="fluent_pages/parts/menu.html" %} 32 | 33 | 34 | The ``get_fluent_page_vars`` tag 35 | ----------------------------------- 36 | 37 | Introduces the ``site`` and ``page`` variables in the template. 38 | This can be used for pages that are rendered by a separate application. 39 | 40 | .. code-block:: html+django 41 | 42 | {% get_fluent_page_vars %} -------------------------------------------------------------------------------- /docs/api/urlresolvers.rst: -------------------------------------------------------------------------------- 1 | .. _fluent_pages.urlresolvers: 2 | 3 | fluent_pages.urlresolvers 4 | ========================== 5 | 6 | .. automodule:: fluent_pages.urlresolvers 7 | 8 | .. autofunction:: fluent_pages.urlresolvers.app_reverse 9 | 10 | .. autofunction:: fluent_pages.urlresolvers.mixed_reverse 11 | 12 | .. autofunction:: fluent_pages.urlresolvers.clear_app_reverse_cache 13 | 14 | Other classes 15 | ------------- 16 | 17 | .. autoexception:: fluent_pages.urlresolvers.MultipleReverseMatch 18 | 19 | .. autoexception:: fluent_pages.urlresolvers.PageTypeNotMounted 20 | 21 | -------------------------------------------------------------------------------- /docs/api/views.rst: -------------------------------------------------------------------------------- 1 | .. _fluent_pages.views: 2 | 3 | fluent_pages.views 4 | ================== 5 | 6 | .. automodule:: fluent_pages.views 7 | 8 | The ``CurrentPageMixin`` class 9 | ------------------------------ 10 | 11 | .. autoclass:: fluent_pages.views.CurrentPageMixin 12 | :members: 13 | 14 | The ``CurrentPageTemplateMixin`` class 15 | -------------------------------------- 16 | 17 | .. autoclass:: fluent_pages.views.CurrentPageTemplateMixin 18 | :members: 19 | 20 | The ``RobotsTxtView`` class 21 | --------------------------- 22 | 23 | .. autoclass:: fluent_pages.views.RobotsTxtView 24 | :members: 25 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGES.rst 2 | -------------------------------------------------------------------------------- /docs/dependencies.rst: -------------------------------------------------------------------------------- 1 | .. _dependencies: 2 | 3 | Package dependencies 4 | ==================== 5 | 6 | This is a quick overview of all used Django packages: 7 | 8 | .. graphviz:: 9 | 10 | digraph G { 11 | fontname = "Bitstream Vera Sans" 12 | fontsize = 8 13 | 14 | node [ 15 | fontname = "Bitstream Vera Sans" 16 | fontsize = 8 17 | shape = "record" 18 | style = "filled" 19 | fillcolor = "gray80" 20 | ] 21 | 22 | edge [ 23 | fontname = "Bitstream Vera Sans" 24 | fontsize = 8 25 | ] 26 | 27 | subgraph clusterFluentPages { 28 | label = "This package" 29 | 30 | fluent_pages [ 31 | label = "django-fluent-pages" 32 | ] 33 | 34 | subgraph clusterPagetypes { 35 | label = "fluent_pages.pagetypes" 36 | 37 | fluentpage [ 38 | label = "fluentpage" 39 | ] 40 | 41 | flatpage [ 42 | label = "flatpage" 43 | ] 44 | 45 | redirectnode [ 46 | label = "redirectnode" 47 | ] 48 | 49 | textfile [ 50 | label = "textfile" 51 | ] 52 | } 53 | } 54 | 55 | any_urlfield [ 56 | label = "django-any-urlfield" 57 | ] 58 | 59 | django_wysiwyg [ 60 | label = "django-wysiwyg" 61 | ] 62 | 63 | fluent_contents [ 64 | label = "django-fluent-contents" 65 | ] 66 | 67 | fluent_utils [ 68 | label = "django-fluent-utils (internal)" 69 | ] 70 | 71 | django_polymorphic [ 72 | label = "django-polymorphic" 73 | ] 74 | 75 | django_mptt [ 76 | label = "django-mptt" 77 | ] 78 | 79 | django_parler [ 80 | label = "django-parler" 81 | ] 82 | 83 | django_polymorphic_tree [ 84 | label = "django-polymorphic-tree" 85 | ] 86 | 87 | fluentpage -> fluent_contents 88 | flatpage -> django_wysiwyg 89 | redirectnode -> any_urlfield [style=dashed] 90 | fluent_utils -> any_urlfield [style=dashed] 91 | 92 | fluent_pages -> django_polymorphic_tree [lhead=clusterFluentPages] 93 | fluent_pages -> django_parler 94 | fluent_pages -> fluent_utils 95 | django_polymorphic_tree -> django_polymorphic 96 | django_polymorphic_tree -> django_mptt 97 | } 98 | 99 | The used packages are: 100 | 101 | .. glossary:: 102 | 103 | django-any-urlfield_: 104 | 105 | An URL field which can also point to an internal Django model. 106 | 107 | django-fluent-contents_: 108 | 109 | The widget engine for flexible block positions. 110 | 111 | django-fluent-utils_: 112 | 113 | Internal utilities for code sharing between django-fluent modules. 114 | 115 | django-mptt_: 116 | 117 | The structure to store tree data in the database. 118 | 119 | Note that *django-fluent-pages* doesn't 120 | use a 100% pure MPTT tree, as it also stores a ``parent_id`` and ``_cached_url`` field in the database. 121 | These fields are added for performance reasons, to quickly resolve parents, children and pages by URL. 122 | 123 | django-parler_: 124 | 125 | Translation support for all models. 126 | 127 | django-polymorphic_: 128 | 129 | Polymorphic inheritance for Django models, it lets queries return the derived models by default. 130 | 131 | django-polymorphic-tree_ 132 | 133 | The tree logic, where each node can be a different model type. 134 | 135 | django-wysiwyg_: 136 | 137 | A flexible WYSIWYG field, which supports various editors. 138 | 139 | 140 | .. _django-any-urlfield: https://github.com/edoburu/django-any-urlfield 141 | .. _django-fluent-contents: https://github.com/django-fluent/django-fluent-contents 142 | .. _django-fluent-utils: https://github.com/django-fluent/django-fluent-utils 143 | .. _django-mptt: https://github.com/django-mptt/django-mptt 144 | .. _django-parler: https://github.com/django-parler/django-parler 145 | .. _django-polymorphic: https://github.com/django-polymorphic/django-polymorphic 146 | .. _django-polymorphic-tree: https://github.com/django-polymorphic/django-polymorphic-tree 147 | .. _django-wysiwyg: https://github.com/pydanny/django-wysiwyg 148 | -------------------------------------------------------------------------------- /docs/images/newpagetypes/shoppage-add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-fluent/django-fluent-pages/621d756542f23d1e2f7679bffe6fcb81b82df005/docs/images/newpagetypes/shoppage-add.png -------------------------------------------------------------------------------- /docs/images/newpagetypes/shoppage-admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-fluent/django-fluent-pages/621d756542f23d1e2f7679bffe6fcb81b82df005/docs/images/newpagetypes/shoppage-admin.png -------------------------------------------------------------------------------- /docs/images/pagetypes/flatpage-admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-fluent/django-fluent-pages/621d756542f23d1e2f7679bffe6fcb81b82df005/docs/images/pagetypes/flatpage-admin.png -------------------------------------------------------------------------------- /docs/images/pagetypes/fluentpage-admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-fluent/django-fluent-pages/621d756542f23d1e2f7679bffe6fcb81b82df005/docs/images/pagetypes/fluentpage-admin.png -------------------------------------------------------------------------------- /docs/images/pagetypes/redirectnode-admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-fluent/django-fluent-pages/621d756542f23d1e2f7679bffe6fcb81b82df005/docs/images/pagetypes/redirectnode-admin.png -------------------------------------------------------------------------------- /docs/images/pagetypes/textfile-admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-fluent/django-fluent-pages/621d756542f23d1e2f7679bffe6fcb81b82df005/docs/images/pagetypes/textfile-admin.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to django-fluent-pages's documentation! 2 | =============================================== 3 | 4 | This module provides a page tree, where each node type can be a different model. 5 | This allows you to structure your site CMS tree as you see fit. For example: 6 | 7 | * Build a tree of flat pages, with a WYSIWYG editor. 8 | * Build a tree with widget-based pages, by integrating django-fluent-contents_. 9 | * Build a tree structure of RST pages, by defining a ``RstPage`` type. 10 | * Build a tree of a *homepage*, *subsection*, and *article* node, each with custom fields like professional CMSes have. 11 | 12 | Each node type can have it's own custom fields, attributes, URL patterns and rendering. 13 | 14 | In case you're building a custom CMS, this module might just be suited for you, 15 | since it provides the tree for you, without bothering with anything else. 16 | The actual page contents is defined via page type plugins. 17 | To get up and running quickly, consult the :ref:`quick-start guide `. 18 | The chapters below describe the configuration of each specific plugin in more detail. 19 | 20 | Preview 21 | ------- 22 | 23 | .. image:: /images/pagetypes/fluentpage-admin.* 24 | :width: 756px 25 | :height: 726px 26 | :alt: django-fluent-pages admin preview 27 | 28 | 29 | Getting started 30 | --------------- 31 | 32 | .. toctree:: 33 | :maxdepth: 2 34 | 35 | quickstart 36 | configuration 37 | templatetags 38 | sitemaps 39 | multilingual 40 | management 41 | 42 | 43 | Using the page type plugins 44 | --------------------------- 45 | 46 | .. toctree:: 47 | :maxdepth: 2 48 | 49 | pagetypes/index 50 | newpagetypes/index 51 | 52 | 53 | API documentation 54 | ----------------- 55 | 56 | .. toctree:: 57 | :maxdepth: 2 58 | 59 | lowlevel 60 | api/index 61 | dependencies 62 | changelog 63 | 64 | 65 | Indices and tables 66 | ================== 67 | 68 | * :ref:`genindex` 69 | * :ref:`modindex` 70 | * :ref:`search` 71 | 72 | .. _django-fluent-contents: https://github.com/django-fluent/django-fluent-contents 73 | -------------------------------------------------------------------------------- /docs/management.rst: -------------------------------------------------------------------------------- 1 | Management Commands 2 | =================== 3 | 4 | The following management commands are provided for administrative utilities: 5 | 6 | 7 | make_language_redirects 8 | ----------------------- 9 | 10 | When a language is unmaintained at the site, 11 | use this command to generate the URL redirects. 12 | The command outputs a script for the web server (currently only in Nginx format). 13 | 14 | Options: 15 | 16 | * :samp:`--from={language}`: the old language 17 | * :samp:`--to={language}`: the new language 18 | * :samp:`--format={nginx}`: the format 19 | * :samp:`--site={id}`: the site for which redirects are created. 20 | 21 | Example: 22 | 23 | .. code-block:: bash 24 | 25 | python manage.py make_language_redirects --from=it --to=en --format=nginx --site=1 26 | 27 | 28 | rebuild_page_tree 29 | ----------------- 30 | 31 | In the unlikely event that the page tree is broken, this utility repairs the tree. 32 | This happened in earlier releases (before 1.0) when entire trees were moved in multi-lingual sites. 33 | 34 | It regenerates the MPTT fields and URLs. 35 | 36 | Options: 37 | 38 | * ``-p`` / ``--dry-run``: tell what would happen, but don't make any changes. 39 | * ``-m`` / ``--mptt-only``: only regenerate the MPTT fields, not the URLs of the tree. 40 | 41 | Example: 42 | 43 | .. code-block:: bash 44 | 45 | python manage.py rebuild_page_tree 46 | 47 | 48 | remove_stale_pages 49 | ------------------ 50 | 51 | .. versionadded:: 1.1.2 52 | 53 | In the unlikely event that a page type was removed, but it's page nodes still exist, 54 | this command can be used to repair the tree. It removes the old pages that point to 55 | content types that no longer exist. 56 | 57 | Options: 58 | 59 | * ``-p`` / ``--dry-run``: tell what would happen, but don't make any changes. 60 | 61 | Example: 62 | 63 | .. code-block:: bash 64 | 65 | python manage.py remove_stale_pages --dry-run 66 | -------------------------------------------------------------------------------- /docs/newpagetypes/admin.rst: -------------------------------------------------------------------------------- 1 | .. _newplugins-admin: 2 | 3 | Customizing the admin interface 4 | =============================== 5 | 6 | The admin rendering of a page type is fully customizable. 7 | 8 | .. versionchanged:: 1.2 9 | 10 | It's no longer necessary to define the ``model_admin`` attribute. 11 | Registering the custom admin class instead using ``admin.site.register()`` 12 | or the ``@admin.register()`` decorator. 13 | 14 | .. code-block:: python 15 | 16 | @page_type_pool.register 17 | class ProductCategoryPagePlugin(PageTypePlugin): 18 | """" 19 | A new page type plugin that binds the rendering and model together. 20 | """ 21 | model = ProductCategoryPage 22 | render_template = "products/productcategorypage.html" 23 | 24 | model_admin = ProductCategoryPageAdmin # only required for fluent-pages 1.1 and below. 25 | 26 | 27 | The admin class needs to inherit from one of the following classes: 28 | 29 | * :class:`fluent_pages.admin.PageAdmin` 30 | * :class:`fluent_pages.admin.HtmlPageAdmin` - in case the model extends from :class:`~fluent_pages.models.HtmlPage` 31 | * :class:`fluent_pages.pagetypes.fluentpage.admin.FluentPageAdmin` - in case the model extends from :class:`~fluent_pages.pagetypes.fluentpage.models.FluentPageBase` 32 | 33 | The admin can be used to customize the "add" and "edit" fields for example: 34 | 35 | .. code-block:: python 36 | 37 | from django.contrib import admin 38 | from fluent_pages.admin import PageAdmin 39 | from .models import ProductCategoryPage 40 | 41 | 42 | @admin.register(ProductCategoryPage) 43 | class ProductCategoryPageAdmin(PageAdmin): 44 | raw_id_fields = PageAdmin.raw_id_fields + ('product_category',) 45 | 46 | 47 | Despire being registered in the admin, the model won't show up in the index page. 48 | The "list" page is never used, as this is rendered by the main :class:`~fluent_pages.admin.PageAdmin` class. 49 | Only the "add" and "edit" page are exposed by the :class:`~fluent_pages.admin.PageAdmin` class too. 50 | 51 | 52 | Customizing fieldsets 53 | --------------------- 54 | 55 | To deal with model inheritance, the fieldsets are not set in stone in the :attr:`~django.contrib.admin.ModelAdmin.fieldsets` attribute. 56 | Instead, the fieldsets are created dynamically using the the :attr:`~fluent_pages.admin.PageAdmin.base_fieldsets` value as starting point. 57 | Any unknown fields (e.g. added by derived models) will be added to a separate "Contents" fieldset. 58 | 59 | The default layout of the :class:`~fluent_pages.admin.PageAdmin` class is: 60 | 61 | .. code-block:: python 62 | 63 | base_fieldsets = ( 64 | PageAdmin.FIELDSET_GENERAL, 65 | PageAdmin.FIELDSET_MENU, 66 | PageAdmin.FIELDSET_PUBLICATION, 67 | ) 68 | 69 | The default layout of the :class:`~fluent_pages.admin.HtmlPageAdmin` is: 70 | 71 | .. code-block:: python 72 | 73 | base_fieldsets = ( 74 | HtmlPageAdmin.FIELDSET_GENERAL, 75 | HtmlPageAdmin.FIELDSET_SEO, 76 | HtmlPageAdmin.FIELDSET_MENU, 77 | HtmlPageAdmin.FIELDSET_PUBLICATION, 78 | ) 79 | 80 | The title of the custom "Contents" fieldset is configurable with the :attr:`~fluent_pages.admin.PageAdmin.extra_fieldset_title` attribute. 81 | 82 | 83 | Customizing the form 84 | -------------------- 85 | 86 | Similar to the :attr:`~fluent_pages.admin.PageAdmin.base_fieldsets` attribute, 87 | there is a :attr:`~fluent_pages.admin.PageAdmin.base_form` attribute to use for the form. 88 | 89 | Inherit from the :class:`~fluent_pages.admin.PageAdminForm` class to create a custom form, 90 | so all base functionality works. 91 | -------------------------------------------------------------------------------- /docs/newpagetypes/index.rst: -------------------------------------------------------------------------------- 1 | .. _newpagetypes: 2 | 3 | Creating new page types 4 | ======================= 5 | 6 | This module is specifically designed to easily add custom page types. 7 | 8 | Typically, a project consists of some standard modules, and perhaps one or two custom types. 9 | Creating these is easy, as shown in the following sections: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | models 15 | rendering 16 | admin 17 | urls 18 | fluent_contents 19 | -------------------------------------------------------------------------------- /docs/newpagetypes/models.rst: -------------------------------------------------------------------------------- 1 | .. _newplugins-example: 2 | 3 | Example plugin code 4 | =================== 5 | 6 | A plugin is a standard Django/Python package. 7 | As quick example, let's create a webshop page. 8 | 9 | The plugin can be created in your Django project, in a separate app 10 | which can be named something like ``pagetypes.shoppage`` or ``mysite.pagetypes``. 11 | 12 | Example code 13 | ------------ 14 | 15 | For the ``pagetypes.shoppage`` package, the following files are needed: 16 | 17 | * ``__init__.py``, naturally. 18 | * ``models.py`` for the database model. 19 | * ``page_type_plugins.py`` for the plugin definition. 20 | 21 | models.py 22 | ~~~~~~~~~ 23 | 24 | The models in :file:`models.py` needs to inherit from the :class:`~fluent_pages.models.Page` class, 25 | the rest is just standard Django model code. 26 | 27 | .. code-block:: python 28 | 29 | from django.db import models 30 | from fluent_pages.models import Page 31 | from myshop.models import ProductCategory 32 | 33 | 34 | class ProductCategoryPage(Page): 35 | product_category = models.ForeignKey(ProductCategory) 36 | 37 | class Meta: 38 | verbose_name = 'Product category page' 39 | verbose_name_plural = 'Product category pages' 40 | 41 | 42 | This :class:`~fluent_pages.models.Page` class provides the basic fields to integrate the model in the tree. 43 | 44 | 45 | page_type_plugins.py 46 | ~~~~~~~~~~~~~~~~~~~~ 47 | 48 | The :file:`page_type_plugins.py` file can contain multiple plugins, each should inherit from the :class:`~fluent_pages.extensions.PageTypePlugin` class. 49 | 50 | .. code-block:: python 51 | 52 | from django.conf.urls import patterns, url 53 | from fluent_pages.extensions import PageTypePlugin, page_type_pool 54 | from .models import ProductCategoryPage 55 | 56 | 57 | @page_type_pool.register 58 | class ProductCategoryPagePlugin(PageTypePlugin): 59 | """" 60 | A new page type plugin that binds the rendering and model together. 61 | """ 62 | model = ProductCategoryPage 63 | render_template = "products/productcategorypage.html" 64 | 65 | # Custom URLs 66 | urls = patterns('myshop.views', 67 | url('^(?P[^/]+)/$', 'product_details'), 68 | ) 69 | 70 | 71 | The plugin class binds all parts together; the model, metadata, and rendering code. 72 | Either the :func:`~fluent_pages.extensions.PageTypePlugin.get_response` function can be overwritten, 73 | or a :attr:`~fluent_contents.extensions.ContentPlugin.render_template` can be defined. 74 | 75 | The other fields, such as the :attr:`~fluent_pages.extensions.PageTypePlugin.urls` are optional. 76 | 77 | 78 | productcategorypage.html 79 | ~~~~~~~~~~~~~~~~~~~~~~~~ 80 | 81 | The default :func:`~fluent_pages.extensions.PageTypePlugin.get_response` code renders the page with a template. 82 | 83 | This can be used to generate the HTML: 84 | 85 | .. code-block:: html+django 86 | 87 | {% extends "pages/base.html" %} 88 | 89 | {% block headtitle %}{{ page.title }}{% endblock %} 90 | 91 | {% block main %} 92 |

93 | Contents of the category: {{ page.product_category }} ({{ page.product_category.products.count }} products). 94 |

95 | 96 |
97 | .... 98 |
99 | {% endblock %} 100 | 101 | Note how the ``page`` variable is available, and the extra ``product_category`` field can be accessed directly. 102 | 103 | 104 | Wrapping up 105 | ~~~~~~~~~~~ 106 | 107 | The plugin is now ready to use. 108 | Don't forget to add the ``pagetypes.shoppage`` package to the ``INSTALLED_APPS``, and create the tables:: 109 | 110 | ./manage.py syncdb 111 | 112 | Now, the plugin will be visible in the "Add page" dialog: 113 | 114 | .. image:: /images/newpagetypes/shoppage-add.png 115 | :width: 771px 116 | :height: 172px 117 | :scale: 95 118 | :alt: New page type in the "Add page" dialog 119 | 120 | After adding it, the admin interface will be visible: 121 | 122 | .. image:: /images/newpagetypes/shoppage-admin.png 123 | :width: 770px 124 | :height: 380px 125 | :scale: 95 126 | :alt: Webshop page type admin interface 127 | 128 | The appearance on the website depends on the site's CSS theme, of course. 129 | 130 | This example showed how a new plugin can be created within 5-15 minutes! 131 | To continue, see :doc:`rendering` to implement custom rendering. 132 | -------------------------------------------------------------------------------- /docs/newpagetypes/rendering.rst: -------------------------------------------------------------------------------- 1 | .. newpagetypes-rendering: 2 | 3 | Customizing the frontend rendering 4 | ================================== 5 | 6 | As displayed in the :doc:`models` page, a page type is made of two classes: 7 | 8 | * A model class in :file:`models.py`. 9 | * A plugin class in :file:`page_type_plugins.py`. 10 | 11 | The plugin class renders the model instance using: 12 | 13 | * A custom :func:`~fluent_pages.extensions.PageTypePlugin.get_response` method. 14 | 15 | * The :attr:`~fluent_pages.extensions.PageTypePlugin.render_template` attribute, 16 | :func:`~fluent_pages.extensions.PageTypePlugin.get_render_template` method 17 | and optionally :func:`~fluent_pages.extensions.PageTypePlugin.get_context` method. 18 | 19 | Simply stated, a plugin provides the "view" of the "page". 20 | 21 | 22 | Simple rendering 23 | ---------------- 24 | 25 | To quickly create plugins with little to no effort, only the :attr:`~fluent_pages.extensions.PageTypePlugin.render_template` needs to be specified. 26 | The template code receives the model object via the ``instance`` variable. 27 | 28 | To switch the template depending on the model, the :func:`~fluent_pages.extensions.PageTypePlugin.get_render_template` method 29 | can be overwritten instead. For example: 30 | 31 | .. code-block:: python 32 | 33 | @page_type.register 34 | class MyPageType(PageTypePlugin): 35 | # ... 36 | 37 | def get_render_template(self, request, page, **kwargs): 38 | return page.template_name or self.render_template 39 | 40 | 41 | To add more context data, overwrite the :class:`~fluent_pages.extensions.PageTypePlugin.get_context` method. 42 | 43 | 44 | Custom rendering 45 | ---------------- 46 | 47 | Instead of only providing extra context data, 48 | the whole :func:`~fluent_pages.extensions.PageTypePlugin.get_response` method can be overwritten as well. 49 | 50 | The :ref:`textfile ` and :ref:`redirectnode ` page types use this for example: 51 | 52 | .. code-block:: python 53 | 54 | def get_response(self, request, redirectnode, **kwargs): 55 | response = HttpResponseRedirect(redirectnode.new_url) 56 | response.status_code = redirectnode.redirect_type 57 | return response 58 | 59 | The standard :func:`~fluent_pages.extensions.PageTypePlugin.get_response` method basically does the following: 60 | 61 | .. code-block:: python 62 | 63 | def get_response(self, request, page, **kwargs): 64 | render_template = self.get_render_template(request, page, **kwargs) 65 | context = self.get_context(request, page, **kwargs) 66 | return self.response_class( 67 | request = request, 68 | template = render_template, 69 | context = context, 70 | ) 71 | 72 | * It takes the template from :func:`~fluent_pages.extensions.PageTypePlugin.get_render_template`. 73 | * It uses the context provided by :func:`~fluent_pages.extensions.PageTypePlugin.get_context`. 74 | * It uses :func:`~fluent_pages.extensions.PageTypePlugin.response_class` class to output the response. 75 | 76 | .. note:: 77 | 78 | The :class:`PageTypePlugin` class is instantiated once, just like the :class:`~django.contrib.admin.ModelAdmin` class. 79 | Unlike the Django class based views, it's not possible to store state at the local instance. 80 | -------------------------------------------------------------------------------- /docs/newpagetypes/urls.rst: -------------------------------------------------------------------------------- 1 | .. newpagetypes-urls: 2 | 3 | Adding custom URLs 4 | ================== 5 | 6 | Page types can provide custom URL patterns. 7 | These URL patterns are relative to the place where the page is added to the page tree. 8 | 9 | This feature is useful for example to: 10 | 11 | * Have a "Shop" page type where all products are sub pages. 12 | * Have a "Blog" page type where all articles are displayed below. 13 | 14 | To use this feature, provide a URLconf or an inline :func:`~django.conf.urls.patterns` list in the page type plugin. 15 | 16 | 17 | Basic example 18 | ------------- 19 | 20 | To have a plugin with custom views, add the :attr:`~fluent_pages.extensions.PageTypePlugin.urls` attribute: 21 | 22 | .. code-block:: python 23 | 24 | @page_type_pool.register 25 | class ProductCategoryPagePlugin(PageTypePlugin): 26 | # ... 27 | 28 | urls = patterns('myshop.views', 29 | url('^(?P[^/]+)/$', 'product_details'), 30 | ) 31 | 32 | 33 | The view is just a plain Django view: 34 | 35 | .. code-block:: python 36 | 37 | from django.http import HttpResponse 38 | from django.shortcuts import get_object_or_404, render 39 | from myshop.models import Product 40 | 41 | def product_details(request, slug): 42 | product = get_object_or_404(Product, slug=slug) 43 | return render(request, 'products/product_details.html', { 44 | 'product': product 45 | }) 46 | 47 | Other custom views can be created in the same way. 48 | 49 | 50 | Resolving URLs 51 | -------------- 52 | 53 | The URLs can't be resolved using the standard :func:`~django.urls.reverse` function unfortunately. 54 | The main reason is that it caches results internally for the lifetime of the WSGI container, 55 | meanwhile pages may be rearranged by the admin. 56 | 57 | Hence, a :func:`~fluent_pages.urlresolvers.app_reverse` function is available. 58 | It can be used to resolve the product page: 59 | 60 | .. code-block:: python 61 | 62 | from fluent_pages.urlresolvers import app_reverse 63 | 64 | app_reverse('product_details', kwargs={'slug': 'myproduct'}) 65 | 66 | In templates, there is an ``appurl`` tag which accomplishes the same effect: 67 | 68 | .. code-block:: html+django 69 | 70 | {% load appurl_tags %} 71 | 72 | My Product 73 | 74 | .. seealso:: 75 | 76 | The `example application `_ in the source demonstrates this feature. 77 | 78 | 79 | Compatibility with regular URLconf 80 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 81 | 82 | An application can provide a standard :file:`urls.py` for regular Django support, 83 | and still support page type URLs too. For this special case, 84 | the :func:`~fluent_pages.urlresolvers.mixed_reverse` function is available. 85 | It attemps to resolve the view in the standard URLconf first, 86 | and falls back to :func:`~fluent_pages.urlresolvers.app_reverse` if the view is not found there. 87 | 88 | A ``mixedurl`` template tag has to be included in the application itself. Use the following code as example: 89 | 90 | .. code-block:: python 91 | 92 | @register.tag 93 | def mixedurl(parser, token): 94 | if 'fluent_pages' in settings.INSTALLED_APPS: 95 | from fluent_pages.templatetags.appurl_tags import appurl 96 | return appurl(parser, token) 97 | else: 98 | from django.template.defaulttags import url 99 | return url(parser, token) 100 | 101 | 102 | .. seealso:: 103 | 104 | The django-fluent-blogs_ application uses this feature to optionally integrate the blog articles to the page tree. 105 | 106 | 107 | .. _django-fluent-blogs: https://github.com/django-fluent/django-fluent-blogs 108 | .. _example: https://github.com/django-fluent/django-fluent-pages/tree/master/example 109 | -------------------------------------------------------------------------------- /docs/pagetypes/fluentpage.rst: -------------------------------------------------------------------------------- 1 | .. _fluentpage: 2 | 3 | The fluentpage page type 4 | ======================== 5 | 6 | The *fluentpage* provides a page type where parts of the page can be filled with flexible content blocks. 7 | 8 | .. image:: /images/pagetypes/fluentpage-admin.* 9 | :width: 756px 10 | :height: 726px 11 | 12 | This feature is provided by django-fluent-contents_. 13 | 14 | The combination of *django-fluent-pages* and *django-fluent-contents* provides the most flexible page layout. 15 | It's possible to use a mix of standard plugins (e.g. *text*, *code*, *forms*) and 16 | customly defined plugins to facilitate complex website designs. 17 | See the documentation of django-fluent-contents_ for more details. 18 | 19 | 20 | Installation 21 | ------------ 22 | 23 | Install the dependencies via *pip*:: 24 | 25 | pip install django-fluent-pages[fluentpage] 26 | pip install django-fluent-contents[text] 27 | 28 | This installs the django-fluent-contents_ package. 29 | 30 | Add the following settings to ``settings.py``: 31 | 32 | .. code-block:: python 33 | 34 | INSTALLED_APPS += ( 35 | 'fluent_pages.pagetypes.fluentpage', 36 | 'fluent_contents', 37 | 38 | # The desired plugins for django-fluent-contents, e.g: 39 | 'fluent_contents.plugins.text', 40 | 'django_wysiwyg', 41 | ) 42 | 43 | 44 | Template design 45 | --------------- 46 | 47 | To render the page, include the tags that django-fluent-contents_ uses to define placeholders. 48 | For example: 49 | 50 | .. code-block:: html+django 51 | 52 | {% extends "mysite/base.html" %} 53 | {% load placeholder_tags %} 54 | 55 | {% block main %} 56 |
57 |
58 | {% block pagetitle %}

{{ page.title }}

{% endblock %} 59 | {% page_placeholder "main" role='m' %} 60 |
61 | 62 | 65 |
66 | {% endblock %} 67 | 68 | These placeholders will be detected and displayed in the admin pages. 69 | 70 | Place the template in the template folder that :ref:`FLUENT_PAGES_TEMPLATE_DIR` points to. 71 | By default, that is the first path in ``TEMPLATE_DIRS``. 72 | 73 | 74 | Configuration 75 | ------------- 76 | 77 | The page type itself doesn't provide any configuration options, 78 | everything can be fully configured by configuring django-fluent-contents_. 79 | See the documentation of each of these :ref:`bundled content plugins ` to use them: 80 | 81 | * :ref:`fluentcontents:code` 82 | * :ref:`fluentcontents:commentsarea` 83 | * :ref:`fluentcontents:disquscommentsarea` 84 | * :ref:`fluentcontents:formdesignerlink` 85 | * :ref:`fluentcontents:gist` 86 | * :ref:`fluentcontents:googledocsviewer` 87 | * :ref:`fluentcontents:iframe` 88 | * :ref:`fluentcontents:markup` 89 | * :ref:`fluentcontents:oembeditem` 90 | * :ref:`fluentcontents:rawhtml` 91 | * :ref:`fluentcontents:sharedcontent` 92 | * :ref:`fluentcontents:text` 93 | * :ref:`fluentcontents:twitterfeed` 94 | 95 | 96 | Creating new plugins 97 | ~~~~~~~~~~~~~~~~~~~~ 98 | 99 | A website with custom design elements can be easily editable by creating custom plugins. 100 | 101 | Creating new plugins is not complicated at all, and simple plugins can can easily be created within 15 minutes. 102 | 103 | The documentation of django-fluent-contents_ explains :ref:`how to create new plugins ` in depth. 104 | 105 | 106 | Advanced features 107 | ----------------- 108 | 109 | This module also provides the :class:`~fluent_pages.pagetypes.fluentpage.models.FluentPageBase` 110 | and :class:`~fluent_pages.pagetypes.fluentpage.admin.FluentPageAdmin` classes, 111 | which can be used as base classes for :ref:`custom page types ` 112 | that also use the same layout mechanisms. 113 | 114 | 115 | .. _django-fluent-contents: https://django-fluent-contents.readthedocs.io/en/latest/ 116 | -------------------------------------------------------------------------------- /docs/pagetypes/index.rst: -------------------------------------------------------------------------------- 1 | .. _pagetypes: 2 | 3 | Bundled Page Type Plugins 4 | ========================= 5 | 6 | This module ships has a set of plugins bundled by default, as they are useful for a broad range of web sites. 7 | The plugin code also serves as an example and inspiration to create your own modules, 8 | so feel free browse the source code of them. 9 | 10 | The available plugins are: 11 | 12 | .. toctree:: 13 | :maxdepth: 1 14 | 15 | flatpage 16 | fluentpage 17 | redirectnode 18 | textfile 19 | others 20 | -------------------------------------------------------------------------------- /docs/pagetypes/others.rst: -------------------------------------------------------------------------------- 1 | .. _other-known-pagetypes: 2 | 3 | Other known page types 4 | ====================== 5 | 6 | Blog page 7 | --------- 8 | 9 | The django-fluent-blogs_ module provides a "Blog page" type, which can be used to include a "Blog" in the page tree. 10 | 11 | To integrate it with this module, configure it using: 12 | 13 | .. code-block:: python 14 | 15 | INSTALLED_APPS += ( 16 | 'fluent_blogs', 17 | 'fluent_blogs.pagetypes.blogpage', 18 | ) 19 | 20 | See the documentation of django-fluent-blogs_ for details. 21 | 22 | FAQ page 23 | -------- 24 | 25 | The django-fluent-faq_ module provides a "FAQ page" type, 26 | which displays a list of FAQ questions and categories. 27 | 28 | To integrate it with this module, configure it using: 29 | 30 | .. code-block:: python 31 | 32 | INSTALLED_APPS += ( 33 | 'fluent_faq', 34 | 'fluent_faq.pagetypes.faqpage', 35 | ) 36 | 37 | See the documentation of django-fluent-faq_ for details. 38 | 39 | 40 | Open ideas 41 | ----------- 42 | 43 | Other page types can also be written, for example: 44 | 45 | * a "Portfolio" page type. 46 | * a "Split test" page type. 47 | * a "Flat page" with reStructuredText content. 48 | * a "Web shop" page type. 49 | * a "Subsite section" page type. 50 | 51 | See the next chapter, :ref:`newpagetypes` to create such plugins. 52 | 53 | 54 | .. _django-fluent-blogs: https://github.com/django-fluent/django-fluent-blogs 55 | .. _django-fluent-faq: https://github.com/django-fluent/django-fluent-faq 56 | -------------------------------------------------------------------------------- /docs/pagetypes/redirectnode.rst: -------------------------------------------------------------------------------- 1 | .. _redirectnode: 2 | 3 | The redirectnode page type 4 | ========================== 5 | 6 | The *redirectnode* allows adding a URL path that redirects the website visitor. 7 | 8 | .. image:: /images/pagetypes/redirectnode-admin.* 9 | :width: 771px 10 | :height: 490px 11 | 12 | 13 | Installation 14 | ------------ 15 | 16 | Install the dependencies via *pip*:: 17 | 18 | pip install django-fluent-pages[redirectnode] 19 | 20 | This installs the django-any-urlfield_ package. 21 | 22 | Add the following settings to ``settings.py``: 23 | 24 | .. code-block:: python 25 | 26 | INSTALLED_APPS += ( 27 | 'fluent_pages.pagetypes.redirectnode', 28 | 'any_urlfield', 29 | ) 30 | 31 | 32 | Configuration 33 | ------------- 34 | 35 | This page type works out of the box. 36 | 37 | By default, the admin can choose between an "External URL" and "Page". 38 | Other models can also be included too, as long as they have a ``get_absolute_url`` method. 39 | Register the respective models to django-any-urlfield_: 40 | 41 | .. code-block:: python 42 | 43 | from any_urlfield.models import AnyUrlField 44 | AnyUrlField.register_model(Article) 45 | 46 | See the :mod:`anyurlfield:any_urlfield.models` documentation for details. 47 | 48 | 49 | 50 | .. _django-any-urlfield: https://django-any-urlfield.readthedocs.io/en/latest/ 51 | -------------------------------------------------------------------------------- /docs/pagetypes/textfile.rst: -------------------------------------------------------------------------------- 1 | .. _textfile: 2 | 3 | The textfile page type 4 | ====================== 5 | 6 | The *textfile* allows adding a URL node that displays plain text. 7 | 8 | .. image:: /images/pagetypes/textfile-admin.* 9 | :width: 771px 10 | :height: 554px 11 | 12 | 13 | This page type serves as simple demo, and can also be used to add a 14 | custom ``robots.txt``, `humans.txt `_ file or ``README`` file to the page tree. 15 | 16 | 17 | .. note:: 18 | 19 | Currently, it's still required to use the "Override URL" field in the form 20 | to include a file extension, as the "Slug" field does not allow this. 21 | 22 | 23 | Installation 24 | ------------ 25 | 26 | Add the following settings to ``settings.py``: 27 | 28 | .. code-block:: python 29 | 30 | INSTALLED_APPS += ( 31 | 'fluent_pages.pagetypes.textfile', 32 | ) 33 | -------------------------------------------------------------------------------- /docs/sitemaps.rst: -------------------------------------------------------------------------------- 1 | .. _sitemaps: 2 | 3 | Sitemaps integration 4 | ==================== 5 | 6 | The pages can be included in the sitemap that :mod:`django.contrib.sitemaps` provides. 7 | This makes it easier for search engines to index all pages. 8 | 9 | Add the following in :file:`urls.py`: 10 | 11 | .. code-block:: python 12 | 13 | from fluent_pages.sitemaps import PageSitemap 14 | from fluent_pages.views import RobotsTxtView 15 | 16 | sitemaps = { 17 | 'pages': PageSitemap, 18 | } 19 | 20 | urlpatterns += patterns('', 21 | url(r'^sitemap.xml$', 'django.contrib.sitemaps.views.sitemap', {'sitemaps': sitemaps}), 22 | url(r'^robots.txt$', RobotsTxtView.as_view()), 23 | ) 24 | 25 | The :mod:`django.contrib.sitemaps` should be included in the ``INSTALLED_APPS`` off course: 26 | 27 | .. code-block:: python 28 | 29 | INSTALLED_APPS += ( 30 | 'django.contrib.sitemaps', 31 | ) 32 | 33 | The pages should now be visible in the ``sitemap.xml``. 34 | 35 | A sitemap is referenced in the ``robots.txt`` URL. 36 | When using the bundled :class:`~fluent_pages.views.RobotsTxtView` in the example above, this happens by default. 37 | 38 | The contents of the ``robots.txt`` URL can be overwritten by overriding the :file:`robots.txt` template. 39 | Note that the :file:`robots.txt` file should point to the sitemap with the full domain name included:: 40 | 41 | Sitemap: http://full-website-domain/sitemap.xml 42 | 43 | For more details about the ``robots.txt`` URL, see the documentation at 44 | http://www.robotstxt.org/ and https://support.google.com/webmasters/answer/6062608?hl=en&rd=1 45 | 46 | .. note:: 47 | 48 | When using Nginx, verify that ``robots.txt`` is also forwarded to your Django application. 49 | 50 | For example, when using ``location = /robots.txt { access_log off; log_not_found off; }``, 51 | the request will not be forwarded to Django because this replaces the standard ``location / { .. }`` block. 52 | -------------------------------------------------------------------------------- /example/README.rst: -------------------------------------------------------------------------------- 1 | Using the example 2 | ================= 3 | 4 | To run the example application, make sure you have the required packages installed. 5 | You can do this using: 6 | 7 | .. code-block:: shell 8 | 9 | mkvirtualenv fpdemo 10 | pip install -r requirements.txt 11 | 12 | (This assumes you already have *virtualenv* and *virtualenvwrapper* installed). 13 | 14 | Next, you can setup the Django instance using: 15 | 16 | .. code-block:: shell 17 | 18 | ./manage.py syncdb --migrate --noinput 19 | ./manage.py createsuperuser --username=admin --email=admin@example.com 20 | ./manage.py loaddata welcome shop_example 21 | 22 | And run it off course: 23 | 24 | .. code-block:: shell 25 | 26 | ./manage.py runserver 27 | 28 | Good luck! 29 | 30 | This module is designed to be generic. In case there is anything you didn't like about it, 31 | or think it's not flexible enough, please let us know. We'd love to improve it! 32 | 33 | If you have any other valuable contribution, suggestion or idea, please let us know as well 34 | at https://github.com/edoburu/django-fluent-pages/ because we will look at it. 35 | Pull requests are welcome too. :-) 36 | 37 | -------------------------------------------------------------------------------- /example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-fluent/django-fluent-pages/621d756542f23d1e2f7679bffe6fcb81b82df005/example/__init__.py -------------------------------------------------------------------------------- /example/fixtures/create.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -x 2 | 3 | cd `dirname $0` 4 | manage=../manage.py 5 | 6 | # Page structure 7 | $manage dumpdata --natural-foreign --indent=2 simpleshop.ProductCategory simpleshop.Product > _simpleshop.json 8 | $manage dumpdata --natural-foreign --indent=2 fluent_pages > _pages_base.json 9 | $manage dumpdata --natural-foreign --indent=2 fluentpage simpleshop.ProductCategoryPage > _pages_models.json 10 | 11 | # Page plugins 12 | $manage dumpdata --natural-foreign --indent=2 fluent_contents > _page_contents_base.json 13 | #$manage dumpdata --natural-foreign --indent=2 sharedcontent > _page_contents_shared.json 14 | $manage dumpdata --natural-foreign --indent=2 text oembeditem picture rawhtml > _page_contents_models.json 15 | -------------------------------------------------------------------------------- /example/fixtures/shop_example.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "simpleshop.productcategory", 5 | "fields": { 6 | "slug": "desktop-computers", 7 | "title": "Desktop computers" 8 | } 9 | }, 10 | { 11 | "pk": 2, 12 | "model": "simpleshop.productcategory", 13 | "fields": { 14 | "slug": "notebooks", 15 | "title": "Notebooks" 16 | } 17 | }, 18 | { 19 | "pk": 1, 20 | "model": "simpleshop.product", 21 | "fields": { 22 | "category": 2, 23 | "description": "Here is your example product, served via the page tree!", 24 | "price": "1199", 25 | "slug": "macbook-pro-13", 26 | "title": "MacBook Pro 13''" 27 | } 28 | }, 29 | { 30 | "pk": 2, 31 | "model": "simpleshop.product", 32 | "fields": { 33 | "category": 2, 34 | "description": "Another product example!", 35 | "price": "1799", 36 | "slug": "macbook-pro-15", 37 | "title": "MacBook Pro 15'" 38 | } 39 | }, 40 | { 41 | "pk": 3, 42 | "model": "simpleshop.product", 43 | "fields": { 44 | "category": 2, 45 | "description": "Another product example!", 46 | "price": "1199", 47 | "slug": "dell-xps-15-laptop", 48 | "title": "Dell XPS 15'' Laptop" 49 | } 50 | }, 51 | { 52 | "pk": 4, 53 | "model": "simpleshop.product", 54 | "fields": { 55 | "category": 2, 56 | "description": "Here is your example product, served via the page tree!", 57 | "price": "529", 58 | "slug": "dell-inspiron-17r", 59 | "title": "Dell Inspiron 17R" 60 | } 61 | }, 62 | { 63 | "pk": 5, 64 | "model": "simpleshop.product", 65 | "fields": { 66 | "category": 2, 67 | "description": "Testing testing via the pagetree. :-)", 68 | "price": "1605", 69 | "slug": "hp-elitebook-8560w", 70 | "title": "HP EliteBook 8560w" 71 | } 72 | }, 73 | { 74 | "pk": 2, 75 | "model": "fluent_pages.urlnode", 76 | "fields": { 77 | "status": "p", 78 | "rght": 2, 79 | "modification_date": "2014-04-03T11:57:55.157Z", 80 | "parent": null, 81 | "level": 0, 82 | "author": [ 83 | "admin" 84 | ], 85 | "publication_date": null, 86 | "creation_date": "2014-04-03T11:57:55.157Z", 87 | "lft": 1, 88 | "publication_end_date": null, 89 | "tree_id": 2, 90 | "parent_site": 1, 91 | "in_navigation": true, 92 | "polymorphic_ctype": [ 93 | "simpleshop", 94 | "productcategorypage" 95 | ] 96 | } 97 | }, 98 | { 99 | "pk": 2, 100 | "model": "fluent_pages.urlnode_translation", 101 | "fields": { 102 | "title": "Notebooks", 103 | "override_url": "", 104 | "master": 2, 105 | "language_code": "en", 106 | "_cached_url": "/notebooks/", 107 | "slug": "notebooks" 108 | } 109 | }, 110 | { 111 | "pk": 2, 112 | "model": "simpleshop.productcategorypage", 113 | "fields": { 114 | "product_category": 2 115 | } 116 | } 117 | ] 118 | -------------------------------------------------------------------------------- /example/fixtures/welcome.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "fluent_pages.urlnode", 5 | "fields": { 6 | "status": "p", 7 | "rght": 2, 8 | "modification_date": "2014-04-03T11:49:48.445Z", 9 | "parent": null, 10 | "level": 0, 11 | "author": [ 12 | "admin" 13 | ], 14 | "publication_date": null, 15 | "creation_date": "2014-04-03T11:49:36.656Z", 16 | "lft": 1, 17 | "publication_end_date": null, 18 | "tree_id": 1, 19 | "parent_site": 1, 20 | "in_navigation": true, 21 | "polymorphic_ctype": [ 22 | "fluentpage", 23 | "fluentpage" 24 | ] 25 | } 26 | }, 27 | { 28 | "pk": 1, 29 | "model": "fluent_pages.urlnode_translation", 30 | "fields": { 31 | "title": "Homepage", 32 | "override_url": "/", 33 | "master": 1, 34 | "language_code": "en", 35 | "_cached_url": "/", 36 | "slug": "homepage" 37 | } 38 | }, 39 | { 40 | "pk": 1, 41 | "model": "fluent_pages.pagelayout", 42 | "fields": { 43 | "template_path": "theme1/pages/standard.html", 44 | "key": "standard", 45 | "title": "standard" 46 | } 47 | }, 48 | { 49 | "pk": 2, 50 | "model": "fluent_pages.pagelayout", 51 | "fields": { 52 | "template_path": "theme1/pages/standard-twocols.html", 53 | "key": "standard-2-cols", 54 | "title": "Standard, 2 cols" 55 | } 56 | }, 57 | { 58 | "pk": 1, 59 | "model": "fluentpage.fluentpage", 60 | "fields": { 61 | "layout": 1 62 | } 63 | }, 64 | { 65 | "pk": 1, 66 | "model": "fluent_contents.placeholder", 67 | "fields": { 68 | "slot": "main1", 69 | "parent_type": [ 70 | "fluentpage", 71 | "fluentpage" 72 | ], 73 | "title": "Main content", 74 | "role": "m", 75 | "parent_id": 1 76 | } 77 | }, 78 | { 79 | "pk": 1, 80 | "model": "fluent_contents.contentitem", 81 | "fields": { 82 | "parent_id": 1, 83 | "sort_order": 0, 84 | "parent_type": [ 85 | "fluentpage", 86 | "fluentpage" 87 | ], 88 | "placeholder": 1, 89 | "polymorphic_ctype": [ 90 | "text", 91 | "textitem" 92 | ] 93 | } 94 | }, 95 | { 96 | "pk": 1, 97 | "model": "text.textitem", 98 | "fields": { 99 | "text": "Welcome to django-fluent-pages.
Open the admin page to get started!
More info: www.django-fluent.org" 100 | } 101 | } 102 | ] 103 | -------------------------------------------------------------------------------- /example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | # Allow starting the app without installing the module. 11 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) 12 | 13 | execute_from_command_line(sys.argv) 14 | -------------------------------------------------------------------------------- /example/requirements.txt: -------------------------------------------------------------------------------- 1 | # Django version 2 | Django == 2.2.* 3 | 4 | # Pillow version 5 | Pillow >= 6.1.0 6 | 7 | # Deps of fluent-pages 8 | django-fluent-utils >= 2.0.1 9 | django-mptt >= 0.10.0 10 | django-parler >= 1.9.2 11 | django-polymorphic-tree >= 1.5 12 | django-polymorphic >= 2.1.2 13 | django-slug-preview >= 1.0.4 14 | django-tag-parser >= 3.1 15 | 16 | # For the `fluent_pages.plugins.fluentpage` plugin: 17 | django-fluent-contents >= 2.0.6 18 | django-template-analyzer >= 1.6.1 19 | 20 | # For the `fluent_contents.plugins.*` apps: 21 | django-wysiwyg >= 0.8.0 22 | django-tinymce >= 2.8.0 23 | Markdown >= 3.1.1 24 | docutils >= 0.15.1 25 | textile >= 3.0.4 26 | Pygments >= 2.4.2 27 | -------------------------------------------------------------------------------- /example/settings.py: -------------------------------------------------------------------------------- 1 | # Add parent path, 2 | # Allow starting the app without installing the module. 3 | import sys 4 | from os.path import dirname, join, realpath 5 | 6 | import django 7 | 8 | sys.path.insert(0, dirname(dirname(realpath(__file__)))) 9 | 10 | DEBUG = True 11 | 12 | ADMINS = ( 13 | # ('Your Name', 'your_email@example.com'), 14 | ) 15 | 16 | MANAGERS = ADMINS 17 | 18 | DATABASES = { 19 | "default": { 20 | "ENGINE": "django.db.backends.sqlite3", 21 | "NAME": dirname(__file__) + "/demo.db", 22 | } 23 | } 24 | 25 | TIME_ZONE = "Europe/Amsterdam" 26 | LANGUAGE_CODE = "en" 27 | SITE_ID = 1 28 | 29 | USE_I18N = True 30 | USE_TZ = True 31 | 32 | MEDIA_ROOT = join(dirname(__file__), "media") 33 | MEDIA_URL = "/media/" 34 | STATIC_ROOT = join(dirname(__file__), "static") 35 | STATIC_URL = "/static/" 36 | 37 | STATICFILES_DIRS = () 38 | STATICFILES_FINDERS = ( 39 | "django.contrib.staticfiles.finders.FileSystemFinder", 40 | "django.contrib.staticfiles.finders.AppDirectoriesFinder", 41 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 42 | ) 43 | 44 | DEFAULT_AUTO_FIELD = "django.db.models.AutoField" 45 | 46 | # Make this unique, and don't share it with anybody. 47 | SECRET_KEY = "-#@bi6bue%#1j)6+4b&#i0g-*xro@%f@_#zwv=2-g_@n3n_kj5" 48 | 49 | TEMPLATES = [ 50 | { 51 | "BACKEND": "django.template.backends.django.DjangoTemplates", 52 | "DIRS": [join(dirname(__file__), "templates")], 53 | "OPTIONS": { 54 | "debug": DEBUG, 55 | "loaders": ( 56 | "django.template.loaders.filesystem.Loader", 57 | "django.template.loaders.app_directories.Loader", 58 | ), 59 | "context_processors": ( 60 | "django.template.context_processors.debug", 61 | "django.template.context_processors.i18n", 62 | "django.template.context_processors.media", 63 | "django.template.context_processors.request", 64 | "django.template.context_processors.static", 65 | "django.contrib.auth.context_processors.auth", 66 | "django.contrib.messages.context_processors.messages", 67 | ), 68 | }, 69 | } 70 | ] 71 | 72 | MIDDLEWARE = ( 73 | "django.middleware.common.CommonMiddleware", 74 | "django.contrib.sessions.middleware.SessionMiddleware", 75 | "django.middleware.csrf.CsrfViewMiddleware", 76 | "django.contrib.auth.middleware.AuthenticationMiddleware", 77 | "django.contrib.messages.middleware.MessageMiddleware", 78 | ) 79 | 80 | ROOT_URLCONF = "urls" 81 | 82 | FIXTURE_DIRS = (join(dirname(__file__), "fixtures"),) 83 | 84 | FLUENT_PAGES_TEMPLATE_DIR = join(dirname(__file__), "theme1", "templates") 85 | 86 | INSTALLED_APPS = ( 87 | "django.contrib.auth", 88 | "django.contrib.contenttypes", 89 | "django.contrib.sessions", 90 | "django.contrib.sites", 91 | "django.contrib.messages", 92 | "django.contrib.staticfiles", 93 | "django.contrib.admin", 94 | "django.contrib.admindocs", 95 | # The CMS 96 | "fluent_pages", 97 | "fluent_pages.pagetypes.fluentpage", 98 | "fluent_pages.pagetypes.redirectnode", 99 | "fluent_pages.pagetypes.textfile", 100 | "fluent_pages.pagetypes.flatpage", 101 | "theme1", 102 | # Extra apps 103 | "simpleshop", 104 | # Required dependencies 105 | "mptt", 106 | "polymorphic", 107 | "polymorphic_tree", 108 | "parler", 109 | "slug_preview", 110 | # Content for fluentpage, with plugins that have no extra configuration requirements 111 | "fluent_contents", 112 | "fluent_contents.plugins.code", 113 | "fluent_contents.plugins.gist", 114 | "fluent_contents.plugins.googledocsviewer", 115 | "fluent_contents.plugins.iframe", 116 | "fluent_contents.plugins.markup", 117 | "fluent_contents.plugins.rawhtml", 118 | "fluent_contents.plugins.text", 119 | "django_wysiwyg", 120 | "tinymce", 121 | ) 122 | 123 | TEST_RUNNER = "django.test.runner.DiscoverRunner" # silence system checks 124 | 125 | # DJANGO_WYSIWYG_FLAVOR = 'yui_advanced' 126 | DJANGO_WYSIWYG_FLAVOR = "tinymce_advanced" 127 | -------------------------------------------------------------------------------- /example/simpleshop/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-fluent/django-fluent-pages/621d756542f23d1e2f7679bffe6fcb81b82df005/example/simpleshop/__init__.py -------------------------------------------------------------------------------- /example/simpleshop/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from simpleshop.models import Product, ProductCategory 3 | 4 | 5 | @admin.register(ProductCategory) 6 | class ProductCategoryAdmin(admin.ModelAdmin): 7 | prepopulated_fields = {"slug": ("title",)} 8 | 9 | 10 | @admin.register(Product) 11 | class ProductAdmin(admin.ModelAdmin): 12 | """ 13 | A simple admin interface for the product administration. 14 | """ 15 | 16 | list_display = ("title", "price", "category") 17 | list_filter = ("category",) 18 | prepopulated_fields = {"slug": ("title",)} 19 | 20 | 21 | -------------------------------------------------------------------------------- /example/simpleshop/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.11.9 on 2018-01-30 19:18 2 | 3 | import django.db.models.deletion 4 | import django.db.models.manager 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [("fluent_pages", "0001_initial")] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name="Product", 17 | fields=[ 18 | ( 19 | "id", 20 | models.AutoField( 21 | auto_created=True, 22 | primary_key=True, 23 | serialize=False, 24 | verbose_name="ID", 25 | ), 26 | ), 27 | ("title", models.CharField(max_length=200, verbose_name="Title")), 28 | ("slug", models.SlugField(verbose_name="Slug")), 29 | ("description", models.TextField(verbose_name="Description")), 30 | ( 31 | "price", 32 | models.DecimalField(decimal_places=2, max_digits=10, verbose_name="Price"), 33 | ), 34 | ], 35 | options={ 36 | "verbose_name": "Product", 37 | "verbose_name_plural": "Products", 38 | "ordering": ("title",), 39 | }, 40 | ), 41 | migrations.CreateModel( 42 | name="ProductCategory", 43 | fields=[ 44 | ( 45 | "id", 46 | models.AutoField( 47 | auto_created=True, 48 | primary_key=True, 49 | serialize=False, 50 | verbose_name="ID", 51 | ), 52 | ), 53 | ("title", models.CharField(max_length=200, verbose_name="Title")), 54 | ("slug", models.SlugField(verbose_name="Slug")), 55 | ], 56 | options={ 57 | "verbose_name": "Productcategory", 58 | "verbose_name_plural": "Productcategories", 59 | "ordering": ("title",), 60 | }, 61 | ), 62 | migrations.CreateModel( 63 | name="ProductCategoryPage", 64 | fields=[ 65 | ( 66 | "urlnode_ptr", 67 | models.OneToOneField( 68 | auto_created=True, 69 | on_delete=django.db.models.deletion.CASCADE, 70 | parent_link=True, 71 | primary_key=True, 72 | serialize=False, 73 | to="fluent_pages.UrlNode", 74 | ), 75 | ), 76 | ( 77 | "product_category", 78 | models.ForeignKey( 79 | on_delete=django.db.models.deletion.PROTECT, 80 | to="simpleshop.ProductCategory", 81 | ), 82 | ), 83 | ], 84 | options={ 85 | "verbose_name": "Product category page", 86 | "verbose_name_plural": "Product category pages", 87 | "db_table": "pagetype_simpleshop_productcategorypage", 88 | }, 89 | bases=("fluent_pages.page",), 90 | ), 91 | migrations.AddField( 92 | model_name="product", 93 | name="category", 94 | field=models.ForeignKey( 95 | on_delete=django.db.models.deletion.PROTECT, 96 | related_name="products", 97 | to="simpleshop.ProductCategory", 98 | verbose_name="Category", 99 | ), 100 | ), 101 | ] 102 | -------------------------------------------------------------------------------- /example/simpleshop/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-fluent/django-fluent-pages/621d756542f23d1e2f7679bffe6fcb81b82df005/example/simpleshop/migrations/__init__.py -------------------------------------------------------------------------------- /example/simpleshop/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | Just a pretty normal model definition of a simple "shop". 3 | """ 4 | from django.db import models 5 | 6 | from fluent_pages.models import Page 7 | 8 | 9 | class ProductCategory(models.Model): 10 | title = models.CharField("Title", max_length=200) 11 | slug = models.SlugField("Slug") 12 | 13 | class Meta: 14 | verbose_name = "Productcategory" 15 | verbose_name_plural = "Productcategories" 16 | ordering = ("title",) 17 | 18 | def __str__(self): 19 | return self.title 20 | 21 | 22 | class Product(models.Model): 23 | category = models.ForeignKey( 24 | ProductCategory, on_delete=models.PROTECT, verbose_name="Category", related_name="products" 25 | ) 26 | title = models.CharField("Title", max_length=200) 27 | slug = models.SlugField("Slug") 28 | 29 | description = models.TextField("Description") 30 | price = models.DecimalField("Price", max_digits=10, decimal_places=2) 31 | # photo = models.ImageField('Photo', blank=True, upload_to='uploads/productphotos') 32 | 33 | class Meta: 34 | verbose_name = "Product" 35 | verbose_name_plural = "Products" 36 | ordering = ("title",) 37 | 38 | def __str__(self): 39 | return self.title 40 | 41 | 42 | class ProductCategoryPage(Page): 43 | """ 44 | The database model for the custom pagetype. 45 | """ 46 | 47 | product_category = models.ForeignKey(ProductCategory, on_delete=models.PROTECT) 48 | 49 | class Meta: 50 | verbose_name = "Product category page" 51 | verbose_name_plural = "Product category pages" 52 | -------------------------------------------------------------------------------- /example/simpleshop/page_type_plugins.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from simpleshop.models import ProductCategoryPage 3 | from simpleshop.views import product_details 4 | 5 | from fluent_pages.extensions import PageTypePlugin, page_type_pool 6 | 7 | 8 | @page_type_pool.register 9 | class ProductCategoryPagePlugin(PageTypePlugin): 10 | """ " 11 | A new pagetype plugin that binds the rendering and model together. 12 | """ 13 | 14 | model = ProductCategoryPage 15 | render_template = "products/productcategorypage.html" 16 | urls = [ 17 | path("/", product_details), 18 | ] 19 | -------------------------------------------------------------------------------- /example/simpleshop/templates/products/product_details.html: -------------------------------------------------------------------------------- 1 | {% extends "theme1/base.html" %} 2 | 3 | {% block main %} 4 |

{{ product.title }}

5 |
6 | {{ product.description|linebreaks }} 7 |
8 | 9 | {% if product.image %}

{{ product.title }}

{% endif %} 10 |

€ {{ product.price|floatformat:2 }}

11 | 12 | {% endblock %} -------------------------------------------------------------------------------- /example/simpleshop/templates/products/productcategorypage.html: -------------------------------------------------------------------------------- 1 | {% extends "theme1/base.html" %} 2 | 3 | {% block headtitle %}{{ page.title }}{% endblock %} 4 | 5 | {% block main %} 6 |

{{ page.title }}

7 |

8 | Contents of the category: {{ page.product_category }} ({{ page.product_category.products.count }} products). 9 |

10 | 11 |
12 | {% for product in page.product_category.products.all %} 13 |
14 |

{{ product.title }}

15 |
16 | {% if product.image %}

{{ product.title }}

{% endif %} 17 |

€ {{ product.price|floatformat:2 }}

18 |
19 |
20 | {% endfor %} 21 |
22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /example/simpleshop/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.shortcuts import get_object_or_404, render 3 | from simpleshop.models import Product 4 | 5 | 6 | def product_details(request, slug): 7 | product = get_object_or_404(Product, slug=slug) 8 | return render(request, "products/product_details.html", {"product": product}) 9 | -------------------------------------------------------------------------------- /example/theme1/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-fluent/django-fluent-pages/621d756542f23d1e2f7679bffe6fcb81b82df005/example/theme1/__init__.py -------------------------------------------------------------------------------- /example/theme1/templates/theme1/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% block headtitle %}{% endblock %} 7 | 8 | {% block extrahead %}{% endblock %} 9 | {% block scripts %}{% endblock %} 10 | 11 | 12 | 13 |
14 |
15 | 18 | 19 | 26 | 27 |
28 | {% block main %}{% endblock %} 29 |
30 | {% block after_main %}{% endblock %} 31 | 32 | {% include "theme1/parts/sidebar.html" %} 33 |
34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /example/theme1/templates/theme1/pages/standard-twocols.html: -------------------------------------------------------------------------------- 1 | {% extends "theme1/base.html" %}{% load placeholder_tags %} 2 | 3 | {% block headtitle %}{{ page.title }}{% endblock %} 4 | {% block bodyclass %}twocols{% endblock %} 5 | 6 | {% block main %} 7 |

{{ page.title }}

8 | 9 |
10 | {% page_placeholder "main1" title="Main content" role="main" %} 11 |
12 | 13 | 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /example/theme1/templates/theme1/pages/standard.html: -------------------------------------------------------------------------------- 1 | {% extends "theme1/base.html" %}{% load placeholder_tags %} 2 | 3 | {% block headtitle %}{{ page.title }}{% endblock %} 4 | 5 | {% block main %} 6 |

{{ page.title }}

7 | 8 |
9 | {% page_placeholder "main1" title="Main content" %} 10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /example/theme1/templates/theme1/parts/sidebar.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | Test 5 |
6 |
7 | 8 |
9 |

Contact:

10 |

11 | E-mail 12 | Twitter 13 | Linked-IN 14 |

15 |
16 |
17 | -------------------------------------------------------------------------------- /example/urls.py: -------------------------------------------------------------------------------- 1 | import tinymce.urls 2 | from django.contrib import admin 3 | from django.urls import include, path 4 | 5 | import fluent_pages.urls 6 | 7 | urlpatterns = [ 8 | path("admin/utils/tinymce/", include(tinymce.urls)), 9 | path("admin/", admin.site.urls), 10 | path("", include(fluent_pages.urls)), 11 | ] 12 | -------------------------------------------------------------------------------- /fluent_pages/__init__.py: -------------------------------------------------------------------------------- 1 | # following PEP 440 2 | __version__ = "3.0.2" 3 | default_app_config = "fluent_pages.apps.FluentPagesConfig" 4 | -------------------------------------------------------------------------------- /fluent_pages/admin/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | The admin site registration. 3 | 4 | This is separate from the admin classes, so those can be imported freely without invoking ``admin.site.register()``. 5 | The admin site registration is only possible in Django 1.7 once all models are loaded. 6 | """ 7 | from django.contrib import admin 8 | 9 | # Leaving all old imports here 10 | # However, when using FLUENT_PAGES_PARENT_ADMIN_MIXIN / FLUENT_PAGES_CHILD_ADMIN_MIXIN 11 | # you can easily get circular import errors. Instead, import the classes from the adminui package. 12 | from fluent_pages.adminui import ( 13 | DefaultPageChildAdmin, 14 | DefaultPageParentAdmin, 15 | HtmlPageAdmin, 16 | PageAdmin, 17 | PageAdminForm, 18 | PageLayoutAdmin, 19 | PageParentAdmin, 20 | ) 21 | from fluent_pages.models import Page, PageLayout 22 | 23 | # Register the models with the admin site 24 | admin.site.register(Page, admin_class=PageParentAdmin) 25 | admin.site.register(PageLayout, admin_class=PageLayoutAdmin) 26 | -------------------------------------------------------------------------------- /fluent_pages/admin/utils.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from fluent_pages.adminui.utils import get_current_edited_page # noqa 4 | from fluent_pages.adminui.utils import get_page_admin_url 5 | 6 | warnings.warn( 7 | "Please use `fluent_pages.adminui.utils` instead, the `fluent_pages.admin.utils` module is deprecated for Django 1.7 compatibility.", 8 | DeprecationWarning, 9 | ) 10 | -------------------------------------------------------------------------------- /fluent_pages/adminui/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | A set of base classes, to build custom admin pages, for your page types. 3 | 4 | These classes are separate from the :mod:`fluent_pages.admin` module on purpose. 5 | Custom page type plugins can inherit these classes to provide their enhanced admin interface. 6 | If this module could be called :mod:`fluent_pages.admin`, it would invoke the app registry 7 | and prevent any further model initialization. 8 | """ 9 | 10 | from .htmlpageadmin import HtmlPageAdmin 11 | from .overrides import PageChildAdmin, PageParentAdmin 12 | 13 | # Import trick: make the DefaultPage*Admin available first, 14 | # so the classes imported by .overrides can actually import those from this module already. 15 | from .pageadmin import DefaultPageChildAdmin, DefaultPageParentAdmin, PageAdminForm 16 | from .pagelayoutadmin import PageLayoutAdmin 17 | 18 | __all__ = ( 19 | "PageParentAdmin", 20 | "DefaultPageParentAdmin", 21 | "PageChildAdmin", 22 | "DefaultPageChildAdmin", 23 | "HtmlPageAdmin", 24 | "PageLayoutAdmin", 25 | "PageAdminForm", 26 | ) 27 | 28 | PageAdmin = PageChildAdmin # noqa, older name for backwards compatibility 29 | -------------------------------------------------------------------------------- /fluent_pages/adminui/htmlpageadmin.py: -------------------------------------------------------------------------------- 1 | from django.contrib.admin.widgets import AdminTextareaWidget, AdminTextInputWidget 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | from .overrides import PageAdmin 5 | 6 | 7 | class HtmlPageAdmin(PageAdmin): 8 | """ 9 | The modeladmin configured to display :class:`~fluent_pages.models.HtmlPage` models. 10 | The :class:`~fluent_pages.models.HtmlPage` also displays a ``keywords`` and ``description`` field. 11 | 12 | This admin class defines another fieldset: :attr:`FIELDSET_SEO`. 13 | The default fieldset layout is: 14 | 15 | .. code-block:: python 16 | 17 | base_fieldsets = ( 18 | HtmlPageAdmin.FIELDSET_GENERAL, 19 | HtmlPageAdmin.FIELDSET_SEO, 20 | HtmlPageAdmin.FIELDSET_MENU, 21 | HtmlPageAdmin.FIELDSET_PUBLICATION, 22 | ) 23 | """ 24 | 25 | FIELDSET_SEO = ( 26 | _("SEO settings"), 27 | { 28 | "fields": ( 29 | "meta_title", 30 | "meta_keywords", 31 | "meta_description", 32 | "meta_image", 33 | "in_sitemaps", 34 | ), 35 | "classes": ("collapse",), 36 | }, 37 | ) 38 | 39 | base_fieldsets = ( 40 | PageAdmin.FIELDSET_GENERAL, 41 | FIELDSET_SEO, 42 | PageAdmin.FIELDSET_MENU, 43 | PageAdmin.FIELDSET_PUBLICATION, 44 | ) 45 | readonly_shared_fields = PageAdmin.readonly_shared_fields + ("in_sitemaps",) 46 | 47 | def formfield_for_dbfield(self, db_field, **kwargs): 48 | if db_field.name in ("meta_title", "meta_keywords"): 49 | kwargs.setdefault("widget", AdminTextInputWidget(attrs={"class": "vLargeTextField"})) 50 | if db_field.name == "meta_description": 51 | kwargs.setdefault("widget", AdminTextareaWidget(attrs={"rows": 3})) 52 | 53 | return super().formfield_for_dbfield(db_field, **kwargs) 54 | -------------------------------------------------------------------------------- /fluent_pages/adminui/overrides.py: -------------------------------------------------------------------------------- 1 | from fluent_utils.load import import_settings_class 2 | 3 | from fluent_pages import appsettings 4 | 5 | from .pageadmin import DefaultPageChildAdmin, DefaultPageParentAdmin 6 | 7 | # Allow to extend the admin. Note this is pretty invasive, 8 | # and custom changes always need to be tested. 9 | if not appsettings.FLUENT_PAGES_PARENT_ADMIN_MIXIN: 10 | PageParentAdmin = DefaultPageParentAdmin 11 | else: 12 | _ParentMixin = import_settings_class("FLUENT_PAGES_PARENT_ADMIN_MIXIN") 13 | PageParentAdmin = type("PageParentAdmin", (_ParentMixin, DefaultPageParentAdmin), {}) 14 | 15 | if not appsettings.FLUENT_PAGES_CHILD_ADMIN_MIXIN: 16 | PageChildAdmin = DefaultPageChildAdmin 17 | else: 18 | _ChildMixin = import_settings_class("FLUENT_PAGES_CHILD_ADMIN_MIXIN") 19 | PageChildAdmin = type("PageAdmin", (_ChildMixin, DefaultPageChildAdmin), {}) 20 | 21 | 22 | # Keep using the older import name everywhere. 23 | # Plugins don't have to be aware of the different between parent/child admins. 24 | PageAdmin = PageChildAdmin 25 | -------------------------------------------------------------------------------- /fluent_pages/adminui/pagelayoutadmin.py: -------------------------------------------------------------------------------- 1 | """ 2 | Admin screen for a layout (=template with metadata). 3 | """ 4 | from django.contrib import admin 5 | 6 | 7 | class PageLayoutAdmin(admin.ModelAdmin): 8 | # Config list page: 9 | list_display = ("title", "key") 10 | fieldsets = ((None, {"fields": ("title", "key", "template_path")}),) 11 | prepopulated_fields = {"key": ("title",)} 12 | -------------------------------------------------------------------------------- /fluent_pages/adminui/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions related to admin views. 3 | """ 4 | from django.urls import resolve, reverse 5 | 6 | from fluent_pages.models import UrlNode 7 | 8 | 9 | def get_page_admin_url(page): 10 | """ 11 | Return the admin URL for a page. 12 | """ 13 | return reverse("admin:fluent_pages_page_change", args=(page.pk,)) 14 | 15 | 16 | def get_current_edited_page(request): 17 | """ 18 | Return the :class:`~fluent_pages.models.Page` object which is currently being edited in the admin. 19 | Returns ``None`` if the current view isn't the "change view" of the the :class:`~fluent_pages.models.Page` model. 20 | """ 21 | match = resolve(request.path_info) 22 | if match.namespace == "admin" and match.url_name == "fluent_pages_page_change": 23 | page_id = int(match.args[0]) 24 | return UrlNode.objects.get(pk=page_id) 25 | return None 26 | -------------------------------------------------------------------------------- /fluent_pages/apps.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from django.apps import AppConfig 4 | from fluent_utils.load import import_module_or_none 5 | 6 | 7 | class FluentPagesConfig(AppConfig): 8 | name = "fluent_pages" 9 | verbose_name = "Fluent Pages" 10 | 11 | def ready(self): 12 | _register_subclass_types() 13 | 14 | 15 | def _register_subclass_types(): 16 | """ 17 | See if any page type plugin uses the old registration system, and register that 18 | Previously django-polymorphic registered all subclasses internally. 19 | Nowadays, all subclasses should just be registered in the regular admin. 20 | """ 21 | from django.contrib import admin 22 | 23 | RE_PLUGIN_MODULE = re.compile(r"\.page_type_plugins\b.*$") 24 | 25 | from fluent_pages.extensions import page_type_pool 26 | 27 | for plugin in page_type_pool.get_plugins(): 28 | if plugin.model in admin.site._registry: 29 | continue 30 | 31 | # First try to perform an admin file import, it may register itself. 32 | admin_path = RE_PLUGIN_MODULE.sub(".admin", plugin.__module__) 33 | module = import_module_or_none(admin_path) 34 | if module is not None and plugin.model in admin.site._registry: 35 | continue 36 | 37 | # Register the admin, since the plugin didn't do this. 38 | if getattr(plugin, "model_admin", None): 39 | admin.site.register(plugin.model, plugin.model_admin) 40 | -------------------------------------------------------------------------------- /fluent_pages/appsettings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Overview of all settings which can be customized. 3 | """ 4 | import os 5 | 6 | from django.conf import settings 7 | from django.core.exceptions import ImproperlyConfigured 8 | from django.utils.text import slugify 9 | from parler import appsettings as parler_appsettings 10 | from parler.utils import is_supported_django_language, normalize_language_code 11 | 12 | default_template_dir = next((t["DIRS"][0] for t in settings.TEMPLATES if t.get("DIRS")), None) 13 | 14 | # Templates 15 | FLUENT_PAGES_BASE_TEMPLATE = getattr( 16 | settings, "FLUENT_PAGES_BASE_TEMPLATE", "fluent_pages/base.html" 17 | ) 18 | FLUENT_PAGES_TEMPLATE_DIR = getattr(settings, "FLUENT_PAGES_TEMPLATE_DIR", default_template_dir) 19 | FLUENT_PAGES_RELATIVE_TEMPLATE_DIR = getattr(settings, "FLUENT_PAGES_RELATIVE_TEMPLATE_DIR", True) 20 | 21 | # User-visible settings 22 | FLUENT_PAGES_DEFAULT_IN_NAVIGATION = getattr(settings, "FLUENT_PAGES_DEFAULT_IN_NAVIGATION", True) 23 | FLUENT_PAGES_KEY_CHOICES = getattr(settings, "FLUENT_PAGES_KEY_CHOICES", ()) 24 | 25 | # Note: the default language setting is used during the migrations 26 | # Allow this module to have other settings, but default to the shared settings 27 | FLUENT_DEFAULT_LANGUAGE_CODE = getattr( 28 | settings, 29 | "FLUENT_DEFAULT_LANGUAGE_CODE", 30 | parler_appsettings.PARLER_DEFAULT_LANGUAGE_CODE, 31 | ) 32 | FLUENT_PAGES_DEFAULT_LANGUAGE_CODE = getattr( 33 | settings, "FLUENT_PAGES_DEFAULT_LANGUAGE_CODE", FLUENT_DEFAULT_LANGUAGE_CODE 34 | ) 35 | FLUENT_PAGES_LANGUAGES = getattr( 36 | settings, "FLUENT_PAGES_LANGUAGES", parler_appsettings.PARLER_LANGUAGES 37 | ) 38 | 39 | # Performance settings 40 | FLUENT_PAGES_PREFETCH_TRANSLATIONS = getattr(settings, "FLUENT_PAGES_PREFETCH_TRANSLATIONS", False) 41 | 42 | # Advanced settings 43 | FLUENT_PAGES_FILTER_SITE_ID = getattr(settings, "FLUENT_PAGES_FILTER_SITE_ID", True) 44 | FLUENT_PAGES_PARENT_ADMIN_MIXIN = getattr(settings, "FLUENT_PAGES_PARENT_ADMIN_MIXIN", None) 45 | FLUENT_PAGES_CHILD_ADMIN_MIXIN = getattr(settings, "FLUENT_PAGES_CHILD_ADMIN_MIXIN", None) 46 | 47 | ROBOTS_TXT_DISALLOW_ALL = getattr(settings, "ROBOTS_TXT_DISALLOW_ALL", settings.DEBUG) 48 | 49 | 50 | # Checks 51 | if not FLUENT_PAGES_TEMPLATE_DIR: 52 | raise ImproperlyConfigured( 53 | "The setting 'FLUENT_PAGES_TEMPLATE_DIR' or 'TEMPLATE_DIRS[0]' need to be defined!" 54 | ) 55 | else: 56 | # Clean settings 57 | FLUENT_PAGES_TEMPLATE_DIR = FLUENT_PAGES_TEMPLATE_DIR.rstrip("/" + os.path.sep) + os.path.sep 58 | 59 | # Test whether the template dir for page templates exists. 60 | settingName = ( 61 | "TEMPLATE_DIRS[0]" 62 | if not hasattr(settings, "FLUENT_PAGES_TEMPLATE_DIR") 63 | else "FLUENT_PAGES_TEMPLATE_DIR" 64 | ) 65 | if not os.path.isabs(FLUENT_PAGES_TEMPLATE_DIR): 66 | raise ImproperlyConfigured(f"The setting '{settingName}' needs to be an absolute path!") 67 | if not os.path.exists(FLUENT_PAGES_TEMPLATE_DIR): 68 | raise ImproperlyConfigured( 69 | "The path '{}' in the setting '{}' does not exist!".format( 70 | FLUENT_PAGES_TEMPLATE_DIR, settingName 71 | ) 72 | ) 73 | 74 | 75 | # Clean settings 76 | FLUENT_PAGES_DEFAULT_LANGUAGE_CODE = normalize_language_code(FLUENT_PAGES_DEFAULT_LANGUAGE_CODE) 77 | 78 | if not is_supported_django_language(FLUENT_PAGES_DEFAULT_LANGUAGE_CODE): 79 | raise ImproperlyConfigured( 80 | "FLUENT_PAGES_DEFAULT_LANGUAGE_CODE '{}' does not exist in LANGUAGES".format( 81 | FLUENT_PAGES_DEFAULT_LANGUAGE_CODE 82 | ) 83 | ) 84 | 85 | FLUENT_PAGES_LANGUAGES = parler_appsettings.add_default_language_settings( 86 | FLUENT_PAGES_LANGUAGES, 87 | "FLUENT_PAGES_LANGUAGES", 88 | hide_untranslated=False, 89 | hide_untranslated_menu_items=False, 90 | code=FLUENT_PAGES_DEFAULT_LANGUAGE_CODE, 91 | fallback=FLUENT_PAGES_DEFAULT_LANGUAGE_CODE, 92 | ) 93 | 94 | # Using a slug field, enforce keys as slugs too. 95 | FLUENT_PAGES_KEY_CHOICES = [(slugify(str(key)), title) for key, title in FLUENT_PAGES_KEY_CHOICES] 96 | 97 | 98 | def get_language_settings(language_code, site_id=None): 99 | """ 100 | Return the language settings for the current site 101 | """ 102 | if site_id is None: 103 | site_id = settings.SITE_ID 104 | 105 | for lang_dict in FLUENT_PAGES_LANGUAGES.get(site_id, ()): 106 | if lang_dict["code"] == language_code: 107 | return lang_dict 108 | 109 | return FLUENT_PAGES_LANGUAGES["default"] 110 | -------------------------------------------------------------------------------- /fluent_pages/extensions/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Special classes to extend the module; e.g. page type plugins. 3 | 4 | The extension mechanism is provided for projects that benefit 5 | from a tighter integration then the Django URLconf can provide. 6 | 7 | The API uses a registration system. 8 | While plugins can be easily detected via ``__subclasses__()``, the register approach is less magic and more explicit. 9 | Having to do an explicit register ensures future compatibility with other API's like reversion. 10 | """ 11 | from .pagetypebase import PageTypePlugin 12 | from .pagetypepool import PageTypeAlreadyRegistered, PageTypeNotFound, PageTypePool, page_type_pool 13 | 14 | __all__ = ( 15 | "PageTypePlugin", 16 | "PageTypeAlreadyRegistered", 17 | "PageTypeNotFound", 18 | "PageTypePool", 19 | "page_type_pool", 20 | ) 21 | -------------------------------------------------------------------------------- /fluent_pages/forms/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Form fields 3 | """ 4 | from .fields import PageChoiceField, RelativeRootPathField, TemplateFilePathField 5 | 6 | __all__ = ("TemplateFilePathField", "RelativeRootPathField", "PageChoiceField") 7 | -------------------------------------------------------------------------------- /fluent_pages/integration/__init__.py: -------------------------------------------------------------------------------- 1 | # import apipkg 2 | # 3 | # realmodule = "fluent_pages.integration._fluent_contents" 4 | # apipkg.initpkg(__name__, { 5 | # 'fluent_contents': { 6 | # 'FluentContentsPage': realmodule + ".models.FluentContentsPage", 7 | # 'FluentContentsPageAdmin': realmodule + ".admin.FluentContentsPageAdmin", 8 | # 'FluentContentsPagePlugin': realmodule + ".admin.FluentContentsPagePlugin", 9 | # } 10 | # }) 11 | -------------------------------------------------------------------------------- /fluent_pages/integration/fluent_contents/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | The base classes to create a page which can display *django-fluent-contents* models. 3 | 4 | The API to interface with *django-fluent-contents* is public, and documented for this reason. 5 | In fact, this module is just a tiny bridge between the page type plugins and the *django-fluent-pages* API. 6 | It can be used to create custom page types that display :class:`~fluent_contents.models.ContentItem` objects. 7 | 8 | The following parts are provided: 9 | 10 | * The admin class; :class:`.admin.FluentContentsPageAdmin` 11 | * The page type model: :class:`.models.FluentContentsPage` 12 | * The plugin class: :class:`.page_type_plugins.FluentContentsPagePlugin` 13 | 14 | These classes can be imported from their respective subpackages:: 15 | 16 | from fluent_pages.integration.fluent_contents.admin import FluentContentsPageAdmin 17 | from fluent_pages.integration.fluent_contents.models import FluentContentsPage 18 | from fluent_pages.integration.fluent_contents.page_type_plugins import FluentContentsPagePlugin 19 | """ 20 | 21 | # There used to be more imports here, but that turned out to be a really bad idea. 22 | # Exposing the model, plugin and admin in one __init__ package means importing the admin, 23 | # and potentially triggering circular imports because of that (e.g. get_user_model(), load all apps) 24 | from .models import FluentContentsPage # noqa 25 | -------------------------------------------------------------------------------- /fluent_pages/integration/fluent_contents/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | The model integration. 3 | Everything can be imported from ``__init__.py``. 4 | """ 5 | 6 | from django.utils.translation import gettext_lazy as _ 7 | from fluent_contents.models import Placeholder 8 | from fluent_contents.models.fields import ContentItemRelation, PlaceholderRelation 9 | 10 | from fluent_pages.models import HtmlPage, UrlNodeManager 11 | 12 | 13 | class FluentContentsPage(HtmlPage): 14 | """ 15 | The base model to create a Page object which hosts placeholders and content items. 16 | """ 17 | 18 | # Access to fluent-contents via the model 19 | # This also makes sure that the admin delete page will list the models 20 | # because they are liked via a GenericForeignKey 21 | 22 | #: Related manager to access all placeholders 23 | placeholder_set = PlaceholderRelation() 24 | 25 | #: Related manager to access all content items 26 | contentitem_set = ContentItemRelation() 27 | 28 | # As this is an abstract model, the default manager is reset in Django 1.10. 29 | objects = UrlNodeManager() 30 | 31 | class Meta: 32 | abstract = True 33 | verbose_name = _("Page") 34 | verbose_name_plural = _("Pages") 35 | 36 | def create_placeholder(self, slot, role="m", title=None): 37 | """ 38 | Create a placeholder on this page. 39 | 40 | To fill the content items, use 41 | :func:`ContentItemModel.objects.create_for_placeholder() `. 42 | 43 | :rtype: :class:`~fluent_contents.models.Placeholder` 44 | """ 45 | return Placeholder.objects.create_for_object(self, slot, role=role, title=title) 46 | 47 | def get_placeholder_by_slot(self, slot): 48 | """ 49 | Return a placeholder that is part of this page. 50 | :rtype: :class:`~fluent_contents.models.Placeholder` 51 | """ 52 | return self.placeholder_set.filter(slot=slot) 53 | 54 | def get_content_items_by_slot(self, slot): 55 | """ 56 | Return all content items of the page, which are stored in the given slot name. 57 | :rtype: :class:`~fluent_contents.models.manager.ContentItemQuerySet` 58 | """ 59 | # Placeholder.objects.get_by_slot(self, slot) 60 | placeholder = self.placeholder_set.filter(slot=slot) 61 | return self.contentitem_set.filter(placeholder=placeholder) 62 | -------------------------------------------------------------------------------- /fluent_pages/integration/fluent_contents/page_type_plugins.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from fluent_pages.extensions import PageTypePlugin 4 | 5 | from .admin import FluentContentsPageAdmin 6 | 7 | 8 | class FluentContentsPagePlugin(PageTypePlugin): 9 | """ 10 | Base plugin to render a page with content items. 11 | """ 12 | 13 | #: Defines the model to use to store the custom fields. 14 | #: It must derive from :class:`~fluent_pages.integration.fluent_contents.FluentContentsPage`. 15 | model = None 16 | 17 | #: Defines the default :class:`~django.contrib.admin.ModelAdmin` class for the add/edit screen. 18 | #: It should inherit from :class:`~fluent_pages.integration.fluent_contents.FluentContentsPageAdmin`. 19 | model_admin = FluentContentsPageAdmin 20 | 21 | def get_render_template(self, request, fluentpage, **kwargs): 22 | """ 23 | Overwritten to automatically pick up the template used in the admin. 24 | """ 25 | if self.render_template: 26 | return self.render_template 27 | 28 | try: 29 | # Try using the actual admin registered, 30 | # not what the plugin promises. 31 | model_admin = admin.site._registry[self.model] 32 | except KeyError: 33 | # Fallback to the old model_admin attribute 34 | model_admin = self.model_admin 35 | 36 | # When the model admin isn't a FluentContentsPageAdmin, 37 | # return None just as if the attribute was not filled in. 38 | return getattr(model_admin, "placeholder_layout_template", None) 39 | -------------------------------------------------------------------------------- /fluent_pages/integration/fluent_contents/tests.py: -------------------------------------------------------------------------------- 1 | from fluent_pages.integration.fluent_contents import FluentContentsPage 2 | from fluent_pages.models import UrlNodeManager 3 | from fluent_pages.tests.utils import AppTestCase 4 | 5 | 6 | class FluentContentsPageTests(AppTestCase): 7 | def test_default_manager(self): 8 | """ 9 | Test that the default manager is correct. 10 | """ 11 | self.assertIsInstance(FluentContentsPage._default_manager, UrlNodeManager) 12 | 13 | class ExamplePage(FluentContentsPage): 14 | pass 15 | 16 | self.assertIsInstance(ExamplePage._default_manager, UrlNodeManager) 17 | -------------------------------------------------------------------------------- /fluent_pages/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-fluent/django-fluent-pages/621d756542f23d1e2f7679bffe6fcb81b82df005/fluent_pages/management/__init__.py -------------------------------------------------------------------------------- /fluent_pages/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-fluent/django-fluent-pages/621d756542f23d1e2f7679bffe6fcb81b82df005/fluent_pages/management/commands/__init__.py -------------------------------------------------------------------------------- /fluent_pages/management/commands/make_language_redirects.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from optparse import make_option 3 | 4 | from django.conf import settings 5 | from django.contrib.sites.models import Site 6 | from django.core.management.base import BaseCommand, CommandError 7 | from django.utils import translation 8 | from django.utils.translation import get_language_info 9 | from parler.utils.context import switch_language 10 | 11 | from fluent_pages.models import UrlNode 12 | 13 | 14 | class Command(BaseCommand): 15 | """ 16 | Generate rewrite/redirect rules for the web server to redirect a single unmaintained 17 | language to another one. 18 | """ 19 | 20 | help = "Find all pages of a given language, and redirect to the canonical version." 21 | args = "language" 22 | 23 | def add_arguments(self, parser): 24 | super().add_arguments(parser) 25 | parser.add_argument( 26 | "--format", 27 | default="nginx", 28 | help='Choose the output format, defaults to "nginx"', 29 | ), 30 | parser.add_argument( 31 | "--site", default=int(settings.SITE_ID), help="Choose the site ID to " 32 | ), 33 | parser.add_argument("--from"), 34 | parser.add_argument("--host"), 35 | parser.add_argument("--to", default=settings.LANGUAGE_CODE), 36 | 37 | def handle(self, *args, **options): 38 | if args: 39 | raise CommandError("Command doesn't accept any arguments") 40 | 41 | site = options["site"] 42 | host = options["host"] 43 | from_lang = options["from"] 44 | to_lang = options["to"] 45 | 46 | if not from_lang: 47 | raise CommandError("Provide a --from=.. language to redirect for") 48 | if not host: 49 | host = Site.objects.get_current().domain 50 | 51 | if "://" not in host: 52 | host = f"http://{host}" 53 | 54 | from_name = get_language_info(from_lang)["name"] 55 | to_name = get_language_info(to_lang)["name"] 56 | 57 | with translation.override(from_lang): 58 | qs = ( 59 | UrlNode.objects.parent_site(site) 60 | .non_polymorphic() 61 | .translated(to_lang) 62 | .order_by("translations___cached_url") 63 | ) 64 | if not qs: 65 | raise CommandError(f"No URLs found for site {site} in {from_name}") 66 | 67 | self.stdout.write( 68 | f"# Redirecting all translated {from_name} URLs to the {to_name} site\n" 69 | ) 70 | self.stdout.write("# Generated using {}".format(" ".join(sys.argv))) 71 | 72 | for page in qs: 73 | from_url = page.default_url 74 | with switch_language(page, to_lang): 75 | to_url = page.get_absolute_url() 76 | 77 | if from_url == to_url: 78 | continue 79 | 80 | if from_url.endswith("/"): 81 | from_regexp = from_url.rstrip("/") 82 | from_rule = f"~ ^{from_regexp}(/|$)" 83 | else: 84 | from_regexp = from_url 85 | from_rule = f"= {from_regexp}" 86 | 87 | if page.plugin.urls: 88 | self.stdout.write( 89 | "location {0} {{ rewrite ^{1}(.*)$ {2}{3}$1; }}\n".format( 90 | from_rule, from_regexp, host, to_url.rstrip("/") 91 | ) 92 | ) 93 | else: 94 | self.stdout.write( 95 | f"location {from_rule} {{ return 301 {host}{to_url}; }}\n" 96 | ) 97 | 98 | # Final redirect for all identical URLs 99 | self.stdout.write("\n# Redirect all remaining and identical URls:\n") 100 | self.stdout.write(f"location / {{ rewrite ^/(.*)$ {host}/$1 permanent; }}\n") 101 | -------------------------------------------------------------------------------- /fluent_pages/management/commands/remove_stale_pages.py: -------------------------------------------------------------------------------- 1 | from django.contrib.contenttypes.models import ContentType 2 | from django.core.management.base import BaseCommand 3 | from django.db.models import Model 4 | 5 | from fluent_pages.extensions import PageTypeNotFound 6 | from fluent_pages.models import UrlNode 7 | 8 | 9 | class Command(BaseCommand): 10 | help = "Remove UrlNodes which are stale, because their model is removed." 11 | 12 | def add_arguments(self, parser): 13 | super().add_arguments(parser) 14 | parser.add_argument( 15 | "-p", 16 | "--dry-run", 17 | action="store_true", 18 | dest="dry_run", 19 | help="Only list what will change, don't make the actual changes.", 20 | ) 21 | 22 | def handle(self, *args, **options): 23 | self.dry_run = options["dry_run"] 24 | stale_cts = self.get_stale_content_types() 25 | self.remove_stale_pages(stale_cts) 26 | 27 | def get_stale_content_types(self): 28 | stale_cts = {} 29 | for ct in ContentType.objects.all(): 30 | if ct.model_class() is None: 31 | stale_cts[ct.pk] = ct 32 | return stale_cts 33 | 34 | def remove_stale_pages(self, stale_cts): 35 | """ 36 | See if there are items that point to a removed model. 37 | """ 38 | stale_ct_ids = list(stale_cts.keys()) 39 | pages = ( 40 | UrlNode.objects.non_polymorphic() # very important, or polymorphic skips them on fetching derived data 41 | .filter(polymorphic_ctype__in=stale_ct_ids) 42 | .order_by("polymorphic_ctype", "pk") 43 | ) 44 | if not pages: 45 | self.stdout.write("No stale pages found.") 46 | return 47 | 48 | if self.dry_run: 49 | self.stdout.write("The following pages are stale:") 50 | else: 51 | self.stdout.write("The following pages were stale:") 52 | 53 | removed_pages = 0 54 | for page in pages: 55 | ct = stale_cts[page.polymorphic_ctype_id] 56 | self.stdout.write( 57 | "- #{id} points to removed {app_label}.{model}".format( 58 | id=page.pk, app_label=ct.app_label, model=ct.model 59 | ) 60 | ) 61 | 62 | if not self.dry_run: 63 | try: 64 | page.delete() 65 | removed_pages += 1 66 | except PageTypeNotFound: 67 | Model.delete(page) 68 | 69 | if removed_pages: 70 | self.stdout.write( 71 | "Note, when the removed pages contain content items, " 72 | "also call `manage.py remove_stale_contentitems --remove-unreferenced" 73 | ) 74 | -------------------------------------------------------------------------------- /fluent_pages/migrations/0002_add_htmlpage_meta_image.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.11.4 on 2017-08-04 19:39 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [("fluent_pages", "0001_initial")] 9 | 10 | operations = [ 11 | migrations.AddField( 12 | model_name="htmlpagetranslation", 13 | name="meta_image", 14 | field=models.ImageField( 15 | blank=True, 16 | help_text="This allows social media sites to pick a default image.", 17 | max_length=100, 18 | null=True, 19 | verbose_name="example image", 20 | ), 21 | ) 22 | ] 23 | -------------------------------------------------------------------------------- /fluent_pages/migrations/0003_set_htmlpage_defaults.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.11.4 on 2017-08-05 08:55 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | def _set_defaults(apps, schema_editor): 7 | HtmlPageTranslation = apps.get_model("fluent_pages", "htmlpagetranslation") 8 | HtmlPageTranslation.objects.filter(meta_title=None).update(meta_title="") 9 | HtmlPageTranslation.objects.filter(meta_keywords=None).update(meta_keywords="") 10 | HtmlPageTranslation.objects.filter(meta_description=None).update(meta_description="") 11 | HtmlPageTranslation.objects.filter(meta_image=None).update(meta_image="") 12 | 13 | 14 | def _set_nulls(apps, schema_editor): 15 | HtmlPageTranslation = apps.get_model("fluent_pages", "htmlpagetranslation") 16 | HtmlPageTranslation.objects.filter(meta_title="").update(meta_title=None) 17 | HtmlPageTranslation.objects.filter(meta_keywords="").update(meta_keywords=None) 18 | HtmlPageTranslation.objects.filter(meta_description="").update(meta_description=None) 19 | HtmlPageTranslation.objects.filter(meta_image="").update(meta_image=None) 20 | 21 | 22 | class Migration(migrations.Migration): 23 | 24 | dependencies = [("fluent_pages", "0002_add_htmlpage_meta_image")] 25 | 26 | operations = [migrations.RunPython(_set_defaults, _set_nulls)] 27 | -------------------------------------------------------------------------------- /fluent_pages/migrations/0004_add_htmlpage_not_null.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.11.4 on 2017-08-05 08:59 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [("fluent_pages", "0003_set_htmlpage_defaults")] 9 | 10 | operations = [ 11 | migrations.AlterField( 12 | model_name="htmlpagetranslation", 13 | name="meta_description", 14 | field=models.CharField( 15 | blank=True, 16 | default="", 17 | help_text="Typically, about 160 characters will be shown in search engines", 18 | max_length=255, 19 | verbose_name="description", 20 | ), 21 | ), 22 | migrations.AlterField( 23 | model_name="htmlpagetranslation", 24 | name="meta_image", 25 | field=models.ImageField( 26 | blank=True, 27 | default="", 28 | help_text="This allows social media sites to pick a default image.", 29 | upload_to="", 30 | verbose_name="example image", 31 | ), 32 | ), 33 | migrations.AlterField( 34 | model_name="htmlpagetranslation", 35 | name="meta_keywords", 36 | field=models.CharField( 37 | blank=True, default="", max_length=255, verbose_name="keywords" 38 | ), 39 | ), 40 | migrations.AlterField( 41 | model_name="htmlpagetranslation", 42 | name="meta_title", 43 | field=models.CharField( 44 | blank=True, 45 | default="", 46 | help_text="When this field is not filled in, the menu title text will be used.", 47 | max_length=255, 48 | verbose_name="page title", 49 | ), 50 | ), 51 | ] 52 | -------------------------------------------------------------------------------- /fluent_pages/migrations/0005_author_on_delete_set_null.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.11.9 on 2018-01-30 19:17 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [("fluent_pages", "0004_add_htmlpage_not_null")] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="urlnode", 15 | name="author", 16 | field=models.ForeignKey( 17 | editable=False, 18 | null=True, 19 | on_delete=django.db.models.deletion.SET_NULL, 20 | to=settings.AUTH_USER_MODEL, 21 | verbose_name="author", 22 | ), 23 | ) 24 | ] 25 | -------------------------------------------------------------------------------- /fluent_pages/migrations/0006_remove_mptt_indices.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-02-27 12:16 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import parler.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('fluent_pages', '0005_author_on_delete_set_null'), 12 | ] 13 | 14 | operations = [ 15 | # As defined in django-mptt >= 0.10.0 16 | migrations.AlterField( 17 | model_name='urlnode', 18 | name='level', 19 | field=models.PositiveIntegerField(editable=False), 20 | ), 21 | migrations.AlterField( 22 | model_name='urlnode', 23 | name='lft', 24 | field=models.PositiveIntegerField(editable=False), 25 | ), 26 | migrations.AlterField( 27 | model_name='urlnode', 28 | name='rght', 29 | field=models.PositiveIntegerField(editable=False), 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /fluent_pages/migrations/0007_use_parler_transactionsfk.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2023-10-16 12:08 2 | 3 | from django.db import migrations 4 | import django.db.models.deletion 5 | import parler.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('fluent_pages', '0006_remove_mptt_indices'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='urlnode_translation', 17 | name='master', 18 | field=parler.fields.TranslationsForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='translations', to='fluent_pages.urlnode'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /fluent_pages/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-fluent/django-fluent-pages/621d756542f23d1e2f7679bffe6fcb81b82df005/fluent_pages/migrations/__init__.py -------------------------------------------------------------------------------- /fluent_pages/models/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | The data layer of the CMS, exposing all database models. 3 | 4 | The objects can be imported from the main package. 5 | There are several sub packages: 6 | 7 | db: The database models 8 | managers: Additional manager classes 9 | modeldata: Classes that expose model data in a sane way (for template designers) 10 | navigation: The menu navigation nodes (for template designers) 11 | """ 12 | 13 | # Like django.db.models, or django.forms, 14 | # have everything split into several packages 15 | from django.conf import settings 16 | 17 | from fluent_pages.forms.fields import PageChoiceField 18 | 19 | from .db import ( 20 | HtmlPage, 21 | HtmlPageTranslation, 22 | Page, 23 | PageLayout, 24 | ParentTranslationDoesNotExist, 25 | UrlNode, 26 | UrlNode_Translation, 27 | ) 28 | from .managers import UrlNodeManager, UrlNodeQuerySet 29 | 30 | __all__ = [ 31 | "UrlNode", 32 | "UrlNode_Translation", 33 | "UrlNodeManager", 34 | "UrlNodeQuerySet", 35 | "ParentTranslationDoesNotExist", 36 | "Page", 37 | "HtmlPage", 38 | "HtmlPageTranslation", 39 | "PageLayout", 40 | ] 41 | 42 | 43 | def _register_cmsfield_url_type(): 44 | try: 45 | from any_urlfield.forms.widgets import SimpleRawIdWidget 46 | from any_urlfield.models import AnyUrlField 47 | except ImportError: 48 | pass 49 | else: 50 | # Allow lambda parameter for late evaluation. 51 | AnyUrlField.register_model( 52 | Page, form_field=lambda: PageChoiceField(widget=SimpleRawIdWidget(Page)) 53 | ) 54 | 55 | 56 | if "any_urlfield" in settings.INSTALLED_APPS: 57 | _register_cmsfield_url_type() 58 | -------------------------------------------------------------------------------- /fluent_pages/models/fields.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django.db import models 3 | from django.db.models.fields.related import ForwardManyToOneDescriptor 4 | from django.utils.translation import gettext_lazy as _ 5 | from polymorphic_tree.models import PolymorphicTreeForeignKey 6 | 7 | from fluent_pages import forms 8 | 9 | 10 | class TemplateFilePathField(models.FilePathField): 11 | """ 12 | A field to select a template path. 13 | """ 14 | 15 | def __init__(self, verbose_name=None, path="", **kwargs): 16 | defaults = dict(match=r".*\.html$", recursive=True) 17 | defaults.update(kwargs) 18 | super().__init__(verbose_name, path=path, **defaults) 19 | 20 | def formfield(self, **kwargs): 21 | # Like the FilePathField, the formfield does the actual work 22 | defaults = {"form_class": forms.TemplateFilePathField} 23 | defaults.update(kwargs) 24 | return super().formfield(**defaults) 25 | 26 | def deconstruct(self): 27 | name, path, args, kwargs = super().deconstruct() 28 | if "path" in kwargs: 29 | del kwargs["path"] 30 | return name, path, args, kwargs 31 | 32 | 33 | class TranslatedForeignKeyDescriptor(ForwardManyToOneDescriptor): 34 | def __get__(self, instance, instance_type=None): 35 | # let the .parent return an object in the same language as our selves. 36 | # note: when the object is switched to a different language, this updates the shared/cached parent. 37 | obj = super().__get__(instance, instance_type) 38 | if instance is not None and obj is not None: 39 | obj.set_current_language(instance.get_current_language()) 40 | return obj 41 | 42 | 43 | class PageTreeForeignKey(PolymorphicTreeForeignKey): 44 | """ 45 | A customized version of the :class:`~polymorphic_tree.models.PolymorphicTreeForeignKey`. 46 | """ 47 | 48 | default_error_messages = { 49 | "required": _("This page type should have a parent."), 50 | "no_children_allowed": _("The selected page cannot have sub pages."), 51 | "child_not_allowed": _("The selected page cannot have this page type as a child!"), 52 | } 53 | 54 | def contribute_to_class(self, cls, name, **kwargs): 55 | super().contribute_to_class(cls, name, **kwargs) 56 | setattr( 57 | cls, self.name, TranslatedForeignKeyDescriptor(self) 58 | ) # override what ForeignKey does. 59 | -------------------------------------------------------------------------------- /fluent_pages/models/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Custom generic managers 3 | """ 4 | from django.apps import apps 5 | from django.contrib.sites.models import Site 6 | from django.db import models 7 | from django.db.models.query import QuerySet 8 | 9 | 10 | def prefill_parent_site(page): 11 | """ 12 | Optimize the ``parent_site`` field of a page if possible, fill it's cache. 13 | """ 14 | if apps.is_installed("django.contrib.sites"): 15 | current_site = Site.objects.get_current() 16 | 17 | if page.parent_site_id == current_site.id: 18 | page.parent_site = current_site # Fill the ORM cache. 19 | 20 | 21 | # Based on django-queryset-transform. 22 | # This object however, operates on a per-object instance 23 | # without breaking the result generators 24 | 25 | 26 | class DecoratingQuerySet(QuerySet): 27 | """ 28 | An enhancement of the QuerySet which allows objects to be decorated 29 | with extra properties before they are returned. 30 | 31 | When using this method with *django-polymorphic* or *django-hvad*, make sure this 32 | class is first in the chain of inherited classes. 33 | """ 34 | 35 | def __init__(self, *args, **kwargs): 36 | super().__init__(*args, **kwargs) 37 | self._decorate_funcs = [] 38 | 39 | def _clone(self): 40 | c = super()._clone() 41 | c._decorate_funcs = self._decorate_funcs 42 | return c 43 | 44 | def decorate(self, fn): 45 | """ 46 | Register a function which will decorate a retrieved object before it's returned. 47 | """ 48 | if fn not in self._decorate_funcs: 49 | self._decorate_funcs.append(fn) 50 | return self 51 | 52 | def _fetch_all(self): 53 | # Make sure the current language is assigned when Django fetches the data. 54 | # This low-level method is overwritten as that works better across Django versions. 55 | # Alternatives include: 56 | # - overwriting iterator() for Django <= 1.10 57 | # - hacking _iterable_class, which breaks django-polymorphic 58 | super()._fetch_all() 59 | for obj in self._result_cache: 60 | for fn in self._decorate_funcs: 61 | fn(obj) 62 | 63 | 64 | class DecoratorManager(models.Manager): 65 | """ 66 | The manager class which ensures the enhanced DecoratorQuerySet object is used. 67 | """ 68 | 69 | def get_queryset(self): 70 | return DecoratingQuerySet(self.model, using=self._db) 71 | -------------------------------------------------------------------------------- /fluent_pages/pagetypes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-fluent/django-fluent-pages/621d756542f23d1e2f7679bffe6fcb81b82df005/fluent_pages/pagetypes/__init__.py -------------------------------------------------------------------------------- /fluent_pages/pagetypes/flatpage/__init__.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.exceptions import ImproperlyConfigured 3 | 4 | VERSION = (0, 1) 5 | 6 | backendapp = "django_wysiwyg" 7 | 8 | # Do some settings checks. 9 | if backendapp not in settings.INSTALLED_APPS: 10 | raise ImproperlyConfigured( 11 | "The '{}' application is required to use the '{}' page type.".format( 12 | backendapp, "flatpage" 13 | ) 14 | ) 15 | 16 | try: 17 | import django_wysiwyg # noqa 18 | except ImportError: 19 | raise ImportError("The 'django-wysiwyg' package is required to use the 'flatpage' page type.") 20 | -------------------------------------------------------------------------------- /fluent_pages/pagetypes/flatpage/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from fluent_pages.admin import HtmlPageAdmin 4 | 5 | from .models import FlatPage 6 | 7 | 8 | @admin.register(FlatPage) 9 | class FlatPageAdmin(HtmlPageAdmin): 10 | readonly_shared_fields = HtmlPageAdmin.readonly_shared_fields + ( 11 | "template_name", 12 | "content", 13 | ) 14 | 15 | # Implicitly loaded: 16 | # change_form_template = "admin/fluent_pages/pagetypes/flatpage/change_form.html" 17 | # Not defined here explicitly, so other templates can override this function. 18 | # and use {% extends default_change_form_template %} instead. 19 | -------------------------------------------------------------------------------- /fluent_pages/pagetypes/flatpage/appsettings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | # Purposefully using the same variable names as fluent_contents.plugins.text 4 | FLUENT_TEXT_CLEAN_HTML = getattr(settings, "FLUENT_TEXT_CLEAN_HTML", False) 5 | FLUENT_TEXT_SANITIZE_HTML = getattr(settings, "FLUENT_TEXT_SANITIZE_HTML", False) 6 | -------------------------------------------------------------------------------- /fluent_pages/pagetypes/flatpage/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations, models 2 | 3 | 4 | class Migration(migrations.Migration): 5 | 6 | dependencies = [("fluent_pages", "0001_initial")] 7 | 8 | operations = [ 9 | migrations.CreateModel( 10 | name="FlatPage", 11 | fields=[ 12 | ( 13 | "urlnode_ptr", 14 | models.OneToOneField( 15 | parent_link=True, 16 | auto_created=True, 17 | primary_key=True, 18 | serialize=False, 19 | to="fluent_pages.UrlNode", 20 | on_delete=models.CASCADE, 21 | ), 22 | ), 23 | ( 24 | "template_name", 25 | models.CharField( 26 | default="fluent_pages/pagetypes/flatpage/default.html", 27 | max_length=200, 28 | null=True, 29 | editable=False, 30 | verbose_name="Layout", 31 | ), 32 | ), 33 | ("content", models.TextField(verbose_name="Content", blank=True)), 34 | ], 35 | options={ 36 | "db_table": "pagetype_flatpage_flatpage", 37 | "verbose_name": "Flat Page", 38 | "verbose_name_plural": "Flat Pages", 39 | }, 40 | bases=("fluent_pages.htmlpage",), 41 | ) 42 | ] 43 | -------------------------------------------------------------------------------- /fluent_pages/pagetypes/flatpage/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-fluent/django-fluent-pages/621d756542f23d1e2f7679bffe6fcb81b82df005/fluent_pages/pagetypes/flatpage/migrations/__init__.py -------------------------------------------------------------------------------- /fluent_pages/pagetypes/flatpage/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.translation import gettext_lazy as _ 3 | from django_wysiwyg.utils import clean_html, sanitize_html 4 | 5 | from fluent_pages.models import HtmlPage 6 | from fluent_pages.pagetypes.flatpage import appsettings 7 | 8 | 9 | class FlatPage(HtmlPage): 10 | """ 11 | A ```FlatPage``` represents a simple HTML page. 12 | """ 13 | 14 | # Allow NULL in the layout, so this system can still be made optional in the future in favor of a configuration setting. 15 | template_name = models.CharField( 16 | _("Layout"), 17 | max_length=200, 18 | default="fluent_pages/pagetypes/flatpage/default.html", 19 | editable=False, 20 | null=True, 21 | ) 22 | content = models.TextField(_("Content"), blank=True) 23 | 24 | # Other fields, such as "registration_required" are not reused, 25 | # because these should be implemented globally in the base page model, or a pluggable authorization layer. 26 | 27 | class Meta: 28 | verbose_name = _("Flat Page") 29 | verbose_name_plural = _("Flat Pages") 30 | 31 | def save(self, *args, **kwargs): 32 | # Make well-formed if requested 33 | if appsettings.FLUENT_TEXT_CLEAN_HTML: 34 | self.content = clean_html(self.content) 35 | 36 | # Remove unwanted tags if requested 37 | if appsettings.FLUENT_TEXT_SANITIZE_HTML: 38 | self.content = sanitize_html(self.content) 39 | 40 | super().save(*args, **kwargs) 41 | -------------------------------------------------------------------------------- /fluent_pages/pagetypes/flatpage/page_type_plugins.py: -------------------------------------------------------------------------------- 1 | from django.utils.safestring import mark_safe 2 | 3 | from fluent_pages.extensions import PageTypePlugin, page_type_pool 4 | from fluent_pages.pagetypes.flatpage.models import FlatPage 5 | 6 | 7 | @page_type_pool.register 8 | class FlatPagePlugin(PageTypePlugin): 9 | model = FlatPage 10 | sort_priority = 11 11 | 12 | def get_render_template(self, request, flatpage, **kwargs): 13 | return flatpage.template_name 14 | 15 | def get_context(self, request, page, **kwargs): 16 | context = super().get_context(request, page, **kwargs) 17 | 18 | # Just like django.contrib.flatpages, mark content as safe: 19 | page = context["page"] 20 | page.content = mark_safe(page.content) 21 | 22 | return context 23 | -------------------------------------------------------------------------------- /fluent_pages/pagetypes/flatpage/templates/admin/fluent_pages/pagetypes/flatpage/change_form.html: -------------------------------------------------------------------------------- 1 | {% extends base_change_form_template %}{# = "admin/fluent_pages/page/change_form.html" #} 2 | {% load wysiwyg %} 3 | 4 | {% block extrahead %}{{ block.super }} 5 | {% wysiwyg_setup %} 6 | {% wysiwyg_editor "id_content" %} 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /fluent_pages/pagetypes/flatpage/templates/fluent_pages/pagetypes/flatpage/default.html: -------------------------------------------------------------------------------- 1 | {% extends FLUENT_PAGES_BASE_TEMPLATE %} 2 | 3 | {% block meta-keywords %}{{ page.keywords }}{% endblock %} 4 | {% block meta-description %}{{ page.description }}{% endblock %} 5 | 6 | {% block title %}{{ page.title }}{% endblock %} 7 | 8 | {% block content %} 9 | {{ page.content }} 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /fluent_pages/pagetypes/flatpage/tests.py: -------------------------------------------------------------------------------- 1 | from fluent_pages.models import UrlNodeManager, UrlNodeQuerySet 2 | from fluent_pages.pagetypes.flatpage.models import FlatPage 3 | from fluent_pages.tests.utils import AppTestCase 4 | 5 | 6 | class FlatPageTests(AppTestCase): 7 | def test_default_manager(self): 8 | """ 9 | Test that the default manager is correct. 10 | """ 11 | self.assertIsInstance(FlatPage._default_manager, UrlNodeManager) 12 | self.assertIsInstance(FlatPage.objects.all(), UrlNodeQuerySet) 13 | -------------------------------------------------------------------------------- /fluent_pages/pagetypes/fluentpage/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-fluent/django-fluent-pages/621d756542f23d1e2f7679bffe6fcb81b82df005/fluent_pages/pagetypes/fluentpage/__init__.py -------------------------------------------------------------------------------- /fluent_pages/pagetypes/fluentpage/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations, models 2 | 3 | 4 | class Migration(migrations.Migration): 5 | 6 | dependencies = [("fluent_pages", "0001_initial")] 7 | 8 | operations = [ 9 | migrations.CreateModel( 10 | name="FluentPage", 11 | fields=[ 12 | ( 13 | "urlnode_ptr", 14 | models.OneToOneField( 15 | parent_link=True, 16 | auto_created=True, 17 | primary_key=True, 18 | serialize=False, 19 | to="fluent_pages.UrlNode", 20 | on_delete=models.CASCADE, 21 | ), 22 | ), 23 | ( 24 | "layout", 25 | models.ForeignKey( 26 | verbose_name="Layout", 27 | to="fluent_pages.PageLayout", 28 | on_delete=models.CASCADE, 29 | null=True, 30 | ), 31 | ), 32 | ], 33 | options={ 34 | "abstract": False, 35 | "db_table": "pagetype_fluentpage_fluentpage", 36 | "verbose_name": "Page", 37 | "verbose_name_plural": "Pages", 38 | "permissions": (("change_page_layout", "Can change Page layout"),), 39 | }, 40 | bases=("fluent_pages.htmlpage",), 41 | ) 42 | ] 43 | -------------------------------------------------------------------------------- /fluent_pages/pagetypes/fluentpage/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-fluent/django-fluent-pages/621d756542f23d1e2f7679bffe6fcb81b82df005/fluent_pages/pagetypes/fluentpage/migrations/__init__.py -------------------------------------------------------------------------------- /fluent_pages/pagetypes/fluentpage/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | from fluent_pages.integration.fluent_contents.models import FluentContentsPage 5 | from fluent_pages.models import PageLayout, UrlNodeManager 6 | 7 | 8 | # This all exists for backwards compatibility 9 | # The new v0.9 method is using fluent_pages.integration.fluent_contents, instead of directly inheriting this app. 10 | # In that way, the pagetypes are stand-alone apps again. 11 | class AbstractFluentPage(FluentContentsPage): 12 | """ 13 | A ```FluentPage``` represents one HTML page of the site. 14 | 15 | .. note:: 16 | 17 | If you really like to use the ``layout`` field in our custom applications, inherit from this class. 18 | Otherwise, please use :class:`fluent_pages.integration.fluent_contents.models.FluentContentsPage` instead. 19 | 20 | This class is abstract, so it's easy to reuse the same CMS functionality in your custom page types 21 | without introducing another table/join indirection in the database. Naturally, the same layout mechanism is used. 22 | In case the ``layout`` should be handled differently, please consider building a variation of this page type application. 23 | """ 24 | 25 | # Allow NULL in the layout, so this system can still be made optional in the future in favor of a configuration setting. 26 | layout = models.ForeignKey( 27 | PageLayout, on_delete=models.CASCADE, verbose_name=_("Layout"), null=True 28 | ) 29 | 30 | # As this is an abstract model, the default manager is reset in Django 1.10. 31 | objects = UrlNodeManager() 32 | 33 | class Meta: 34 | abstract = True 35 | verbose_name = _("Page") 36 | verbose_name_plural = _("Pages") 37 | permissions = (("change_page_layout", _("Can change Page layout")),) 38 | 39 | 40 | class FluentPage(AbstractFluentPage): 41 | """ 42 | A ```FluentPage``` represents one HTML page of the site. 43 | """ 44 | 45 | class Meta(AbstractFluentPage.Meta): 46 | pass 47 | -------------------------------------------------------------------------------- /fluent_pages/pagetypes/fluentpage/page_type_plugins.py: -------------------------------------------------------------------------------- 1 | from fluent_pages.extensions import page_type_pool 2 | from fluent_pages.integration.fluent_contents.page_type_plugins import FluentContentsPagePlugin 3 | 4 | from .models import FluentPage 5 | 6 | 7 | @page_type_pool.register 8 | class FluentPagePlugin(FluentContentsPagePlugin): 9 | model = FluentPage 10 | sort_priority = 10 11 | 12 | def get_render_template(self, request, fluentpage, **kwargs): 13 | # Allow subclasses to easily override it by specifying `render_template` after all. 14 | # The default, is to use the template_path from the layout object. 15 | return self.render_template or fluentpage.layout.template_path 16 | -------------------------------------------------------------------------------- /fluent_pages/pagetypes/fluentpage/static/fluent_pages/fluentpage/fluent_layouts.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file deals with the high level layout switching / fetching layout info. 3 | * When a new layout is fetched, it is passed to the fluent_contents module to rebuild the tabs. 4 | */ 5 | var fluent_layouts = { 6 | 'ct_id': null, // for older django-polymorphic support 7 | 'api_url': null 8 | }; 9 | 10 | (function($) 11 | { 12 | var app_root = location.href.indexOf('/fluent_pages/') + 14; 13 | var initial_layout_id = null; 14 | 15 | $.fn.ready( onReady ); 16 | 17 | 18 | 19 | /** 20 | * Initialize this component, 21 | * bind events and select the first option if there is only one. 22 | */ 23 | function onReady() 24 | { 25 | var layout_selector = $("#id_layout"); 26 | if(layout_selector.length == 0) // readonly field. 27 | return; 28 | fluent_layouts._select_single_option( layout_selector ); 29 | layout_selector.change( fluent_layouts.onLayoutChange ); 30 | fluent_contents.layout.onInitialize( fluent_layouts.fetch_layout_on_refresh ); 31 | } 32 | 33 | 34 | fluent_layouts.fetch_layout_on_refresh = function() 35 | { 36 | var layout_selector = $("#id_layout"); 37 | 38 | // Firefox will restore form values at refresh. 39 | // In case this happens, fetch the newly selected layout 40 | var selected_layout_id = layout_selector.val() || 0; 41 | initial_layout_id = layout_selector.attr('data-original-value'); 42 | if( selected_layout_id != initial_layout_id ) 43 | { 44 | fluent_contents.tabs.hide(); 45 | layout_selector.change(); 46 | return true; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | 53 | /** 54 | * If a selectbox has only one choice, enable it. 55 | */ 56 | fluent_layouts._select_single_option = function(selectbox) 57 | { 58 | var options = selectbox[0].options; 59 | if( ( options.length == 1 ) 60 | || ( options.length == 2 && options[0].value == "" ) ) 61 | { 62 | selectbox.val( options[ options.length - 1 ].value ); 63 | } 64 | } 65 | 66 | 67 | /** 68 | * The layout has changed. 69 | */ 70 | fluent_layouts.onLayoutChange = function(event) 71 | { 72 | // TODO: Avoid accessing direct API, have a proper documented external interface. 73 | 74 | var layout_id = this.value; 75 | if( ! layout_id ) 76 | { 77 | fluent_contents.tabs.hide(true); 78 | return; 79 | } 80 | 81 | // Disable content 82 | fluent_contents.layout.expire(); 83 | 84 | if( event.originalEvent ) 85 | { 86 | // Real change event, no manual invocation made above 87 | fluent_contents.tabs.show(true); 88 | } 89 | 90 | fluent_layouts.fetch_layout(layout_id); 91 | } 92 | 93 | 94 | fluent_layouts.fetch_layout = function(layout_id) 95 | { 96 | // Get the ct_id from the template. 97 | // ?ct_id is for django-polymorphic < 2.0 support 98 | var ct_id = parseInt(fluent_layouts.ct_id); 99 | if(isNaN(ct_id)) { 100 | alert("Internal CMS error: missing `fluent_layouts.ct_id` variable in the template!"); 101 | return; 102 | } 103 | 104 | // Get layout info. 105 | $.ajax({ 106 | url: fluent_layouts.api_url.replace('99999', parseInt(layout_id)) + "?ct_id=" + ct_id, 107 | success: function(layout, textStatus, xhr) 108 | { 109 | // Ask to update the tabs! 110 | fluent_contents.layout.load(layout); 111 | }, 112 | dataType: 'json', 113 | error: function(xhr, textStatus, ex) 114 | { 115 | // When the server has DEBUG enabled, show the Django response in the console. 116 | response = xhr.responseText; 117 | if(response && window.console && response.indexOf('DJANGO_SETTINGS_MODULE') != -1) { 118 | console.error(response); 119 | } 120 | 121 | alert("Internal CMS error: failed to fetch layout data!"); // can't yet rely on $.ajaxError 122 | } 123 | }) 124 | } 125 | 126 | })(window.jQuery || django.jQuery); 127 | -------------------------------------------------------------------------------- /fluent_pages/pagetypes/fluentpage/templates/admin/fluentpage/change_form.html: -------------------------------------------------------------------------------- 1 | {% extends base_change_form_template %} 2 | 3 | {% block extrahead %}{{ block.super }} 4 | 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /fluent_pages/pagetypes/fluentpage/tests.py: -------------------------------------------------------------------------------- 1 | from fluent_pages.models import UrlNodeManager, UrlNodeQuerySet 2 | from fluent_pages.pagetypes.fluentpage.models import FluentPage 3 | from fluent_pages.tests.utils import AppTestCase 4 | 5 | 6 | class FluentPageTests(AppTestCase): 7 | def test_default_manager(self): 8 | """ 9 | Test that the default manager is correct. 10 | """ 11 | self.assertIsInstance(FluentPage._default_manager, UrlNodeManager) 12 | self.assertIsInstance(FluentPage.objects.all(), UrlNodeQuerySet) 13 | -------------------------------------------------------------------------------- /fluent_pages/pagetypes/fluentpage/widgets.py: -------------------------------------------------------------------------------- 1 | from django.forms.widgets import Select 2 | 3 | 4 | class LayoutSelector(Select): 5 | def render( 6 | self, name, value, attrs=None, choices=() 7 | ): # Django 1.10: choices is no longer used. 8 | attrs["data-original-value"] = value 9 | return super().render(name, value, attrs) 10 | -------------------------------------------------------------------------------- /fluent_pages/pagetypes/redirectnode/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-fluent/django-fluent-pages/621d756542f23d1e2f7679bffe6fcb81b82df005/fluent_pages/pagetypes/redirectnode/__init__.py -------------------------------------------------------------------------------- /fluent_pages/pagetypes/redirectnode/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.admin.options import get_ul_class 3 | from django.contrib.admin.widgets import AdminRadioSelect 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | from fluent_pages.admin import PageAdmin 7 | 8 | from .models import RedirectNode 9 | 10 | 11 | @admin.register(RedirectNode) 12 | class RedirectNodeAdmin(PageAdmin): 13 | FIELDSET_REDIRECT = ( 14 | _("Redirect settings"), 15 | {"fields": ("new_url", "redirect_type")}, 16 | ) 17 | 18 | # Exclude in_sitemap 19 | fieldsets = ( 20 | PageAdmin.FIELDSET_GENERAL, 21 | FIELDSET_REDIRECT, 22 | PageAdmin.FIELDSET_MENU, 23 | PageAdmin.FIELDSET_PUBLICATION, 24 | ) 25 | 26 | # Sadly, can't use radio_fields for translatable fields 27 | # radio_fields = {'redirect_type': admin.VERTICAL} 28 | # radio_fields.update(PageAdmin.radio_fields) 29 | 30 | def formfield_for_choice_field(self, db_field, request=None, **kwargs): 31 | """ 32 | Get a form Field for a database Field that has declared choices. 33 | """ 34 | # If the field is named as a radio_field, use a RadioSelect 35 | if db_field.name == "redirect_type": 36 | kwargs["widget"] = AdminRadioSelect(attrs={"class": get_ul_class(admin.VERTICAL)}) 37 | return db_field.formfield(**kwargs) 38 | -------------------------------------------------------------------------------- /fluent_pages/pagetypes/redirectnode/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | import parler.fields 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [("fluent_pages", "0001_initial")] 8 | 9 | operations = [ 10 | migrations.CreateModel( 11 | name="RedirectNode", 12 | fields=[ 13 | ( 14 | "urlnode_ptr", 15 | models.OneToOneField( 16 | parent_link=True, 17 | auto_created=True, 18 | primary_key=True, 19 | serialize=False, 20 | to="fluent_pages.UrlNode", 21 | on_delete=models.CASCADE, 22 | ), 23 | ) 24 | ], 25 | options={ 26 | "db_table": "pagetype_redirectnode_redirectnode", 27 | "verbose_name": "Redirect", 28 | "verbose_name_plural": "Redirects", 29 | }, 30 | bases=("fluent_pages.page",), 31 | ), 32 | migrations.CreateModel( 33 | name="RedirectNodeTranslation", 34 | fields=[ 35 | ( 36 | "id", 37 | models.AutoField( 38 | verbose_name="ID", 39 | serialize=False, 40 | auto_created=True, 41 | primary_key=True, 42 | ), 43 | ), 44 | ( 45 | "language_code", 46 | models.CharField(db_index=True, max_length=15, verbose_name="Language"), 47 | ), 48 | ("new_url", models.URLField(max_length=255, verbose_name="New URL")), 49 | ( 50 | "redirect_type", 51 | models.IntegerField( 52 | default=302, 53 | help_text="Use 'normal redirect' unless you want to transfer SEO ranking to the new page.", 54 | verbose_name="Redirect type", 55 | choices=[ 56 | (302, "Normal redirect"), 57 | (301, "Permanent redirect (for SEO ranking)"), 58 | ], 59 | ), 60 | ), 61 | ( 62 | "master", 63 | parler.fields.TranslationsForeignKey( 64 | related_name="redirect_translations", 65 | editable=False, 66 | to="redirectnode.RedirectNode", 67 | on_delete=models.CASCADE, 68 | null=True, 69 | ), 70 | ), 71 | ], 72 | options={ 73 | "db_table": "redirectnode_redirectnode_translation", 74 | "verbose_name": "Redirect Translation", 75 | "default_permissions": (), 76 | "managed": True, 77 | }, 78 | bases=(models.Model,), 79 | ), 80 | migrations.AlterUniqueTogether( 81 | name="redirectnodetranslation", 82 | unique_together={("language_code", "master")}, 83 | ), 84 | ] 85 | -------------------------------------------------------------------------------- /fluent_pages/pagetypes/redirectnode/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-fluent/django-fluent-pages/621d756542f23d1e2f7679bffe6fcb81b82df005/fluent_pages/pagetypes/redirectnode/migrations/__init__.py -------------------------------------------------------------------------------- /fluent_pages/pagetypes/redirectnode/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.translation import gettext_lazy as _ 3 | from fluent_utils.softdeps.any_urlfield import AnyUrlField 4 | from parler.models import TranslatedFields 5 | 6 | from fluent_pages.models import Page 7 | 8 | 9 | class RedirectNode(Page): 10 | """ 11 | A redirect node 12 | """ 13 | 14 | REDIRECT_TYPE_CHOICES = ( 15 | (302, _("Normal redirect")), 16 | (301, _("Permanent redirect (for SEO ranking)")), 17 | # Currently using status codes, however, it is perfectly possible 18 | # to add Refresh-header redirects later, with a temporary message in between. 19 | ) 20 | 21 | # Note that the UrlField can support internal links too when django-any-urlfield is installed. 22 | redirect_translations = TranslatedFields( 23 | new_url=AnyUrlField(_("New URL"), max_length=255), 24 | redirect_type=models.IntegerField( 25 | _("Redirect type"), 26 | choices=REDIRECT_TYPE_CHOICES, 27 | default=302, 28 | help_text=_( 29 | "Use 'normal redirect' unless you want to transfer SEO ranking to the new page." 30 | ), 31 | ), 32 | ) 33 | 34 | class Meta: 35 | # If this page class didn't exist as real model before, 36 | # it would be very tempting to turn it into a proxy: 37 | # proxy = True 38 | verbose_name = _("Redirect") 39 | verbose_name_plural = _("Redirects") 40 | 41 | # While it's very tempting to overwrite get_absolute_url() or 'url' with the new URL, 42 | # the consequences for caching are probably too big to cope with. Just redirect instead. 43 | -------------------------------------------------------------------------------- /fluent_pages/pagetypes/redirectnode/page_type_plugins.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponseRedirect 2 | from django.utils.encoding import force_str 3 | 4 | from fluent_pages.extensions import PageTypePlugin, page_type_pool 5 | from fluent_pages.pagetypes.redirectnode.models import RedirectNode 6 | 7 | 8 | @page_type_pool.register 9 | class RedirectNodePlugin(PageTypePlugin): 10 | model = RedirectNode 11 | default_in_sitemaps = False 12 | 13 | def get_response(self, request, redirectnode, **kwargs): 14 | response = HttpResponseRedirect(force_str(redirectnode.new_url)) 15 | response.status_code = redirectnode.redirect_type 16 | return response 17 | -------------------------------------------------------------------------------- /fluent_pages/pagetypes/redirectnode/tests.py: -------------------------------------------------------------------------------- 1 | from any_urlfield.models import AnyUrlValue 2 | 3 | from fluent_pages.models import UrlNodeManager, UrlNodeQuerySet 4 | from fluent_pages.pagetypes.redirectnode.models import RedirectNode 5 | from fluent_pages.tests.testapp.models import SimpleTextPage 6 | from fluent_pages.tests.utils import AppTestCase 7 | 8 | 9 | class RedirectNodeTests(AppTestCase): 10 | def test_default_manager(self): 11 | """ 12 | Test that the default manager is correct. 13 | """ 14 | self.assertIsInstance(RedirectNode._default_manager, UrlNodeManager) 15 | self.assertIsInstance(RedirectNode.objects.all(), UrlNodeQuerySet) 16 | 17 | def test_resolve_anyurlfield(self): 18 | root = SimpleTextPage.objects.create( 19 | title="Home", 20 | slug="home", 21 | status=SimpleTextPage.PUBLISHED, 22 | author=self.user, 23 | override_url="/", 24 | ) 25 | RedirectNode.objects.create( 26 | title="Redirect", 27 | status=RedirectNode.PUBLISHED, 28 | author=self.user, 29 | parent=root, 30 | slug="redirect", 31 | new_url=AnyUrlValue("fluent_pages.urlnode", root.pk), 32 | ) 33 | response = self.client.get("/redirect/") 34 | self.assertRedirects(response, "/") 35 | -------------------------------------------------------------------------------- /fluent_pages/pagetypes/textfile/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-fluent/django-fluent-pages/621d756542f23d1e2f7679bffe6fcb81b82df005/fluent_pages/pagetypes/textfile/__init__.py -------------------------------------------------------------------------------- /fluent_pages/pagetypes/textfile/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations, models 2 | 3 | 4 | class Migration(migrations.Migration): 5 | 6 | dependencies = [("fluent_pages", "0001_initial")] 7 | 8 | operations = [ 9 | migrations.CreateModel( 10 | name="TextFile", 11 | fields=[ 12 | ( 13 | "urlnode_ptr", 14 | models.OneToOneField( 15 | parent_link=True, 16 | auto_created=True, 17 | primary_key=True, 18 | serialize=False, 19 | to="fluent_pages.UrlNode", 20 | on_delete=models.CASCADE, 21 | ), 22 | ), 23 | ("content", models.TextField(verbose_name="File contents")), 24 | ( 25 | "content_type", 26 | models.CharField( 27 | default="text/plain", 28 | max_length=100, 29 | verbose_name="File type", 30 | choices=[ 31 | ("text/plain", "Plain text"), 32 | ("text/xml", "XML"), 33 | ("text/html", "HTML"), 34 | ], 35 | ), 36 | ), 37 | ], 38 | options={ 39 | "db_table": "pagetype_textfile_textfile", 40 | "verbose_name": "Plain text file", 41 | "verbose_name_plural": "Plain text files", 42 | }, 43 | bases=("fluent_pages.page",), 44 | ) 45 | ] 46 | -------------------------------------------------------------------------------- /fluent_pages/pagetypes/textfile/migrations/0002_add_translation_model.py: -------------------------------------------------------------------------------- 1 | import parler.fields 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [("textfile", "0001_initial")] 8 | 9 | operations = [ 10 | migrations.CreateModel( 11 | name="TextFileTranslation", 12 | fields=[ 13 | ( 14 | "id", 15 | models.AutoField( 16 | verbose_name="ID", 17 | serialize=False, 18 | auto_created=True, 19 | primary_key=True, 20 | ), 21 | ), 22 | ( 23 | "language_code", 24 | models.CharField(max_length=15, verbose_name="Language", db_index=True), 25 | ), 26 | ("content", models.TextField(verbose_name="File contents")), 27 | ( 28 | "master", 29 | parler.fields.TranslationsForeignKey( 30 | related_name="text_translations", 31 | editable=False, 32 | to="textfile.TextFile", 33 | on_delete=models.CASCADE, 34 | null=True, 35 | ), 36 | ), 37 | ], 38 | options={ 39 | "managed": True, 40 | "db_table": "textfile_textfile_translation", 41 | "db_tablespace": "", 42 | "default_permissions": (), 43 | "verbose_name": "Plain text file Translation", 44 | }, 45 | bases=(models.Model,), 46 | ), 47 | migrations.AlterUniqueTogether( 48 | name="textfiletranslation", 49 | unique_together={("language_code", "master")}, 50 | ), 51 | ] 52 | -------------------------------------------------------------------------------- /fluent_pages/pagetypes/textfile/migrations/0003_migrate_translatable_fields.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.exceptions import ObjectDoesNotExist 3 | from django.db import migrations, models 4 | 5 | from fluent_pages import appsettings 6 | 7 | 8 | def forwards_func(apps, schema_editor): 9 | UrlNode_Translation = apps.get_model("fluent_pages", "UrlNode_Translation") 10 | TextFileTranslation = apps.get_model("textfile", "TextFileTranslation") 11 | default_choices = ("en", "en-us", appsettings.FLUENT_PAGES_DEFAULT_LANGUAGE_CODE) 12 | 13 | for textfile in apps.get_model("textfile", "TextFile").objects.all(): 14 | available_languages = list( 15 | UrlNode_Translation.objects.filter(master_id=textfile.id).values_list( 16 | "language_code", flat=True 17 | ) 18 | ) 19 | 20 | # Find the first language that is usable. 21 | # Move the fields to the translation of that language. 22 | lang = next( 23 | (code for code in default_choices if code in available_languages), 24 | available_languages[0], 25 | ) 26 | 27 | TextFileTranslation.objects.create( 28 | master_id=textfile.pk, language_code=lang, content=textfile.content 29 | ) 30 | 31 | 32 | def backwards_func(apps, schema_editor): 33 | TextFileTranslation = apps.get_model("textfile", "TextFileTranslation") 34 | 35 | # Convert all fields back to the single-language table. 36 | for textfile in apps.get_model("textfile", "TextFile").objects.all(): 37 | translations = TextFileTranslation.objects.filter(master_id=textfile.id) 38 | try: 39 | # Try default translation 40 | translation = translations.get( 41 | language_code=appsettings.FLUENT_PAGES_DEFAULT_LANGUAGE_CODE 42 | ) 43 | except ObjectDoesNotExist: 44 | try: 45 | # Try internal fallback 46 | translation = translations.get(language_code__in=("en-us", "en")) 47 | except ObjectDoesNotExist: 48 | # Hope there is a single translation 49 | translation = translations.get() 50 | 51 | textfile.content = translation.content 52 | textfile.save() # As intended: doesn't call UrlNode.save() but Model.save() only. 53 | 54 | 55 | class Migration(migrations.Migration): 56 | 57 | dependencies = [("textfile", "0002_add_translation_model")] 58 | 59 | operations = [migrations.RunPython(forwards_func, backwards_func)] 60 | -------------------------------------------------------------------------------- /fluent_pages/pagetypes/textfile/migrations/0004_remove_untranslated_fields.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations, models 2 | 3 | 4 | class Migration(migrations.Migration): 5 | 6 | dependencies = [("textfile", "0003_migrate_translatable_fields")] 7 | 8 | operations = [migrations.RemoveField(model_name="textfile", name="content")] 9 | -------------------------------------------------------------------------------- /fluent_pages/pagetypes/textfile/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-fluent/django-fluent-pages/621d756542f23d1e2f7679bffe6fcb81b82df005/fluent_pages/pagetypes/textfile/migrations/__init__.py -------------------------------------------------------------------------------- /fluent_pages/pagetypes/textfile/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.translation import gettext_lazy as _ 3 | from parler.models import TranslatedFields 4 | 5 | from fluent_pages.models import Page 6 | 7 | 8 | class TextFile(Page): 9 | """ 10 | A plain text node. 11 | """ 12 | 13 | CONTENT_TYPE_CHOICES = ( 14 | ("text/plain", _("Plain text")), 15 | ("text/xml", _("XML")), 16 | ("text/html", _("HTML")), 17 | ) 18 | UTF8_TYPES = ("text/html", "text/xml") 19 | 20 | content_type = models.CharField( 21 | _("File type"), 22 | max_length=100, 23 | default="text/plain", 24 | choices=CONTENT_TYPE_CHOICES, 25 | ) 26 | 27 | text_translations = TranslatedFields(content=models.TextField(_("File contents"))) 28 | 29 | class Meta: 30 | verbose_name = _("Plain text file") 31 | verbose_name_plural = _("Plain text files") 32 | -------------------------------------------------------------------------------- /fluent_pages/pagetypes/textfile/page_type_plugins.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | 3 | from fluent_pages.extensions import PageTypePlugin, page_type_pool 4 | 5 | from .models import TextFile 6 | 7 | 8 | @page_type_pool.register 9 | class TextFilePlugin(PageTypePlugin): 10 | model = TextFile 11 | is_file = True 12 | default_in_sitemaps = False 13 | 14 | def get_response(self, request, textfile, **kwargs): 15 | content_type = textfile.content_type 16 | if content_type in TextFile.UTF8_TYPES: 17 | content_type += "; charset=utf-8" # going to enforce this. 18 | 19 | return HttpResponse(content=textfile.content, content_type=content_type) 20 | -------------------------------------------------------------------------------- /fluent_pages/pagetypes/textfile/tests.py: -------------------------------------------------------------------------------- 1 | from fluent_pages.models import UrlNodeManager, UrlNodeQuerySet 2 | from fluent_pages.pagetypes.textfile.models import TextFile 3 | from fluent_pages.tests.utils import AppTestCase 4 | 5 | 6 | class TextFileTests(AppTestCase): 7 | def test_default_manager(self): 8 | """ 9 | Test that the default manager is correct. 10 | """ 11 | self.assertIsInstance(TextFile._default_manager, UrlNodeManager) 12 | self.assertIsInstance(TextFile.objects.all(), UrlNodeQuerySet) 13 | -------------------------------------------------------------------------------- /fluent_pages/sitemaps.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides integration for the :mod:`django.contrib.sitemaps ` module. 3 | This can be done using: 4 | 5 | .. code-block:: python 6 | 7 | from fluent_pages.sitemaps import PageSitemap 8 | 9 | sitemaps = { 10 | 'pages': PageSitemap, 11 | } 12 | 13 | urlpatterns += [ 14 | url(r'^sitemap.xml$', 'django.contrib.sitemaps.views.sitemap', {'sitemaps': sitemaps}), 15 | ] 16 | """ 17 | from django.contrib.sitemaps import Sitemap 18 | 19 | from fluent_pages.models import UrlNode 20 | 21 | 22 | class PageSitemap(Sitemap): 23 | """ 24 | The sitemap definition for the pages created with *django-fluent-pages*. 25 | It follows the API for the :mod:`django.contrib.sitemaps ` module. 26 | """ 27 | 28 | def items(self): 29 | """ 30 | Return all items of the sitemap. 31 | """ 32 | # Note that .active_translations() can't be combined with other filters for translations__.. fields. 33 | return ( 34 | UrlNode.objects.in_sitemaps() 35 | .non_polymorphic() 36 | .active_translations() 37 | .prefetch_related("translations") 38 | .order_by("level", "translations__language_code", "translations___cached_url") 39 | ) 40 | 41 | def lastmod(self, urlnode): 42 | """Return the last modification of the page.""" 43 | return urlnode.last_modified 44 | 45 | def location(self, urlnode): 46 | """Return url of a page.""" 47 | return urlnode.url 48 | -------------------------------------------------------------------------------- /fluent_pages/static/fluent_pages/admin/pagetree.css: -------------------------------------------------------------------------------- 1 | /* column layout */ 2 | 3 | .jqtree-django .col-status_column { width: 80px; } 4 | .jqtree-django .col-modification_date { width: 220px; } 5 | .jqtree-django .col-actions_column { width: 120px; } 6 | .jqtree-django .col-language_column { width: 100px; } 7 | 8 | 9 | /* language buttons */ 10 | 11 | thead th.col-language_column { 12 | border-left: 0; 13 | } 14 | 15 | thead th.col-language_column div { 16 | display: none; 17 | } 18 | 19 | .jqtree-django div.col.col-language_column { 20 | border-left: 0; 21 | } 22 | 23 | .available-languages { 24 | font-weight: normal; 25 | float: right; 26 | padding-right: 5px; 27 | font-size: 9px; 28 | color: #666; 29 | } 30 | 31 | -------------------------------------------------------------------------------- /fluent_pages/templates/admin/fluent_pages/integration/fluent_contents/base_change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/fluent_pages/page/base_change_form.html" %} 2 | {% comment %} 3 | 4 | This is the base template that all pagetypes will use 5 | which like to combine django-fluent-pages with django-fluent-contents. 6 | It's defined in FluentContentsPageAdmin 7 | 8 | {% endcomment %} 9 | {% load i18n %} 10 | 11 | {% block after_first_fieldset %} 12 | {# the inlines for django-fluent-contents, to show the PlaceholderEditor first #} 13 |
14 | {% for inline_admin_formset in inline_admin_formsets %} 15 | {% if inline_admin_formset.opts.is_fluent_editor_inline %} 16 | {% include inline_admin_formset.opts.template %} 17 | {% endif %} 18 | {% endfor %} 19 |
20 | {% endblock %} 21 | 22 | {% block inline_field_sets %} 23 | {# remaining inlines added by pagetypes #} 24 |
25 | {% for inline_admin_formset in inline_admin_formsets %} 26 | {% if not inline_admin_formset.opts.is_fluent_editor_inline %} 27 | {% include inline_admin_formset.opts.template %} 28 | {% endif %} 29 | {% endfor %} 30 |
31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /fluent_pages/templates/admin/fluent_pages/page/base_change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/polymorphic_tree/change_form.html" %} 2 | {% load i18n admin_urls admin_modify %} 3 | 4 | {% comment %} 5 | Override the content block, so all templates will have the same set of blocks. 6 | 7 | This adds: 8 | * after_first_fieldset 9 | * the check of "original.is_published" in the toolbar 10 | {% endcomment %} 11 | {% block content %}
12 | {% block object-tools %} 13 | {% if change %}{% if not is_popup %} 14 |
    15 | {% block object-tools-items %} 16 |
  • 17 | {% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %} 18 | {% trans "History" %} 19 |
  • 20 | {% if original.url %}
  • {% trans "View on site" %}
  • {% endif %} 21 | {% endblock %} 22 |
23 | {% endif %}{% endif %} 24 | {% if language_tabs %}{% include "admin/parler/language_tabs.html" %}{% endif %}{# manually included, this template overrides parler defaults #} 25 | {% endblock %} 26 |
{% csrf_token %}{% block form_top %}{% endblock %} 27 |
28 | {% if is_popup %}{% endif %} 29 | {% if to_field %}{% endif %} 30 | {% if save_on_top %}{% block submit_buttons_top %}{% submit_row %}{% endblock %}{% endif %} 31 | {% if errors %} 32 |

33 | {% if errors|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %} 34 |

35 | {{ adminform.form.non_field_errors }} 36 | {% endif %} 37 | 38 | {% block field_sets %} 39 | {# first fieldset #} 40 | {% for fieldset in adminform %} 41 | {% if forloop.first %}{% include "admin/includes/fieldset.html" %}{% endif %} 42 | {% endfor %} 43 | 44 | {% block after_first_fieldset %}{% endblock %} 45 | 46 | {# all remaining fieldsets #} 47 | {% for fieldset in adminform %} 48 | {% if not forloop.first %}{% include "admin/includes/fieldset.html" %}{% endif %} 49 | {% endfor %} 50 | {% endblock %} 51 | 52 | {% block after_field_sets %}{% endblock %} 53 | 54 | {% block inline_field_sets %} 55 | {% for inline_admin_formset in inline_admin_formsets %} 56 | {% include inline_admin_formset.opts.template %} 57 | {% endfor %} 58 | {% endblock %} 59 | 60 | {% block after_related_objects %}{% endblock %} 61 | 62 | {% block submit_buttons_bottom %}{% submit_row %}{% endblock %} 63 | 64 | {% block admin_change_form_document_ready %} 65 | 82 | {% endblock %} 83 | 84 | {# JavaScript for prepopulated fields #} 85 | {% prepopulated_fields_js %} 86 | 87 |
88 |
89 | {% endblock %} 90 | -------------------------------------------------------------------------------- /fluent_pages/templates/admin/fluent_pages/page/change_form.html: -------------------------------------------------------------------------------- 1 | {% extends base_change_form_template %} 2 | {% comment %} 3 | In case no page template was created (which the admin auto-selects by path), 4 | this template will work as catch-all, and choose the proper base template 5 | that the current admin class needs. 6 | {% endcomment %} 7 | -------------------------------------------------------------------------------- /fluent_pages/templates/admin/fluent_pages/page/change_list.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/polymorphic_tree/change_list.html" %} 2 | -------------------------------------------------------------------------------- /fluent_pages/templates/fluent_pages/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% block title %}{% endblock %} - No base template 7 | 8 | 9 | 10 |

No base template is set for django-fluent-pages

11 | 12 |

To configure the base template, either: 13 |

    14 |
  • create a template named: fluent_pages/base.html.
  • 15 |
  • set FLUENT_PAGES_BASE_TEMPLATE to the desired base template file.
  • 16 |
17 |

The template needs to provide the following blocks:

18 | 19 | 20 | 21 |
contentsThe placeholder for the main content.
titleThe contents for the <title> tag.
22 | 23 |
24 |

The page contents is displayed below:

25 | 26 | {% block content %}{% endblock %} 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /fluent_pages/templates/fluent_pages/example-base.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{ page.title }}{% endblock %} 4 | {% block fullheadtitle %}{% if page.meta_title %}{{ page.meta_title }}{% else %}{{ block.super }}{% endif %}{% endblock %} 5 | {% block meta-keywords %}{{ page.meta_keywords }}{% endblock %} 6 | {% block meta-description %}{{ page.meta_description }}{% endblock %} 7 | 8 | {% block extrahead %}{{ block.super }}{% if page.meta_robots %} 9 | 10 | {% endif %}{% endblock %} 11 | 12 | {% comment %} 13 | When integrating django-staff-toolbar, use: 14 | 15 | {% load staff_toolbar_tags %} 16 | 17 | {% block staff_toolbar %} 18 | {% set_staff_object page %} 19 | {{ block.super }} 20 | {% endblock %} 21 | 22 | {% endcomment %} -------------------------------------------------------------------------------- /fluent_pages/templates/fluent_pages/example-cmspage.html: -------------------------------------------------------------------------------- 1 | {% load fluent_pages_tags fluent_contents_tags %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% if page.meta_robots %}{% endif %} 10 | {% block fullheadtitle %}{{ page.title }} | {{ site.title|default:site.domain }}{% endblock %} 11 | 12 | {% block scripts %}{% endblock %} 13 | 14 | 15 | 16 |
17 |
18 | 21 | 22 | 25 | 26 |
27 | 28 | 31 | 32 |

{% block pagetitle %}{{ page.title }}{% endblock %}

33 |
34 | {% block content %}{% page_placeholder "main" %}{% endblock %} 35 |
36 | 37 |
38 | 39 |
40 |
41 | 42 | 43 | -------------------------------------------------------------------------------- /fluent_pages/templates/fluent_pages/intro_page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Welcome to django-fluent-pages 6 | 33 | 34 | 35 | 36 |
37 |

Welcome to Django Fluent!

38 |
39 | 40 |
41 |

The website on {{ site.domain|default:request.get_host }} is empty

42 |

You can start by adding your first page in the admin interface.

43 | 44 | 45 |
46 |

Developer help

47 |

See the documentation to get started:

48 | 52 | 53 |
54 |

Addons:

55 | 62 |
63 |
64 |
65 | 66 |
67 |

68 | You're seeing this message because you have DEBUG = True in your 69 | Django settings file and you haven't published any pages yet. Get to work! 70 |

71 |
72 | 73 | -------------------------------------------------------------------------------- /fluent_pages/templates/fluent_pages/parts/breadcrumb.html: -------------------------------------------------------------------------------- 1 | {% if breadcrumb %} 2 | 7 | {% endif %} -------------------------------------------------------------------------------- /fluent_pages/templates/fluent_pages/parts/menu.html: -------------------------------------------------------------------------------- 1 | {% load mptt_tags %}{% if menu_items %} 2 | 9 | {% else %} 10 | 11 | {% endif %} 12 | -------------------------------------------------------------------------------- /fluent_pages/templates/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org/ 2 | # https://www.google.com/support/webmasters/bin/answer.py?hl=en&answer=156449 3 | 4 | {% for sitemap_url in sitemap_urls %}Sitemap: {{ sitemap_url }} 5 | {% endfor %}{% if ROBOTS_TXT_DISALLOW_ALL %} 6 | User-agent: * 7 | Disallow: / 8 | {% endif %} 9 | -------------------------------------------------------------------------------- /fluent_pages/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-fluent/django-fluent-pages/621d756542f23d1e2f7679bffe6fcb81b82df005/fluent_pages/templatetags/__init__.py -------------------------------------------------------------------------------- /fluent_pages/templatetags/appurl_tags.py: -------------------------------------------------------------------------------- 1 | """ 2 | Template tag to resolve page URLs which have an URLconf attached to them. 3 | Load this module using: 4 | 5 | .. code-block:: html+django 6 | 7 | {% load appurl_tags %} 8 | 9 | Usage: 10 | 11 | .. code-block:: html+django 12 | 13 | {% appurl "my_viewname" %} 14 | 15 | {% appurl "my_viewname" arg1 arg2 %} 16 | 17 | {% appurl "my_viewname" kwarg1=value kwargs2=value as varname %} 18 | 19 | """ 20 | from django.template import Library 21 | from django.utils.encoding import smart_str 22 | from tag_parser.basetags import BaseAssignmentOrOutputNode 23 | 24 | from fluent_pages.models.db import UrlNode 25 | from fluent_pages.urlresolvers import mixed_reverse 26 | 27 | register = Library() 28 | 29 | __all__ = ("AppUrlNode", "appurl") 30 | 31 | 32 | class AppUrlNode(BaseAssignmentOrOutputNode): 33 | min_args = 1 34 | max_args = None 35 | allowed_kwargs = None # Allow kwargs! 36 | 37 | def get_value(self, context, *tag_args, **tag_kwargs): 38 | view_name = tag_args[0] 39 | url_args = tag_args[1::] 40 | url_kwargs = {smart_str(name, "ascii"): value for name, value in tag_kwargs.items()} 41 | 42 | # The app_reverse() tag can handle multiple results fine if it knows what the current page is. 43 | # Try to find it. 44 | request = context.get("request") 45 | page = getattr(request, "_current_fluent_page", None) 46 | if not page: 47 | # There might be a 'page' variable, that was retrieved via `{% get_fluent_page_vars %}`. 48 | # However, django-haystack also uses this variable name, so check whether it's the correct object. 49 | page = context.get("page") 50 | if not isinstance(page, UrlNode): 51 | page = None 52 | 53 | # request.current_app is passed everywhere if available, otherwise it does not exist. 54 | # Somehow it needs to be assigned explicitly in the app 55 | # via: request.current_app = request.resolver_match.namespace 56 | current_app = getattr(request, "current_app", None) 57 | 58 | # Try a normal URLConf URL, then an app URL 59 | return mixed_reverse( 60 | view_name, 61 | args=url_args, 62 | kwargs=url_kwargs, 63 | current_app=current_app, 64 | current_page=page, 65 | ) 66 | 67 | 68 | @register.tag 69 | def appurl(parser, token): 70 | # This tag parser function kept because it's also used as export. 71 | # In docs/newpagetypes/urls.rst, it's also described. 72 | return AppUrlNode.parse(parser, token) 73 | -------------------------------------------------------------------------------- /fluent_pages/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-fluent/django-fluent-pages/621d756542f23d1e2f7679bffe6fcb81b82df005/fluent_pages/tests/__init__.py -------------------------------------------------------------------------------- /fluent_pages/tests/test_admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.contrib.contenttypes.models import ContentType 3 | from django.core.cache import cache 4 | 5 | from fluent_pages.models import Page 6 | from fluent_pages.tests.testapp.models import ( 7 | ChildTypesPage, 8 | PlainTextFile, 9 | SimpleTextPage, 10 | WebShopPage, 11 | ) 12 | from fluent_pages.tests.utils import AppTestCase 13 | 14 | 15 | class AdminTests(AppTestCase): 16 | def setUp(self): 17 | # Need to make sure that django-parler's cache isn't reused, 18 | # because the transaction is rolled back on each test method. 19 | cache.clear() 20 | 21 | # Adding a superuser, to circumvent any permission checks on moving nodes. 22 | User = get_user_model() 23 | self.test_user = User.objects.create_superuser("a", "ab@example.com", "b") 24 | self.test_user.save() 25 | self.client.force_login(self.test_user) 26 | 27 | @classmethod 28 | def setUpTree(cls): 29 | cls.root = SimpleTextPage.objects.create( 30 | title="Home", slug="home", status=SimpleTextPage.PUBLISHED, author=cls.user 31 | ) 32 | cls.root2 = ChildTypesPage.objects.create( 33 | title="Root2", 34 | slug="root2", 35 | status=ChildTypesPage.PUBLISHED, 36 | author=cls.user, 37 | ) 38 | 39 | def test_child_types(self): 40 | """test that all child types are created and correct""" 41 | ids = { 42 | self.root.polymorphic_ctype_id, 43 | self.root2.polymorphic_ctype_id, 44 | ContentType.objects.get_for_model(PlainTextFile).id, 45 | ContentType.objects.get_for_model(WebShopPage).id, 46 | } 47 | childtypes = set(self.root2.get_child_types()) 48 | self.assertEqual(len(ids), len(ids | childtypes)) 49 | 50 | def move_mode(self, expect_status=200): 51 | """Make root a child of root2""" 52 | response = self.client.post( 53 | "/admin/fluent_pages/page/api/node-moved/", 54 | { 55 | "moved_id": self.root.id, 56 | "target_id": self.root2.id, 57 | "position": "inside", 58 | "previous_parent_id": "", 59 | }, 60 | ) 61 | self.assertEqual(response.status_code, expect_status) 62 | 63 | def test_allowed(self): 64 | """ "test the move with no modifications to child types""" 65 | # try the move 66 | self.move_mode() 67 | # refresh objects 68 | self.root = Page.objects.get(pk=self.root.pk) 69 | self.root2 = Page.objects.get(pk=self.root2.pk) 70 | 71 | self.assertTrue(self.root.url.startswith(self.root2.url)) 72 | 73 | def test_not_allowed(self): 74 | """ "test the move after removing child from allowed child types""" 75 | # modify the childtypes cache 76 | page_key = self.root2.page_key 77 | self.root2._PolymorphicMPTTModel__child_types[page_key].remove( 78 | self.root.polymorphic_ctype_id 79 | ) 80 | # try the move 81 | self.move_mode(expect_status=409) 82 | # refresh objects 83 | self.root = Page.objects.get(pk=self.root.pk) 84 | self.root2 = Page.objects.get(pk=self.root2.pk) 85 | 86 | self.assertFalse(self.root.url.startswith(self.root2.url)) 87 | -------------------------------------------------------------------------------- /fluent_pages/tests/test_forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.core.cache import cache 3 | 4 | from fluent_pages.forms.fields import PageChoiceField 5 | from fluent_pages.models.fields import PageTreeForeignKey 6 | from fluent_pages.tests.testapp.models import SimpleTextPage 7 | from fluent_pages.tests.utils import AppTestCase 8 | 9 | 10 | class ModelDataTests(AppTestCase): 11 | """ 12 | Tests for URL resolving. 13 | """ 14 | 15 | def setUp(self): 16 | # Need to make sure that django-parler's cache isn't reused, 17 | # because the transaction is rolled back on each test method. 18 | cache.clear() 19 | 20 | @classmethod 21 | def setUpTree(cls): 22 | cls.root = SimpleTextPage.objects.create( 23 | title="Home", 24 | slug="home", 25 | status=SimpleTextPage.PUBLISHED, 26 | author=cls.user, 27 | override_url="/", 28 | ) 29 | cls.draft1 = SimpleTextPage.objects.create( 30 | title="Draft1", 31 | slug="draft1", 32 | parent=cls.root, 33 | status=SimpleTextPage.DRAFT, 34 | author=cls.user, 35 | ) 36 | 37 | def test_pagechooserfield_success(self): 38 | class TestForm(forms.Form): 39 | page = PageChoiceField() 40 | 41 | form = TestForm(data={"page": str(self.root.pk)}) 42 | 43 | self.assertTrue(form.is_valid(), form.errors) 44 | self.assertEqual(form.cleaned_data["page"].pk, self.root.pk) 45 | 46 | def test_pagechooserfield_invalid(self): 47 | class TestForm(forms.Form): 48 | page = PageChoiceField() 49 | 50 | form = TestForm(data={"page": "99999"}) 51 | 52 | self.assertFalse(form.is_valid()) 53 | self.assertNotIn("not published", str(form.errors["page"][0])) 54 | self.assertIn( 55 | "Select a valid choice. That choice is not one of the available choices.", 56 | str(form.errors["page"][0]), 57 | ) 58 | 59 | def test_pagechooserfield_draft(self): 60 | class TestForm(forms.Form): 61 | page = PageChoiceField() 62 | 63 | form = TestForm(data={"page": str(self.draft1.pk)}) 64 | 65 | self.assertFalse(form.is_valid()) 66 | self.assertIn("not published", str(form.errors["page"][0])) 67 | -------------------------------------------------------------------------------- /fluent_pages/tests/test_templatetags.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from fluent_pages.tests.testapp.models import SimpleTextPage 4 | from fluent_pages.tests.utils import AppTestCase 5 | 6 | 7 | class TemplateTagTests(AppTestCase): 8 | """ 9 | Tests for URL resolving. 10 | """ 11 | 12 | root_url = "/" 13 | subpage1_url = "/test_subpage1/" 14 | 15 | @classmethod 16 | def setUpTree(cls): 17 | root = SimpleTextPage.objects.create( 18 | title="Home", 19 | slug="home", 20 | status=SimpleTextPage.PUBLISHED, 21 | author=cls.user, 22 | override_url="/", 23 | ) 24 | root2 = SimpleTextPage.objects.create( 25 | title="Root2", 26 | slug="root2", 27 | status=SimpleTextPage.PUBLISHED, 28 | author=cls.user, 29 | ) 30 | 31 | level1a = SimpleTextPage.objects.create( 32 | title="Level1a", 33 | slug="level1a", 34 | parent=root, 35 | status=SimpleTextPage.PUBLISHED, 36 | author=cls.user, 37 | ) 38 | level1b = SimpleTextPage.objects.create( 39 | title="Level1b", 40 | slug="level1b", 41 | parent=root, 42 | status=SimpleTextPage.PUBLISHED, 43 | author=cls.user, 44 | ) 45 | 46 | def test_menu_404(self): 47 | response = self.client.get("/404/") 48 | html = response.content.decode("utf-8") 49 | 50 | # Kind of JSON like, but not really (has trailing commma) 51 | menu = html[html.find("menu =") + 7 :] 52 | menu = re.sub(r"\s+", "", menu) 53 | 54 | self.assertEqual( 55 | menu, 56 | """[{'title':"Home','url':"/",'active':false,'children':[""" 57 | """{'title':"Level1a','url':"/level1a/",'active':false},""" 58 | """{'title':"Level1b','url':"/level1b/",'active':false},""" 59 | """]},""" 60 | """{'title':"Root2','url':"/root2/",'active':false},]""", 61 | ) 62 | -------------------------------------------------------------------------------- /fluent_pages/tests/testapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-fluent/django-fluent-pages/621d756542f23d1e2f7679bffe6fcb81b82df005/fluent_pages/tests/testapp/__init__.py -------------------------------------------------------------------------------- /fluent_pages/tests/testapp/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations, models 2 | 3 | 4 | class Migration(migrations.Migration): 5 | 6 | dependencies = [("fluent_pages", "0001_initial")] 7 | 8 | operations = [ 9 | migrations.CreateModel( 10 | name="PlainTextFile", 11 | fields=[ 12 | ( 13 | "urlnode_ptr", 14 | models.OneToOneField( 15 | auto_created=True, 16 | parent_link=True, 17 | serialize=False, 18 | primary_key=True, 19 | to="fluent_pages.UrlNode", 20 | on_delete=models.CASCADE, 21 | ), 22 | ), 23 | ("content", models.TextField(verbose_name="Contents")), 24 | ], 25 | options={ 26 | "db_table": "pagetype_testapp_plaintextfile", 27 | "verbose_name_plural": "Plain text files", 28 | "verbose_name": "Plain text file", 29 | }, 30 | bases=("fluent_pages.page",), 31 | ), 32 | migrations.CreateModel( 33 | name="SimpleTextPage", 34 | fields=[ 35 | ( 36 | "urlnode_ptr", 37 | models.OneToOneField( 38 | auto_created=True, 39 | parent_link=True, 40 | serialize=False, 41 | primary_key=True, 42 | to="fluent_pages.UrlNode", 43 | on_delete=models.CASCADE, 44 | ), 45 | ), 46 | ("contents", models.TextField(verbose_name="Contents")), 47 | ], 48 | options={ 49 | "db_table": "pagetype_testapp_simpletextpage", 50 | "verbose_name_plural": "Plain text pages", 51 | "verbose_name": "Plain text page", 52 | }, 53 | bases=("fluent_pages.htmlpage",), 54 | ), 55 | migrations.CreateModel( 56 | name="ChildTypesPage", 57 | fields=[ 58 | ( 59 | "urlnode_ptr", 60 | models.OneToOneField( 61 | auto_created=True, 62 | parent_link=True, 63 | serialize=False, 64 | primary_key=True, 65 | to="fluent_pages.UrlNode", 66 | on_delete=models.CASCADE, 67 | ), 68 | ), 69 | ("contents", models.TextField(verbose_name="Contents")), 70 | ], 71 | options={ 72 | "db_table": "pagetype_testapp_childtypespage", 73 | "verbose_name_plural": "Plain text pages", 74 | "verbose_name": "Plain text page", 75 | }, 76 | bases=("fluent_pages.htmlpage",), 77 | ), 78 | migrations.CreateModel( 79 | name="WebShopPage", 80 | fields=[ 81 | ( 82 | "urlnode_ptr", 83 | models.OneToOneField( 84 | auto_created=True, 85 | parent_link=True, 86 | serialize=False, 87 | primary_key=True, 88 | to="fluent_pages.UrlNode", 89 | on_delete=models.CASCADE, 90 | ), 91 | ) 92 | ], 93 | options={ 94 | "db_table": "pagetype_testapp_webshoppage", 95 | "verbose_name_plural": "Webshop pages", 96 | "verbose_name": "Webshop page", 97 | }, 98 | bases=("fluent_pages.page",), 99 | ), 100 | ] 101 | -------------------------------------------------------------------------------- /fluent_pages/tests/testapp/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-fluent/django-fluent-pages/621d756542f23d1e2f7679bffe6fcb81b82df005/fluent_pages/tests/testapp/migrations/__init__.py -------------------------------------------------------------------------------- /fluent_pages/tests/testapp/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from fluent_pages.models import HtmlPage, Page 4 | 5 | 6 | class SimpleTextPage(HtmlPage): 7 | contents = models.TextField("Contents") 8 | 9 | class Meta: 10 | verbose_name = "Plain text page" 11 | verbose_name_plural = "Plain text pages" 12 | app_label = "testapp" 13 | 14 | 15 | class ChildTypesPage(HtmlPage): 16 | contents = models.TextField("Contents") 17 | 18 | class Meta: 19 | verbose_name = "Plain text page" 20 | verbose_name_plural = "Plain text pages" 21 | app_label = "testapp" 22 | 23 | 24 | class PlainTextFile(Page): 25 | content = models.TextField("Contents") 26 | 27 | class Meta: 28 | verbose_name = "Plain text file" 29 | verbose_name_plural = "Plain text files" 30 | app_label = "testapp" 31 | 32 | 33 | class WebShopPage(Page): 34 | class Meta: 35 | verbose_name = "Webshop page" 36 | verbose_name_plural = "Webshop pages" 37 | app_label = "testapp" 38 | -------------------------------------------------------------------------------- /fluent_pages/tests/testapp/page_type_plugins.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | 3 | from fluent_pages.extensions import PageTypePlugin, page_type_pool 4 | from fluent_pages.tests.testapp.models import ( 5 | ChildTypesPage, 6 | PlainTextFile, 7 | SimpleTextPage, 8 | WebShopPage, 9 | ) 10 | 11 | 12 | @page_type_pool.register 13 | class SimpleTextPagePlugin(PageTypePlugin): 14 | """ 15 | Place a simple page in the page tree. 16 | """ 17 | 18 | model = SimpleTextPage 19 | render_template = "testapp/simpletextpage.html" 20 | 21 | 22 | @page_type_pool.register 23 | class ChildTypesPagePlugin(PageTypePlugin): 24 | """ 25 | Place a simple page in the page tree. 26 | """ 27 | 28 | model = ChildTypesPage 29 | render_template = "testapp/simpletextpage.html" 30 | 31 | child_types = ["self", "SimpleTextPage", "testapp.PlainTextFile", WebShopPage] 32 | 33 | 34 | @page_type_pool.register 35 | class PlainTextFilePlugin(PageTypePlugin): 36 | """ 37 | Place a simple page in the page tree. 38 | """ 39 | 40 | model = PlainTextFile 41 | is_file = True 42 | 43 | def get_response(self, request, textfile, **kwargs): 44 | return HttpResponse(content=textfile.content, content_type="text/plain") 45 | 46 | 47 | @page_type_pool.register 48 | class WebShopPagePlugin(PageTypePlugin): 49 | """ 50 | Place a "webshop" node in the page tree 51 | """ 52 | 53 | model = WebShopPage 54 | urls = "fluent_pages.tests.testapp.urls_webshop" 55 | -------------------------------------------------------------------------------- /fluent_pages/tests/testapp/templates/404.html: -------------------------------------------------------------------------------- 1 | Got 404 on: {{ request.path }} 2 | 3 | {# should work at 404 pages #} 4 | {% load fluent_pages_tags %} 5 | menu = {% render_menu template="testapp/json_menu.html" %} 6 | -------------------------------------------------------------------------------- /fluent_pages/tests/testapp/templates/testapp/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block headtitle %}{% endblock %} 6 | 7 | 8 | 9 | 10 |
11 |

{% block pagetitle %}{% endblock %}

12 | {% block main %}{% endblock %} 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /fluent_pages/tests/testapp/templates/testapp/json_menu.html: -------------------------------------------------------------------------------- 1 | {% load mptt_tags %}{% if menu_items %} 2 | [ 3 | {% recursetree menu_items %}{# node is a Page #} 4 | {'title': "{{ node.title }}', 'url': "{{ node.url }}", 'active': {{ node.is_active|yesno:"true,false" }} 5 | {% if children %}, 'children': [{{ children }}]{% endif %} 6 | }, 7 | {% endrecursetree %} 8 | ] 9 | {% endif %} -------------------------------------------------------------------------------- /fluent_pages/tests/testapp/templates/testapp/simpletextpage.html: -------------------------------------------------------------------------------- 1 | {% extends "testapp/base.html" %} 2 | 3 | {% block main %} 4 |
{{ page.contents }}
{# test client node: keep div exactly like this. #} 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /fluent_pages/tests/testapp/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.http import Http404 3 | from django.urls import include, path 4 | 5 | import fluent_pages.admin # Register model 6 | import fluent_pages.urls 7 | 8 | 9 | def simulate_404(request): 10 | raise Http404("Test") 11 | 12 | 13 | urlpatterns = [ 14 | path("admin/", admin.site.urls), 15 | path("404/", simulate_404), 16 | path("", include(fluent_pages.urls)), 17 | ] 18 | -------------------------------------------------------------------------------- /fluent_pages/tests/testapp/urls_nonroot.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import include, path 3 | 4 | import fluent_pages.urls 5 | 6 | urlpatterns = [ 7 | path("admin/", admin.site.urls), 8 | path("pages/", include(fluent_pages.urls)), 9 | ] 10 | -------------------------------------------------------------------------------- /fluent_pages/tests/testapp/urls_webshop.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.urls import path 3 | 4 | 5 | def webshop_index(request): 6 | return HttpResponse("test_webshop: index_page") 7 | 8 | 9 | def webshop_article(request, slug): 10 | return HttpResponse("test_webshop: article: " + slug) 11 | 12 | 13 | urlpatterns = [ 14 | path("", webshop_index, name="webshop_index"), 15 | path("/", webshop_article, name="webshop_article"), 16 | ] 17 | -------------------------------------------------------------------------------- /fluent_pages/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | The URLs to serve the CMS. 3 | 4 | They can be included using: 5 | 6 | urlpatterns += [ 7 | url(r'', include('fluent_pages.urls')) 8 | ] 9 | 10 | The following named URLs are defined: 11 | - fluent-page-admin-redirect - An redirect to the admin. 12 | - fluent-page - Display of a page. 13 | 14 | By Appending @admin to an URL, the request will be redirected to the admin URL of the page. 15 | """ 16 | from django.urls import path, re_path 17 | 18 | from fluent_pages.views import CmsPageAdminRedirect, CmsPageDispatcher 19 | 20 | # This urlpatterns acts as a catch-all, as there is no terminating slash in the pattern. 21 | # This allows the pages to have any name, including file names such as /robots.txt 22 | # Sadly, that circumvents the CommonMiddleware check whether a slash needs to be appended to a path. 23 | # The APPEND_SLASH behavior is implemented in the CmsPageDispatcher so the standard behavior still works as expected. 24 | urlpatterns = [ 25 | re_path( 26 | r"^(?P.*)@admin$", 27 | CmsPageAdminRedirect.as_view(), 28 | name="fluent-page-admin-redirect", 29 | ), 30 | re_path(r"^(?P.*)$", CmsPageDispatcher.as_view(), name="fluent-page-url"), 31 | path("", CmsPageDispatcher.as_view(), name="fluent-page"), 32 | ] 33 | -------------------------------------------------------------------------------- /fluent_pages/views/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | All views of the CMS 3 | """ 4 | from .dispatcher import CmsPageAdminRedirect, CmsPageDispatcher 5 | from .mixins import CurrentPageMixin, CurrentPageTemplateMixin 6 | from .seo import RobotsTxtView 7 | 8 | __all__ = ( 9 | "CmsPageDispatcher", 10 | "CmsPageAdminRedirect", 11 | "CurrentPageMixin", 12 | "CurrentPageTemplateMixin", 13 | "RobotsTxtView", 14 | ) 15 | -------------------------------------------------------------------------------- /fluent_pages/views/mixins.py: -------------------------------------------------------------------------------- 1 | """ 2 | Mixins to simplify creating URLpattern views in custom page pages. 3 | """ 4 | from django.conf import settings 5 | from django.core.exceptions import ImproperlyConfigured 6 | from parler.views import ViewUrlMixin 7 | 8 | from fluent_pages.urlresolvers import mixed_reverse 9 | 10 | 11 | class CurrentPageMixin(ViewUrlMixin): 12 | """ 13 | Access the current page. 14 | This can be used for views which are defined as page type views 15 | in :attr:`PageTypePlugin.urls `. 16 | 17 | The template context will have the same variables as the regular page templates would have, 18 | which are: 19 | * ``page`` 20 | * ``site`` 21 | * :ref:`FLUENT_PAGES_BASE_TEMPLATE` 22 | """ 23 | 24 | def get_current_page(self): 25 | """ 26 | Return the current page. 27 | """ 28 | return getattr(self.request, "_current_fluent_page", None) 29 | 30 | def get_context_data(self, **kwargs): 31 | """ 32 | Add the plugin context to the template. 33 | """ 34 | context = super().get_context_data(**kwargs) 35 | page = self.get_current_page() 36 | 37 | # Expose the same context data as PageTypePlugin.get_context() 38 | # so make it work consistently between CMS pages and mounted app pages. 39 | # Delay the 'site' query until the variable is read, and cache it afterwards. 40 | if page is not None: 41 | context.update(page.plugin.get_context(self.request, page)) 42 | 43 | # Improve the integration of django-staff-toolbar, if used. 44 | # However, avoid being too disruptive, in case the view exposes an object themselves. 45 | if "staff_toolbar" in settings.INSTALLED_APPS: 46 | if ( 47 | getattr(self, "object", None) is None 48 | and "object" not in context 49 | and not hasattr(self, "get_staff_object") 50 | and not hasattr(self.request, "staff_object") 51 | ): 52 | self.request.staff_object = page 53 | 54 | return context 55 | 56 | def get_view_url(self): 57 | """ 58 | When using the :attr:`ViewUrlMixin.view_url_name ` feature of *django-parler*, 59 | this makes sure that mounted pages are also supported. 60 | 61 | It uses :func:`fluent_pages.urlresolvers.mixed_reverse` function to resolve the :attr:`view_url_name`. 62 | """ 63 | # This method is used by the ``get_translated_url`` template tag of django-parler 64 | if self.view_url_name: 65 | return mixed_reverse( 66 | self.view_url_name, 67 | args=self.args, 68 | kwargs=self.kwargs, 69 | current_page=self.get_current_page(), 70 | ) 71 | else: 72 | return super().get_view_url() 73 | 74 | 75 | class CurrentPageTemplateMixin(CurrentPageMixin): 76 | """ 77 | Automaticaly reuse the template of the current page for the URL pattern view. 78 | """ 79 | 80 | def get_template_names(self): 81 | """ 82 | Auto-include the template of the CMS page. 83 | """ 84 | page = self.get_current_page() 85 | if page is not None: 86 | extra_template = page.plugin.get_render_template(self.request, page) 87 | else: 88 | extra_template = None 89 | 90 | names = [] 91 | try: 92 | # This call will likely resolve into: 93 | # * TemplateResponseMixin (reads template_name) 94 | # * SingleObjectTemplateResponseMixin (uses model name) 95 | names = super(CurrentPageMixin, self).get_template_names() 96 | except ImproperlyConfigured: 97 | # No problem, if the plugin offered a template name that will be used. 98 | if extra_template: 99 | names.append(extra_template) 100 | else: 101 | # Really need to define `template_name` yourself. 102 | raise 103 | else: 104 | # There are already template choices, 105 | # add the page templates as last choice (if the model-specific page does not exist). 106 | if extra_template: 107 | names.append(extra_template) 108 | 109 | return names 110 | -------------------------------------------------------------------------------- /fluent_pages/views/seo.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.urls import reverse 3 | from django.utils.translation import get_language 4 | from django.views.generic import TemplateView 5 | from parler.utils import is_multilingual_project 6 | 7 | from fluent_pages import appsettings 8 | 9 | 10 | class RobotsTxtView(TemplateView): 11 | """ 12 | Exposing a ``robots.txt`` template in the Django project. 13 | 14 | Add this view to the ``urls.py``: 15 | 16 | .. code-block:: python 17 | 18 | from fluent_pages.views import RobotsTxtView 19 | 20 | urlpatterns = [ 21 | # ... 22 | 23 | url(r'^robots.txt$', RobotsTxtView.as_view()), 24 | ] 25 | 26 | Naturally, this pattern should be placed outside :func:`~django.conf.urls.i18n.i18n_patterns` 27 | as it should appear at the top level. 28 | 29 | A ``robots.txt`` template is included by default, which you have override in your own project. 30 | Possible templates could look like: 31 | 32 | Simple: 33 | 34 | .. code-block:: html+django 35 | 36 | Sitemap: {{ ROOT_URL }}sitemap.xml 37 | {% if ROBOTS_TXT_DISALLOW_ALL %} 38 | User-agent: * 39 | Disallow: / 40 | {% endif %} 41 | 42 | Sitemaps per language for usage with :func:`~django.conf.urls.i18n.i18n_patterns`: 43 | 44 | .. code-block:: html+django 45 | 46 | {% for language in language_codes %}Sitemap: {{ ROOT_URL }}{{ language }}/sitemap.xml 47 | {% endfor %} 48 | 49 | Alternative: 50 | 51 | .. code-block:: html+django 52 | 53 | {% for sitemap_url in sitemap_urls %}Sitemap: {{ sitemap_url }} 54 | {% endfor %} 55 | 56 | """ 57 | 58 | #: The content_type to return. 59 | content_type = "text/plain" 60 | #: The template to render. You can override this template. 61 | template_name = "robots.txt" 62 | 63 | def render_to_response(self, context, **response_kwargs): 64 | response_kwargs[ 65 | "content_type" 66 | ] = self.content_type # standard TemplateView does not offer this! 67 | return super().render_to_response(context, **response_kwargs) 68 | 69 | def get_context_data(self, **kwargs): 70 | context = super().get_context_data() 71 | is_multilingual = is_multilingual_project() 72 | 73 | context["ROBOTS_TXT_DISALLOW_ALL"] = appsettings.ROBOTS_TXT_DISALLOW_ALL 74 | context["ROOT_URL"] = root_url = self.request.build_absolute_uri("/") 75 | context["is_multilingual_project"] = is_multilingual 76 | context["language_codes"] = self.get_i18n_patterns_codes() 77 | context["sitemap_urls"] = self.get_sitemap_urls(root_url) 78 | return context 79 | 80 | def get_sitemap_urls(self, root_url): 81 | """ 82 | Return all possible sitemap URLs, which the template can use. 83 | """ 84 | if self.has_i18n_patterns_urls(): 85 | language_codes = self.get_i18n_patterns_codes() 86 | return [f"{root_url}{language_code}/sitemap.xml" for language_code in language_codes] 87 | else: 88 | return [f"{root_url}sitemap.xml"] 89 | 90 | def has_i18n_patterns_urls(self): 91 | """ 92 | Check whether something like :func:`~django.conf.urls.i18n.i18n_patterns` is used. 93 | """ 94 | return f"/{get_language()}/" in reverse("fluent-page") 95 | 96 | def get_i18n_patterns_codes(self): 97 | """ 98 | Return the possible values that :func:`~django.conf.urls.i18n.i18n_patterns` support. 99 | """ 100 | return [code for code, title in settings.LANGUAGES] 101 | -------------------------------------------------------------------------------- /makemessages.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | from os import path 4 | 5 | import django 6 | from django.conf import settings 7 | from django.core.management import call_command 8 | 9 | 10 | def main(): 11 | if not settings.configured: 12 | module_root = path.dirname(path.realpath(__file__)) 13 | 14 | settings.configure( 15 | DEBUG=False, 16 | INSTALLED_APPS=( 17 | "django.contrib.auth", 18 | "django.contrib.contenttypes", 19 | "django.contrib.sites", 20 | "fluent_pages", 21 | "mptt", 22 | ), 23 | SITE_ID=1, 24 | FLUENT_PAGES_TEMPLATE_DIR=path.join( 25 | module_root, "fluent_pages", "tests", "testapp", "templates" 26 | ), 27 | ) 28 | 29 | django.setup() 30 | makemessages() 31 | 32 | 33 | def makemessages(): 34 | os.chdir("fluent_pages") 35 | call_command("makemessages", locale=("en", "nl"), verbosity=1) 36 | 37 | 38 | if __name__ == "__main__": 39 | main() 40 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel", "Django"] 3 | 4 | [tool.isort] 5 | profile = "black" 6 | line_length = 99 7 | 8 | [tool.black] 9 | line-length = 99 10 | exclude = ''' 11 | /( 12 | \.git 13 | | \.tox 14 | | \.venv 15 | | dist 16 | )/ 17 | ''' 18 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python -Wd 2 | import sys 3 | import warnings 4 | from os import path 5 | 6 | import django 7 | from django.conf import global_settings as default_settings 8 | from django.conf import settings 9 | from django.core.management import execute_from_command_line 10 | 11 | # python -Wd, or run via coverage: 12 | warnings.simplefilter("always", DeprecationWarning) 13 | 14 | # Give feedback on used versions 15 | sys.stderr.write(f"Using Python version {sys.version[:5]} from {sys.executable}\n") 16 | sys.stderr.write( 17 | "Using Django version {} from {}\n".format( 18 | django.get_version(), path.dirname(path.abspath(django.__file__)) 19 | ) 20 | ) 21 | 22 | if not settings.configured: 23 | module_root = path.dirname(path.realpath(__file__)) 24 | 25 | settings.configure( 26 | DATABASES={ 27 | "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}, 28 | }, 29 | INSTALLED_APPS=( 30 | "django.contrib.auth", 31 | "django.contrib.contenttypes", 32 | "django.contrib.messages", 33 | "django.contrib.sessions", 34 | "django.contrib.sites", 35 | "django.contrib.admin", 36 | "fluent_pages", 37 | "fluent_pages.pagetypes.flatpage", 38 | "fluent_pages.pagetypes.fluentpage", 39 | "fluent_pages.pagetypes.redirectnode", 40 | "fluent_pages.pagetypes.textfile", 41 | "fluent_pages.tests.testapp", 42 | "mptt", 43 | "parler", 44 | "polymorphic", 45 | "polymorphic_tree", 46 | "fluent_contents", 47 | "django_wysiwyg", 48 | "any_urlfield", 49 | ), 50 | MIDDLEWARE=( 51 | "django.middleware.common.CommonMiddleware", 52 | "django.contrib.sessions.middleware.SessionMiddleware", 53 | "django.middleware.csrf.CsrfViewMiddleware", 54 | "django.contrib.auth.middleware.AuthenticationMiddleware", 55 | "django.contrib.messages.middleware.MessageMiddleware", 56 | ), 57 | TEMPLATES=[ 58 | { 59 | "BACKEND": "django.template.backends.django.DjangoTemplates", 60 | "DIRS": (), 61 | "OPTIONS": { 62 | "loaders": ( 63 | "django.template.loaders.filesystem.Loader", 64 | "django.template.loaders.app_directories.Loader", 65 | ), 66 | "context_processors": ( 67 | "django.template.context_processors.debug", 68 | "django.template.context_processors.i18n", 69 | "django.template.context_processors.media", 70 | "django.template.context_processors.request", 71 | "django.template.context_processors.static", 72 | "django.contrib.messages.context_processors.messages", 73 | "django.contrib.auth.context_processors.auth", 74 | ), 75 | }, 76 | } 77 | ], 78 | TEST_RUNNER="django.test.runner.DiscoverRunner", 79 | DEFAULT_AUTO_FIELD="django.db.models.AutoField", 80 | SITE_ID=4, 81 | SECRET_KEY="testtest", 82 | PARLER_LANGUAGES={4: ({"code": "nl", "fallback": "en"}, {"code": "en"})}, 83 | PARLER_DEFAULT_LANGUAGE_CODE="en", # Having a good fallback causes more code to run, more error checking. 84 | ROOT_URLCONF="fluent_pages.tests.testapp.urls", 85 | FLUENT_PAGES_TEMPLATE_DIR=path.join( 86 | module_root, "fluent_pages", "tests", "testapp", "templates" 87 | ), 88 | ) 89 | 90 | DEFAULT_TEST_APPS = ["fluent_pages"] 91 | 92 | 93 | def runtests(): 94 | other_args = list(filter(lambda arg: arg.startswith("-"), sys.argv[1:])) 95 | test_apps = ( 96 | list(filter(lambda arg: not arg.startswith("-"), sys.argv[1:])) or DEFAULT_TEST_APPS 97 | ) 98 | argv = sys.argv[:1] + ["test", "--traceback"] + other_args + test_apps 99 | execute_from_command_line(argv) 100 | 101 | 102 | if __name__ == "__main__": 103 | runtests() 104 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import codecs 3 | import os 4 | import re 5 | import sys 6 | from os import path 7 | 8 | from setuptools import find_packages, setup 9 | 10 | # When creating the sdist, make sure the django.mo file also exists: 11 | if "sdist" in sys.argv or "develop" in sys.argv: 12 | os.chdir("fluent_pages") 13 | try: 14 | from django.core import management 15 | 16 | management.call_command("compilemessages", stdout=sys.stderr, verbosity=1) 17 | except ImportError: 18 | if "sdist" in sys.argv: 19 | raise 20 | finally: 21 | os.chdir("..") 22 | 23 | 24 | def read(*parts): 25 | file_path = path.join(path.dirname(__file__), *parts) 26 | return codecs.open(file_path, encoding="utf-8").read() 27 | 28 | 29 | def find_version(*parts): 30 | version_file = read(*parts) 31 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) 32 | if version_match: 33 | return str(version_match.group(1)) 34 | raise RuntimeError("Unable to find version string.") 35 | 36 | 37 | setup( 38 | name="django-fluent-pages", 39 | version=find_version("fluent_pages", "__init__.py"), 40 | license="Apache 2.0", 41 | install_requires=[ 42 | "django-fluent-utils>=2.0.1", # DRY utility code 43 | "django-mptt>=0.10.0", 44 | "django-parler>=2.0.1", 45 | "django-polymorphic>=2.1.2", 46 | "django-polymorphic-tree>=1.5.1", 47 | "django-slug-preview>=1.0.4", 48 | "django-tag-parser>=3.2", 49 | "Pillow", # needed by ImageField 50 | ], 51 | requires=["Django (>=2.2)"], 52 | extras_require={ 53 | "tests": [ 54 | "django-any-urlfield>=2.6.2", 55 | "django-wysiwyg>=0.7.1", 56 | "django-fluent-contents>=2.0.7", 57 | ], 58 | "flatpage": ["django-wysiwyg>=0.7.1"], 59 | "fluentpage": ["django-fluent-contents>=2.0.7"], 60 | "redirectnode": [ 61 | "django-any-urlfield>=2.6.2" 62 | ], # Needs Pickle support for translated new_url field. 63 | }, 64 | description="A flexible, scalable CMS with custom node types, and flexible block content.", 65 | long_description=read("README.rst"), 66 | author="Diederik van der Boor", 67 | author_email="opensource@edoburu.nl", 68 | url="https://github.com/edoburu/django-fluent-pages", 69 | download_url="https://github.com/edoburu/django-fluent-pages/zipball/master", 70 | packages=find_packages(exclude=("example*",)), 71 | include_package_data=True, 72 | test_suite="runtests", 73 | zip_safe=False, 74 | classifiers=[ 75 | "Development Status :: 5 - Production/Stable", 76 | "Environment :: Web Environment", 77 | "Intended Audience :: Developers", 78 | "License :: OSI Approved :: Apache Software License", 79 | "Operating System :: OS Independent", 80 | "Programming Language :: Python", 81 | "Programming Language :: Python :: 3.6", 82 | "Programming Language :: Python :: 3.7", 83 | "Programming Language :: Python :: 3.8", 84 | "Programming Language :: Python :: 3.9", 85 | "Framework :: Django", 86 | "Framework :: Django :: 2.2", 87 | "Framework :: Django :: 3.1", 88 | "Framework :: Django :: 3.2", 89 | "Framework :: Django :: 4.0", 90 | "Framework :: Django :: 4.1", 91 | "Framework :: Django :: 4.2", 92 | "Topic :: Internet :: WWW/HTTP", 93 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 94 | "Topic :: Software Development :: Libraries :: Application Frameworks", 95 | "Topic :: Software Development :: Libraries :: Python Modules", 96 | ], 97 | ) 98 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist= 3 | py38-django{22,31,32}, 4 | py310-django{42}, 5 | docs 6 | 7 | [testenv] 8 | deps = 9 | django22: Django ~= 2.2 10 | django31: Django ~= 3.1 11 | django32: Django ~= 3.2 12 | django42: Django ~= 4.2 13 | django-dev: https://github.com/django/django/tarball/main 14 | django-any-urlfield >= 2.6.1 15 | django-wysiwyg >= 0.7.1 16 | django-fluent-contents >= 2.0.5 17 | commands= 18 | python --version 19 | python runtests.py 20 | 21 | [testenv:docs] 22 | changedir = docs 23 | deps = 24 | Sphinx 25 | -r{toxinidir}/docs/_ext/djangodummy/requirements.txt 26 | commands = sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html 27 | --------------------------------------------------------------------------------