├── .codeclimate.yml ├── .coveragerc ├── .github └── workflows │ ├── codeql-analysis.yml │ └── django.yml ├── .gitignore ├── .pep8 ├── .tx └── config ├── AUTHORS ├── CHANGELOG ├── CONTRIBUTING ├── Dockerfile ├── FAQ ├── INSTALL ├── LICENSE ├── MANIFEST.in ├── README ├── README.rst ├── THANKS ├── TODO ├── doc ├── 3rd-party-apps.rst ├── Makefile ├── changelog.rst ├── commands.rst ├── conf.py ├── contributions.rst ├── create-blog-application.rst ├── display-content.rst ├── edit-content.rst ├── gerbi.png ├── images │ ├── admin-screenshot-1.png │ ├── admin-screenshot-2.png │ ├── ckeditor_placeholder.png │ ├── inline-edit.png │ ├── rte-light.png │ └── section.png ├── index.rst ├── installation.rst ├── introduction.rst ├── navigation-template-tags.rst ├── page-api.rst ├── placeholders.rst ├── settings-list.rst └── static │ └── main.css ├── docker-compose.yaml ├── example ├── __init__.py ├── blog │ ├── __init__.py │ ├── urls.py │ └── views.py ├── locale │ ├── ru │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── uk │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── manage.py ├── settings.py ├── static │ └── cms │ │ ├── blog.css │ │ └── main.css ├── templates │ ├── blog-card.html │ ├── blog-home.html │ ├── blog-post.html │ ├── index.html │ ├── language-selector.html │ └── search │ │ ├── indexes │ │ └── pages │ │ │ └── page_text.txt │ │ └── search.html └── urls.py ├── pages ├── __init__.py ├── admin │ ├── __init__.py │ ├── forms.py │ ├── utils.py │ └── views.py ├── api.py ├── app_config.py ├── cache.py ├── checks.py ├── command_line.py ├── context_processors.py ├── fixtures │ └── pages_tests.json ├── locale │ ├── da │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── de │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ ├── django.po │ │ │ ├── djangojs.mo │ │ │ └── djangojs.po │ ├── en │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── es │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fr │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── he │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── it │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── nl │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── ru │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ ├── django.po │ │ │ ├── djangojs.mo │ │ │ └── djangojs.po │ ├── sk │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── tr │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── uk │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ ├── django.po │ │ ├── djangojs.mo │ │ └── djangojs.po ├── management │ ├── __init__.py │ ├── commands │ │ ├── __init__.py │ │ ├── pages_demo.py │ │ ├── pages_pull.py │ │ └── pages_push.py │ └── utils.py ├── managers.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_page_sites.py │ ├── 0003_page_uuid.py │ ├── 0004_auto_20161209_0648.py │ ├── 0005_media.py │ ├── 0006_auto_20170119_0628.py │ ├── 0007_language_code.py │ ├── 0008_auto_20181102_0818.py │ └── __init__.py ├── models.py ├── phttp.py ├── placeholders.py ├── plugins │ ├── __init__.py │ ├── jsonexport │ │ ├── __init__.py │ │ ├── actions.py │ │ ├── admin_urls.py │ │ ├── management │ │ │ ├── __init__.py │ │ │ └── commands │ │ │ │ ├── __init__.py │ │ │ │ ├── pages_export_json.py │ │ │ │ └── pages_import_json.py │ │ ├── models.py │ │ ├── templates │ │ │ └── admin │ │ │ │ └── pages │ │ │ │ └── page │ │ │ │ └── import_pages.html │ │ ├── tests.py │ │ └── utils.py │ └── pofiles │ │ ├── __init__.py │ │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ ├── pages_export_po.py │ │ │ └── pages_import_po.py │ │ └── utils.py ├── search_indexes.py ├── serializers.py ├── settings.py ├── static │ └── pages │ │ ├── css │ │ ├── font-awesome.css │ │ ├── font-awesome.min.css │ │ ├── inline-edit.css │ │ ├── pages.css │ │ └── rte.css │ │ ├── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 │ │ ├── images │ │ ├── icons │ │ │ ├── add.gif │ │ │ ├── add.svg │ │ │ ├── delete.svg │ │ │ ├── draft.svg │ │ │ ├── edit.gif │ │ │ ├── edit.svg │ │ │ ├── hidden.gif │ │ │ ├── hidden.svg │ │ │ ├── icons.gif │ │ │ ├── insert.gif │ │ │ ├── move.gif │ │ │ ├── published.svg │ │ │ ├── redirect.gif │ │ │ ├── sort.svg │ │ │ ├── view.gif │ │ │ └── view.svg │ │ ├── loading.gif │ │ └── rte │ │ │ ├── bold.gif │ │ │ ├── close.gif │ │ │ ├── image.png │ │ │ ├── italic.gif │ │ │ ├── link.png │ │ │ └── unordered.gif │ │ ├── img │ │ └── icon_searchbox.png │ │ └── javascript │ │ ├── iframe.rte.js │ │ ├── iframeResizer.min.js │ │ ├── jquery.js │ │ ├── jquery.query-2.1.7.js │ │ ├── jquery.rte.js │ │ ├── jquery.ui.js │ │ ├── pages.js │ │ ├── pages_form.js │ │ └── pages_list.js ├── templates │ ├── admin │ │ └── pages │ │ │ ├── media │ │ │ └── change_list.html │ │ │ └── page │ │ │ ├── change_form.html │ │ │ ├── change_list.html │ │ │ ├── change_list_table.html │ │ │ ├── includes │ │ │ └── fieldset.html │ │ │ ├── menu.html │ │ │ └── sub_menu.html │ ├── pages │ │ ├── breadcrumb.html │ │ ├── contact.html │ │ ├── content.html │ │ ├── dynamic_tree_menu.html │ │ ├── embed.html │ │ ├── examples │ │ │ ├── ckeditor.html │ │ │ ├── cool.html │ │ │ ├── debug.html │ │ │ ├── files.html │ │ │ ├── index.html │ │ │ └── nice.html │ │ ├── inline-edit.html │ │ ├── menu.html │ │ ├── revisions.html │ │ ├── sub_menu.html │ │ ├── tests │ │ │ ├── auto_render.txt │ │ │ ├── auto_render2.txt │ │ │ ├── base.html │ │ │ ├── block.html │ │ │ ├── block2.html │ │ │ ├── block3.html │ │ │ ├── extends.html │ │ │ ├── fileinput.html │ │ │ ├── test1.html │ │ │ ├── test2.html │ │ │ ├── test3.html │ │ │ ├── test4.html │ │ │ ├── test5.html │ │ │ ├── test6.html │ │ │ ├── test7.html │ │ │ └── untranslated.html │ │ ├── traduction_helper.html │ │ └── widgets │ │ │ ├── file_input.html │ │ │ ├── languages.html │ │ │ └── richtextarea.html │ └── search │ │ ├── indexes │ │ └── pages │ │ │ └── page_text.txt │ │ └── search.html ├── templatetags │ ├── __init__.py │ ├── ckeditor_placeholder.py │ └── pages_tags.py ├── test_runner.py ├── testproj │ ├── __init__.py │ ├── documents │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── models.py │ │ ├── urls.py │ │ └── views.py │ ├── manage.py │ ├── search_sites.py │ ├── templates │ │ ├── 404.html │ │ ├── block_placeholder.html │ │ ├── slow_template_parsing.html │ │ └── syntax_error.html │ ├── test_settings.py │ ├── urls.py │ └── views.py ├── tests │ ├── __init__.py │ ├── test_checks.py │ ├── test_commands.py │ ├── test_functional.py │ ├── test_plugins.py │ ├── test_regression.py │ ├── test_selenium.py │ ├── test_templates.py │ ├── test_unit.py │ └── testcase.py ├── urlconf_registry.py ├── urls.py ├── utils.py ├── views.py ├── widgets.py └── widgets_registry.py ├── requirements-doc.txt ├── requirements-frozen.txt ├── setup.cfg └── setup.py /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | languages: 2 | Python: true 3 | JavaScript: true 4 | exclude_paths: 5 | - "pages/static/pages/javascript/jquery*" 6 | - "doc/*" 7 | - "example/*" 8 | - "pages/tests/*" 9 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | omit = 3 | */tests/* 4 | */testproj/* 5 | */plugins/* 6 | pages/serializers.py 7 | pages/api.py 8 | pages/search_indexes.py -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "Code scanning - action" 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 7 * * 1' 8 | 9 | jobs: 10 | CodeQL-Build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v2 17 | with: 18 | # We must fetch at least the immediate parents so that if this is 19 | # a pull request then we can checkout the head. 20 | fetch-depth: 2 21 | 22 | # If this run was triggered by a pull request event, then checkout 23 | # the head of the pull request instead of the merge commit. 24 | - run: git checkout HEAD^2 25 | if: ${{ github.event_name == 'pull_request' }} 26 | 27 | # Initializes the CodeQL tools for scanning. 28 | - name: Initialize CodeQL 29 | uses: github/codeql-action/init@v1 30 | # Override language selection by uncommenting this and choosing your languages 31 | # with: 32 | # languages: go, javascript, csharp, python, cpp, java 33 | 34 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 35 | # If this step fails, then you should remove it and run the build manually (see below) 36 | - name: Autobuild 37 | uses: github/codeql-action/autobuild@v1 38 | 39 | # ℹ️ Command-line programs to run using the OS shell. 40 | # 📚 https://git.io/JvXDl 41 | 42 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 43 | # and modify them (or add more) to build your code if your project 44 | # uses a compiled language 45 | 46 | #- run: | 47 | # make bootstrap 48 | # make release 49 | 50 | - name: Perform CodeQL Analysis 51 | uses: github/codeql-action/analyze@v1 52 | -------------------------------------------------------------------------------- /.github/workflows/django.yml: -------------------------------------------------------------------------------- 1 | name: Django CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | max-parallel: 4 15 | matrix: 16 | python-version: [3.8, 3.9, "3.10", 3.12] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install Dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install .[tests] 28 | - name: Run Tests 29 | run: | 30 | python pages/test_runner.py 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | mptt/ 2 | venv/ 3 | tagging/ 4 | build/ 5 | coverage 6 | .coverage 7 | dist/ 8 | whoosh_index/ 9 | example/media/ 10 | example/collect-static/ 11 | django_page_cms.egg-info/ 12 | cms.db 13 | local_settings.py 14 | *.pyc 15 | .build 16 | .DS_Store 17 | .idea 18 | pages/testproj/media/ 19 | screenshot_* 20 | *~ 21 | .tox 22 | ghostdriver.log 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /.pep8: -------------------------------------------------------------------------------- 1 | [pep8] 2 | ignore = E128,E402,E731 3 | max-line-length = 100 4 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | 4 | [django-page-cms-1.translation] 5 | file_filter = pages/locale//LC_MESSAGES/django.po 6 | source_file = pages/locale/en/LC_MESSAGES/django.po 7 | source_lang = en 8 | 9 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Batiste Bieler 2 | Jannis Leidel 3 | Antoni Aloy López 4 | Benjamin Wohlwend 5 | poweredbypenguins 6 | homebrew79 7 | Mathieu Meylan 8 | Marco Bonetti 9 | Gabriel Walt 10 | David Michon 11 | Michael Thornhill 12 | Chris Vigelius 13 | summer.is.gone 14 | Yoan Blanc 15 | Jacques Beaurain 16 | Steve Losh 17 | Nick Sergeant 18 | Johannes Knutsen 19 | mkriheli 20 | klichukb 21 | Ivan Gromov 22 | Evgeniy Gulitsyn 23 | Cans https://github.com/cans 24 | Francesco Facconi 25 | Markus "mjtorn" Törnqvist 26 | Remigiusz Dymecki 27 | elky 28 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | doc/changelog.rst 2 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | Please refer to the project page for contributions instructions: 2 | 3 | https://github.com/batiste/django-page-cms/blob/master/doc/contributions.rst 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-slim 2 | ENV PYTHONUNBUFFERED 1 3 | RUN mkdir /code 4 | WORKDIR /code 5 | COPY ./example /code/example 6 | COPY ./pages /code/pages 7 | COPY ./doc /code/doc 8 | COPY README.rst /code/ 9 | COPY requirements-frozen.txt /code/ 10 | COPY setup.py /code/ 11 | RUN pip install .[full] -------------------------------------------------------------------------------- /FAQ: -------------------------------------------------------------------------------- 1 | FAQ 2 | === 3 | 4 | http://code.google.com/p/django-page-cms/wiki/FAQ -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | Please refer to the project page for installation instructions: 2 | 3 | http://packages.python.org/django-page-cms/installation.html 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008, Batiste Bieler 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided 13 | with the distribution. 14 | * Neither the name of the author nor the names of other 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include INSTALL 2 | include LICENSE 3 | include AUTHORS 4 | include TODO 5 | include README 6 | include README.rst 7 | include THANKS 8 | include CHANGELOG 9 | include MANIFEST.in 10 | recursive-include doc * 11 | recursive-include pages * 12 | recursive-include requirements * 13 | recursive-include example * 14 | recursive-include example/templates * 15 | recursive-include example *.html 16 | recursive-include pages/templates * 17 | recursive-include pages/plugins/jsonexport/templates * 18 | recursive-include pages/static * 19 | recursive-include pages/locale * 20 | recursive-include pages/fixtures * 21 | recursive-include pages *.css 22 | recursive-include pages *.json 23 | recursive-include pages *.js 24 | recursive-include pages *.html 25 | recursive-include pages *.mo 26 | recursive-include pages *.po 27 | global-exclude *.pyc 28 | global-exclude *.db 29 | prune pages/testproj/media/* 30 | prune doc/.build 31 | prune .DS_Store 32 | prune example/whoosh_index 33 | prune example/media 34 | # global-exclude coverage/* 35 | # this important to prune at the end 36 | # prune dist 37 | 38 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | See README.rst 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | django-page-cms 3 | =============== 4 | 5 | .. image:: https://readthedocs.org/projects/django-page-cms/badge/?version=latest 6 | :target: http://django-page-cms.readthedocs.io/en/latest/ 7 | 8 | .. image:: https://codeclimate.com/github/batiste/django-page-cms/badges/gpa.svg 9 | :target: https://codeclimate.com/github/batiste/django-page-cms 10 | :alt: Code Climate 11 | 12 | .. image:: https://scrutinizer-ci.com/g/batiste/django-page-cms/badges/quality-score.png?b=master 13 | :target: https://scrutinizer-ci.com/g/batiste/django-page-cms/?branch=master 14 | :alt: Scrutinizer Code Quality 15 | 16 | This Django CMS enables you to create and administrate hierarchical pages in a simple and powerful way. 17 | 18 | For a quick demo. 19 | 20 | .. code:: bash 21 | 22 | $ pip3 install "django-page-cms[full]"; gerbi --create mywebsite 23 | 24 | Or with docker 25 | 26 | .. code:: bash 27 | 28 | docker compose up web 29 | 30 | To create a super user account 31 | 32 | .. code:: bash 33 | 34 | docker compose run web python example/manage.py createsuperuser 35 | 36 | To create a demo website 37 | 38 | .. code:: bash 39 | 40 | docker compose run web python example/manage.py pages_demo 41 | 42 | To run tests with docker 43 | 44 | .. code:: bash 45 | 46 | docker compose run run-test 47 | 48 | More informations 49 | 50 | * `Install `_ 51 | * `Full documentation `_ 52 | * `How to contribute `_ 53 | 54 | Django page CMS is based around a placeholders concept. Placeholder is a special template tag that 55 | you use in your page templates. Every time you add a placeholder in your template such field 56 | dynamically appears in the page admin interface. 57 | 58 | Each page can have a different template with different placeholders. 59 | 60 | .. image:: https://github.com/batiste/django-page-cms/raw/master/doc/images/admin-screenshot-1.png 61 | :width: 350px 62 | :align: center 63 | 64 | Those placeholder can also be edited inline 65 | 66 | .. image:: https://github.com/batiste/django-page-cms/raw/master/doc/images/inline-edit.png 67 | :width: 350px 68 | :align: center 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /THANKS: -------------------------------------------------------------------------------- 1 | overkrik for his russian tranlation 2 | fantomas42 for his french translation 3 | dreglad for his spanish transation 4 | Chris Curvey for his install documentation 5 | il...@cs.au.dk for his danish translation -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | There is probably a lot to do here: 2 | 3 | https://github.com/batiste/django-page-cms/issues -------------------------------------------------------------------------------- /doc/3rd-party-apps.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | Integrate with other applications 3 | =================================== 4 | 5 | 6 | Delegate the rendering of a page to an application 7 | =================================================== 8 | 9 | By delegating the rendering of a page to another application, you will 10 | be able to use customized views and still get all the CMS variables 11 | to render a proper navigation. 12 | 13 | First you need a `urls.py` file that you can register to the CMS. This file might look like this:: 14 | 15 | from django.conf.urls import url 16 | from pages.testproj.documents.views import document_view 17 | 18 | urlpatterns = [ 19 | url(r'^doc-(?P[0-9]+)$', document_view, name='document_details'), 20 | url(r'^$', document_view, name='document_root'), 21 | ] 22 | 23 | .. note:: 24 | 25 | The CMS will pass the keyword arguments `current_page`, `pages_navigation` and `lang` to the view 26 | 27 | Then you need to register the urlconf module with the CMS to use it 28 | within the admin interface. Here is an example for a document application.:: 29 | 30 | from pages.urlconf_registry import register_urlconf 31 | 32 | register_urlconf('Documents', 'pages.testproj.documents.urls', 33 | label='Display documents') 34 | 35 | As soon as you have registered your `urls.py`, a new field will appear in the page administration. 36 | Choose the `Display documents`. The view used to render this page on the frontend 37 | is now choosen by `pages.testproj.documents.urls`. 38 | 39 | .. note:: 40 | 41 | The path passed to your urlconf module is the remaining path 42 | available after the page slug. Eg: `/pages/page1/doc-1` will become `doc-1`. 43 | 44 | .. note:: 45 | 46 | If you want to have the reverse URLs with your delegated application, 47 | you will need to include your URLs into your main urls.py, eg:: 48 | 49 | (r'^pages/', include('pages.urls')), 50 | ... 51 | (r'^pages/(?P.*)', include('pages.testproj.documents.urls')), 52 | 53 | Here is an example of a valid view from the documents application:: 54 | 55 | from django.shortcuts import render 56 | from pages.testproj.documents.models import Document 57 | 58 | def document_view(request, *args, **kwargs): 59 | context = dict(kwargs) 60 | if kwargs.get('current_page', False): 61 | documents = Document.objects.filter(page=kwargs['current_page']) 62 | context['documents'] = documents 63 | if kwargs.has_key('document_id'): 64 | document = Document.objects.get(pk=int(kwargs['document_id'])) 65 | context['document'] = document 66 | context['in_document_view'] = True 67 | return render(request, 'pages/examples/index.html', context) 68 | 69 | The `document_view` will receive a bunch of extra parameters related to the CMS: 70 | 71 | * `current_page` the page object, 72 | * `path` the path used to reach the page, 73 | * `lang` the current language, 74 | * `pages_navigation` the list of pages used to render navigation. 75 | 76 | .. note:: 77 | 78 | If the field doesn't appear within the admin interface make sure that 79 | your registry code is executed properly. 80 | 81 | .. note:: 82 | 83 | Look at the testproj in the repository for an example on how to integrate 84 | an external application. 85 | 86 | .. _sitemaps: 87 | 88 | Sitemaps 89 | ================= 90 | 91 | Gerbi CMS provide 2 sitemaps classes to use with `Django sitemap framework `_:: 92 | 93 | from pages.views import PageSitemap, MultiLanguagePageSitemap 94 | 95 | (r'^sitemap\.xml$', 'django.contrib.sitemaps.views.sitemap', 96 | {'sitemaps': {'pages':PageSitemap}}), 97 | 98 | # or for multi language: 99 | 100 | (r'^sitemap\.xml$', 'django.contrib.sitemaps.views.sitemap', 101 | {'sitemaps': {'pages':MultiLanguagePageSitemap}}) 102 | 103 | The `PageSitemap` class provide a sitemap for every published page in the default language. 104 | The `MultiLanguagePageSitemap` is gonna create an extra entry for every other language. -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | 9 | # Internal variables. 10 | PAPEROPT_a4 = -D latex_paper_size=a4 11 | PAPEROPT_letter = -D latex_paper_size=letter 12 | ALLSPHINXOPTS = -d .build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 13 | 14 | .PHONY: help clean html web pickle htmlhelp latex changes linkcheck 15 | 16 | help: 17 | @echo "Please use \`make ' where is one of" 18 | @echo " html to make standalone HTML files" 19 | @echo " pickle to make pickle files" 20 | @echo " json to make JSON files" 21 | @echo " htmlhelp to make HTML files and a HTML help project" 22 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 23 | @echo " changes to make an overview over all changed/added/deprecated items" 24 | @echo " linkcheck to check all external links for integrity" 25 | 26 | clean: 27 | -rm -rf .build/* 28 | 29 | html: 30 | mkdir -p .build/html .build/doctrees 31 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) .build/html 32 | @echo 33 | @echo "Build finished. The HTML pages are in .build/html." 34 | 35 | pickle: 36 | mkdir -p .build/pickle .build/doctrees 37 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) .build/pickle 38 | @echo 39 | @echo "Build finished; now you can process the pickle files." 40 | 41 | web: pickle 42 | 43 | json: 44 | mkdir -p .build/json .build/doctrees 45 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) .build/json 46 | @echo 47 | @echo "Build finished; now you can process the JSON files." 48 | 49 | htmlhelp: 50 | mkdir -p .build/htmlhelp .build/doctrees 51 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) .build/htmlhelp 52 | @echo 53 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 54 | ".hhp project file in .build/htmlhelp." 55 | 56 | latex: 57 | mkdir -p .build/latex .build/doctrees 58 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) .build/latex 59 | @echo 60 | @echo "Build finished; the LaTeX files are in .build/latex." 61 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 62 | "run these through (pdf)latex." 63 | 64 | changes: 65 | mkdir -p .build/changes .build/doctrees 66 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) .build/changes 67 | @echo 68 | @echo "The overview file is in .build/changes." 69 | 70 | linkcheck: 71 | mkdir -p .build/linkcheck .build/doctrees 72 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) .build/linkcheck 73 | @echo 74 | @echo "Link check complete; look for any errors in the above output " \ 75 | "or in .build/linkcheck/output.txt." 76 | -------------------------------------------------------------------------------- /doc/commands.rst: -------------------------------------------------------------------------------- 1 | ========================= 2 | Commands 3 | ========================= 4 | 5 | .. contents:: 6 | 7 | Import and export content between diffent hosts using the API 8 | ================================================================ 9 | 10 | Gerbi CMS comes with a simple API that uses Django REST Framework. You can enable the 11 | API by setting the PAGE_API_ENABLED in the CMS settings to `True`. 12 | 13 | When this is done, your host should have an API at the following 14 | address
/api/ 15 | 16 | Pulling data from a host: pages_pull 17 | --------------------------------------- 18 | 19 | From example if you enabled the API on your local development instance you can do:: 20 | 21 | $ python manage.py pages_pull staff_account_name:password 22 | Fetching page data on http://127.0.0.1:8000/api/ 23 | data/download.json written to disk 24 | 25 | The default for the host is the localhost (127.0.0.1:8000). This command accept the option `--filename` and `--host` 26 | 27 | Pushing data to a host: pages_push 28 | ------------------------------------- 29 | 30 | Similarly you can push the collected data to an host:: 31 | 32 | python manage.py pages_push staff_account_name:password 33 | Fetching the state of the pages on the server http://127.0.0.1:8000/api/ 34 | Update page 1 ............. 35 | Update page 2 .... 36 | Update page 3 .... 37 | 38 | This command accepts the option `--filename` and `--host` 39 | 40 | Limitations 41 | ------------------ 42 | 43 | The push command does it's best creating and updating pages between 2 hosts but equivalency 44 | cannot be guaranteed. Also files associated with the pages are yet 45 | not transfered using this API. 46 | 47 | To best syncronise 2 hosts (A:production, B: staging) first pull from A and push the content 48 | to B to have an accurate representation of production data. 49 | 50 | Do your modification on B then then push back on A. 51 | 52 | 53 | Export pages content into translatable PO files 54 | ======================================================= 55 | 56 | The pages CMS provide a command for those that would prefer 57 | to use PO files instead of the admin inteface to translate the 58 | pages content. 59 | 60 | To export all the content from the published page into PO files 61 | you can execute this Django command:: 62 | 63 | $ python manage.py pages_export_po 64 | 65 | The files a created in the `poexport` directory if no path is provided. 66 | 67 | After the translation is done, you can import back the changes with 68 | another command:: 69 | 70 | $ python manage.py pages_import_po -------------------------------------------------------------------------------- /doc/contributions.rst: -------------------------------------------------------------------------------- 1 | ======================= 2 | Contribute to Gerbi CMS 3 | ======================= 4 | 5 | I recommend to `create a fork on github `_ and 6 | make modifications in your branch. Please follow those instructions: 7 | 8 | * Add your name to the AUTHORS file. 9 | * Write tests for any new code. Try to keep the test coverage >= 90%. 10 | * Follow the PEP-08 as much as possible. 11 | * If a new dependency is introduced, justify it. 12 | * Be careful of performance regresssion. 13 | * Every new CMS setting should start with PAGE_ 14 | * Every new template_tag should start with pages_ 15 | 16 | Then create a pull request. A short explanation of what you did and why you did it goes a long way. 17 | 18 | Write tests 19 | ----------- 20 | 21 | Gerbi CMS try to keep the code base stable. The test coverage is higher 22 | than 90% and we try to keep it that way. 23 | 24 | To run all the tests:: 25 | 26 | $ python pages/test_runner.py 27 | 28 | To run a specific test case:: 29 | 30 | $ python pages/test_runner.py pages.tests.test_selenium.SeleniumTestCase 31 | 32 | To run a specific test in a test case:: 33 | 34 | $ python pages/test_runner.py pages.tests.test_selenium.SeleniumTestCase.test_admin_move_page 35 | 36 | 37 | Translations 38 | ------------ 39 | 40 | `We use transifex for translations `_ 41 | -------------------------------------------------------------------------------- /doc/display-content.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | Display page's content in templates 3 | =================================== 4 | 5 | Gerbi CMS provide several template tags to extract data from the CMS. 6 | To use these tags in your templates you must load them first: 7 | 8 | .. code-block:: html+django 9 | 10 | {% load pages_tags %} 11 | 12 | .. contents:: 13 | :local: 14 | :depth: 2 15 | 16 | get_content 17 | ----------- 18 | 19 | Store a content type from a page into a context variable that you can reuse after: 20 | 21 | .. code-block:: html+django 22 | 23 | {% get_content current_page "title" as content %} 24 | 25 | You can also use the slug of a page: 26 | 27 | .. code-block:: html+django 28 | 29 | {% get_content "my-page-slug" "title" as content %} 30 | 31 | You can also use the id of a page: 32 | 33 | .. code-block:: html+django 34 | 35 | {% get_content 10 "title" as content %} 36 | 37 | .. note:: 38 | 39 | You can use either the page object, the slug, or the id of the page. 40 | 41 | show_content 42 | ------------ 43 | 44 | Output the content of a page directly within the template: 45 | 46 | .. code-block:: html+django 47 | 48 | {% show_content current_page "title" %} 49 | 50 | .. note:: 51 | 52 | You can use either the page object, the slug, or the id of the page. 53 | 54 | page_has_content 55 | ---------------- 56 | 57 | Conditional tag that only renders its nodes if the page 58 | has content for a particular content type. By default the 59 | current page is used. 60 | 61 | Syntax: 62 | 63 | .. code-block:: html+django 64 | 65 | {% page_has_content [] %} 66 | ... 67 | {% end page_has_content %} 68 | 69 | Example: 70 | 71 | .. code-block:: html+django 72 | 73 | {% page_has_content 'header-image' %} 74 | 75 | {% end_page_has_content %} 76 | 77 | 78 | get_page 79 | ------------ 80 | 81 | Retrieve a Page object and store it into a context variable that you can reuse after. Here is 82 | an example of the use of this template tag to display a list of news: 83 | 84 | .. code-block:: html+django 85 | 86 |

Latest news

87 | {% get_page "news" as news_page %} 88 |
    89 | {% for new in news_page.get_children %} 90 |
  • 91 |

    {{ new.title }}

    92 | {{ new.publication_date }} 93 | {% show_content new body %} 94 |
  • 95 | {% endfor %} 96 |
97 | 98 | 99 | .. note:: 100 | 101 | You can use either the slug, or the id of the page. 102 | 103 | show_absolute_url 104 | ------------------- 105 | 106 | This tag show the absolute url of a page. The difference with the `Page.get_url_path` method is 107 | that the template knows which language is used within the context and display the URL accordingly: 108 | 109 | .. code-block:: html+django 110 | 111 | {% show_absolute_url current_page %} 112 | 113 | .. note:: 114 | 115 | You can use either the page object, the slug, or the id of the page. 116 | 117 | 118 | Delegate the page rendering to another application 119 | ---------------------------------------------------- 120 | 121 | :doc:`You can set another application to render certain pages of your website `. 122 | 123 | Subclass the default view 124 | ----------------------------- 125 | 126 | This CMS use a class based view. It is therefor easy 127 | to override the default behavior of this view. For example if you want to inject 128 | additional context information into all the pages templates you can override 129 | the method extra_context:: 130 | 131 | 132 | from pages.views import Details 133 | from news.models import News 134 | 135 | class NewsView(Details): 136 | 137 | def extra_context(self, request, context): 138 | lastest_news = News.object.all() 139 | context.update({'news': lastest_news}) 140 | 141 | details = NewsView() 142 | 143 | For your view to be used in place of the CMS one, you simply need 144 | to point to it with something similar to this:: 145 | 146 | from django.conf.urls import url 147 | from YOUR_APP.views import details 148 | from pages import page_settings 149 | 150 | urlpatterns = [] 151 | 152 | if page_settings.PAGE_USE_LANGUAGE_PREFIX: 153 | urlpatterns += [ 154 | url(r'^(?P[-\w]+)/(?P.*)$', details, 155 | name='pages-details-by-path') 156 | ] 157 | else: 158 | urlpatterns += [ 159 | url(r'^(?P.*)$', details, name='pages-details-by-path') 160 | ] 161 | 162 | .. note:: 163 | 164 | Have a look at `pages.urls` for a up to date example of URLs configuration. 165 | 166 | -------------------------------------------------------------------------------- /doc/edit-content.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | Inline page editing 3 | =================================== 4 | 5 | Gerbi CMS provide a way to easily edit the placeholder's content 6 | on your own frontend when you are authenticated as a staff user. 7 | 8 | .. image:: images/inline-edit.png 9 | 10 | To activate the feature on your frontend you need to add 2 tags inside your templates. One in your tag and one in the tag: 11 | 12 | .. code-block:: html+django 13 | 14 | {% load pages_tags static i18n %} 15 | 16 | 17 | ... 18 | {% pages_edit_media %} 19 | 20 | 21 | ... 22 | {% pages_edit_init %} 23 | 24 | 25 | 26 | For placeholder editing to work properly you will need to surround the placeholder with an HTML element like so: 27 | 28 | .. code-block:: html+django 29 | 30 |
31 | {% placeholder "title" %} 32 |
33 | 34 | Placeholder editing works automatically by injecting an HTML comment into the placeholder output. So thing 35 | like this will not work: 36 | 37 | .. code-block:: html+django 38 | 39 |
40 | 41 |
42 | 43 | Because the rendered comment will not result into an HTML node: 44 | 45 | .. code-block:: html+django 46 | 47 |
48 | 49 |
50 | 51 | To fix this issue you can use placeholders as rendering blocks like so: 52 | 53 | .. code-block:: html+django 54 | 55 |
56 | {% imageplaceholder 'img' block %} 57 | {% if content %} 58 | 59 | {% endif %} 60 | {% endplaceholder %} 61 |
62 | -------------------------------------------------------------------------------- /doc/gerbi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/doc/gerbi.png -------------------------------------------------------------------------------- /doc/images/admin-screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/doc/images/admin-screenshot-1.png -------------------------------------------------------------------------------- /doc/images/admin-screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/doc/images/admin-screenshot-2.png -------------------------------------------------------------------------------- /doc/images/ckeditor_placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/doc/images/ckeditor_placeholder.png -------------------------------------------------------------------------------- /doc/images/inline-edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/doc/images/inline-edit.png -------------------------------------------------------------------------------- /doc/images/rte-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/doc/images/rte-light.png -------------------------------------------------------------------------------- /doc/images/section.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/doc/images/section.png -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | =============================== 2 | Gerbi's CMS documentation 3 | =============================== 4 | 5 | Welcome on the documentation of the simple multilingual Gerbi CMS (package name: django-page-cms). You track the latest 6 | changes to the code base on the `github project page `_. 7 | To get more information about this CMS and its feature go to the :doc:`Introduction section `. 8 | 9 | For a quick install:: 10 | 11 | $ pip install django-page-cms[full]; gerbi --create mywebsite 12 | 13 | For more complete installations instructions go to 14 | the :doc:`Installation section `. To get source code with git use:: 15 | 16 | $ git clone git://github.com/batiste/django-page-cms.git django-page-cms 17 | 18 | Build this documentation from cloned repo:: 19 | 20 | $ pip install .[docs] 21 | $ python setup.py build_sphinx 22 | 23 | Table of content 24 | ====================================== 25 | 26 | .. toctree:: 27 | :maxdepth: 2 28 | 29 | introduction.rst 30 | contributions.rst 31 | placeholders.rst 32 | installation.rst 33 | display-content.rst 34 | edit-content.rst 35 | navigation-template-tags.rst 36 | 37 | create-blog-application 38 | 3rd-party-apps.rst 39 | commands.rst 40 | 41 | settings-list.rst 42 | changelog.rst 43 | page-api.rst 44 | 45 | 46 | Indices and tables 47 | ================== 48 | 49 | * :ref:`genindex` 50 | * :ref:`modindex` 51 | * :ref:`search` 52 | 53 | -------------------------------------------------------------------------------- /doc/introduction.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Introduction 3 | ============ 4 | 5 | Gerbi CMS enable you to create and administrate hierarchical pages in a simple and powerful way. 6 | 7 | Gerbi CMS is based around a placeholders concept. A placeholder is a template tag that 8 | you can use in your page's templates. Every time you add a placeholder in your template a field 9 | dynamically appears in the page admin. 10 | 11 | The project code repository is found at this address: http://github.com/batiste/django-page-cms 12 | 13 | .. contents:: 14 | :local: 15 | :depth: 1 16 | 17 | Screenshot 18 | ============ 19 | 20 | Admin page list 21 | 22 | .. image:: images/admin-screenshot-1.png 23 | 24 | Admin page edition 25 | 26 | .. image:: images/admin-screenshot-2.png 27 | 28 | Features 29 | ============ 30 | 31 | * :doc:`Automatic creation of localized placeholders ` 32 | (content area) in admin by adding placeholders tags into page templates. 33 | * Django admin application integration. 34 | * Multilingual support. 35 | * Inline editing. 36 | * Media Library. 37 | * Revisions. 38 | * Plugin for page content export and import in JSON files within the admin interface 39 | * Plugin for page content export and import in PO format for translations 40 | * `Search indexation with Django haystack `_. 41 | * Advanced rights management (publisher, language manager). 42 | * :ref:`Rich Text Editors ` are directly available. 43 | * Page can be moved in the tree in a visual way (drag & drop + simple click). 44 | * The tree can be expanded/collapsed. A cookie remember your preferences. 45 | * Possibility to specify a different page URL for each language. 46 | * Directory-like page hierarchy (page can have the same name if they are not in the same directory). 47 | * Every page can have multiple alias URLs. It's especially useful to migrate websites. 48 | * :doc:`Possibility to integrate 3th party apps `. 49 | * Image placeholder. 50 | * Support for future publication start/end date. 51 | * Page redirection to another page or arbirtrary URLs. 52 | * Page tagging. 53 | * `Sites framework `_ 54 | 55 | Dependencies & Compatibility 56 | ============================ 57 | 58 | * Django 2.0 or 3.0 59 | * Python 3.6, 3.7, 3.8 60 | * `django-mptt `_ is needed 61 | * `django-taggit if needed `_ (if PAGE_TAGGING = True) 62 | * `django-haystack if needed `_ 63 | * Gerbi CMS is shipped with jQuery. 64 | * Compatible with MySQL, PostgreSQL, SQLite3, some issues are known with Oracle. 65 | 66 | .. note:: 67 | 68 | For install instruction go to the :doc:`Installation section ` 69 | 70 | How to contribute 71 | ================== 72 | 73 | :doc:`Contributions section ` 74 | 75 | Report a bug 76 | ============ 77 | 78 | `Github issues `_ 79 | 80 | 81 | Internationalisation 82 | ==================== 83 | 84 | This application is available in English, German, French, Spanish, Danish, Russian and Hebrew. 85 | 86 | `We use transifex `_ 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /doc/navigation-template-tags.rst: -------------------------------------------------------------------------------- 1 | =============================================== 2 | How to use the various navigation template tags 3 | =============================================== 4 | 5 | Presenting a navigational structure to the user is an common task on every website. 6 | Django-pages-cms offers various template tags which can be used to create a site navigation menu. 7 | 8 | .. contents:: 9 | :local: 10 | 11 | pages_navigation variable 12 | =========================== 13 | 14 | By default the variable `pages_navigation` will be available within 15 | all of the CMS pages. `pages_navigation` is a list of Pages obtained 16 | by calling the `get_navigation` method on the `Details` class based view 17 | of the CMS:: 18 | 19 | def get_navigation(self, request, path, lang): 20 | """Get the pages that are at the root level.""" 21 | return Page.objects.navigation().order_by("tree_id") 22 | 23 | You can subclass the `Details` class based view to change 24 | this behaviour. 25 | 26 | pages_menu 27 | ========== 28 | 29 | The pages_menu tag displays the whole navigation tree, including all subpages. 30 | This is useful for smaller sites which do not have a large number of pages. 31 | 32 | Use the following snippet in your template:: 33 | 34 |
    35 | {% for page in pages_navigation %} 36 | {% pages_menu page %} 37 | {% endfor %} 38 |
39 | 40 | The pages_menu tag uses the `pages/menu.html` template to render the navigation menu. 41 | By default, the menu is rendered as a nested list:: 42 | 43 |
    44 |
  • page1
  • 45 | ... 46 |
47 | 48 | You can of course change `pages/menu.html` with the Django override mechanism 49 | to render things differently. 50 | 51 | pages_dynamic_tree_menu 52 | ======================= 53 | 54 | The pages_dynamic_tree_menu tag works similar to the pages_menu tag. 55 | but instead of displaying the whole navigation structure, 56 | only the following pages are displayed: 57 | 58 | * all "root" pages (pages which have no parent) 59 | * all parents of the current page 60 | * all direct children of the current page 61 | 62 | This type of navigation is recommended if your site has a large number 63 | of pages and/or a deep hierarchy, which is too complex or large 64 | to be presented to the user at once. 65 | 66 | 67 | Use the following snippet in your template:: 68 | 69 |
    70 | {% for page in pages_navigation %} 71 | {% pages_dynamic_tree_menu page %} 72 | {% endfor %} 73 |
74 | 75 | The pages_dynamic_tree_menu tag uses the `pages/dynamic_tree_menu.html` 76 | template to render the navigation menu. By default, the menu is rendered 77 | as a nested list similar to the pages_menu tag. 78 | 79 | pages_sub_menu 80 | ============== 81 | 82 | The pages_sub_menu tag shows all the children of the root of the current page (the highest in the hierarchy). 83 | This is typically used for a secondary navigation menu. 84 | 85 | Use the following snippet to display a list of all the 86 | children of the current root:: 87 | 88 |
    89 | {% pages_sub_menu current_page %} 90 |
91 | 92 | Again, the default template `pages/sub_menu.html` will render the items as a nested, 93 | unordered list (see above). 94 | 95 | 96 | pages_siblings_menu 97 | =================== 98 | 99 | The pages_siblings_menu tag shows all the children of the immediate parent of the current page. This can be used for example as a secondary menu. 100 | 101 | Use the following snippet to display a list of all the children of the 102 | immediate parent of the current page:: 103 | 104 |
    105 | {% pages_siblings_menu current_page %} 106 |
107 | 108 | Again, the default template `pages/sub_menu.html` will render the items as a nested, 109 | unordered list (see above). 110 | 111 | 112 | pages_breadcrumb 113 | ================ 114 | 115 | With the pages_breadcrumb tag, it is possible to use the "breadcrumb"/"you are here" 116 | navigational pattern, consisting of a list of all parents of the current page:: 117 | 118 | {% pages_breadcrumb current_page %} 119 | 120 | The output of the pages_breadcrumb tag is defined by the template `pages/breadcrumb.html`. 121 | 122 | load_pages 123 | ========== 124 | 125 | The load_pages Tag can be used to load the navigational structure 126 | in views which are *not* rendered through page's own details() view. 127 | It will check the current template context and adds the pages and 128 | current_page variable to the context, if they are not present. 129 | 130 | This is useful if you are using a common base template for your whole site, 131 | and want the pages menu to be always present, even if the actual content 132 | is not a page. 133 | 134 | The load_pages does not take any parameters and must 135 | be placed before one of the menu-rendering tags:: 136 | 137 | {% load_pages %} 138 | -------------------------------------------------------------------------------- /doc/page-api.rst: -------------------------------------------------------------------------------- 1 | ====================== 2 | Page CMS reference API 3 | ====================== 4 | 5 | .. contents:: 6 | :local: 7 | :depth: 1 8 | 9 | The application model 10 | ====================== 11 | 12 | Gerbi CMS declare rather simple models: :class:`Page ` 13 | :class:`Content ` and :class:`PageAlias `. 14 | 15 | 16 | Placeholders 17 | ============ 18 | 19 | .. automodule:: pages.placeholders 20 | :members: 21 | :undoc-members: 22 | 23 | Template tags 24 | ============= 25 | 26 | .. automodule:: pages.templatetags.pages_tags 27 | :members: 28 | 29 | Widgets 30 | ======= 31 | 32 | .. automodule:: pages.widgets 33 | :members: 34 | :undoc-members: 35 | 36 | Page Model 37 | ========== 38 | 39 | .. autoclass:: pages.models.Page 40 | :members: 41 | 42 | Page Manager 43 | ============ 44 | 45 | .. autoclass:: pages.managers.PageManager 46 | :members: 47 | :undoc-members: 48 | 49 | Page view 50 | ========== 51 | 52 | .. autoclass:: pages.views.Details 53 | :members: 54 | 55 | Content Model 56 | ============= 57 | 58 | .. autoclass:: pages.models.Content 59 | :members: 60 | :undoc-members: 61 | 62 | Content Manager 63 | =============== 64 | 65 | .. autoclass:: pages.managers.ContentManager 66 | :members: 67 | :undoc-members: 68 | 69 | PageAlias Model 70 | =============== 71 | 72 | .. autoclass:: pages.models.PageAlias 73 | :members: 74 | :undoc-members: 75 | 76 | PageAlias Manager 77 | ================= 78 | 79 | .. autoclass:: pages.managers.PageAliasManager 80 | :members: 81 | :undoc-members: 82 | 83 | Utils 84 | ===== 85 | 86 | .. automodule:: pages.utils 87 | :members: 88 | :undoc-members: 89 | 90 | Http 91 | ==== 92 | 93 | .. automodule:: pages.phttp 94 | :members: 95 | :undoc-members: 96 | -------------------------------------------------------------------------------- /doc/static/main.css: -------------------------------------------------------------------------------- 1 | .highlight .o { 2 | color:#2f2fd0; 3 | } 4 | 5 | .highlight .p { 6 | color:green; 7 | } 8 | 9 | .section img { 10 | max-width: 100%; 11 | } -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | db: 5 | image: postgres 6 | 7 | web: 8 | build: . 9 | command: bash -c "python example/manage.py migrate && python example/manage.py runserver 0.0.0.0:8000" 10 | volumes: 11 | - .:/code/ 12 | ports: 13 | - "8000:8000" 14 | depends_on: 15 | - db 16 | 17 | run-test: 18 | build: . 19 | command: bash -c "python pages/test_runner.py" 20 | depends_on: 21 | - db 22 | 23 | build-doc: 24 | build: . 25 | command: > 26 | bash -c "pip install .[docs] 27 | && python setup.py build_sphinx" 28 | 29 | fast: 30 | image: ghcr.io/batiste/django-page-cms/cms:latest 31 | command: bash -c "python example/manage.py migrate && python example/manage.py runserver 0.0.0.0:8000" 32 | volumes: 33 | - .:/code/ 34 | ports: 35 | - "8000:8000" 36 | depends_on: 37 | - db 38 | -------------------------------------------------------------------------------- /example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/example/__init__.py -------------------------------------------------------------------------------- /example/blog/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/example/blog/__init__.py -------------------------------------------------------------------------------- /example/blog/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from blog import views 3 | from django.urls import include, path, re_path 4 | 5 | urlpatterns = [ 6 | url(r'^category/(?P[0-9]+)$', views.category_view, name='blog_category_view'), 7 | url(r'^$', views.blog_index, name='blog_index') 8 | ] -------------------------------------------------------------------------------- /example/blog/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from pages.models import Page 3 | from taggit.models import Tag 4 | from django.core.paginator import Paginator 5 | 6 | def category_view(request, *args, **kwargs): 7 | context = dict(kwargs) 8 | category = Tag.objects.get(id=kwargs['tag_id']) 9 | page = context['current_page'] 10 | blogs = page.get_children_for_frontend().filter(tags__name__in=[category.name]) 11 | 12 | paginator = Paginator(blogs, 8) 13 | page_index = request.GET.get('page') 14 | blogs = paginator.get_page(page_index) 15 | context['blogs'] = blogs 16 | 17 | context['category'] = category.name 18 | return render(request, 'blog-home.html', context) 19 | 20 | def blog_index(request, *args, **kwargs): 21 | context = dict(kwargs) 22 | page = context['current_page'] 23 | blogs = page.get_children_for_frontend() 24 | 25 | paginator = Paginator(blogs, 7) 26 | page = request.GET.get('page') 27 | blogs = paginator.get_page(page) 28 | context['blogs'] = blogs 29 | context['template_name'] = 'blog-home.html' 30 | 31 | return render(request, 'blog-home.html', context) -------------------------------------------------------------------------------- /example/locale/ru/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/example/locale/ru/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /example/locale/ru/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2019-10-22 09:59-0500\n" 11 | "PO-Revision-Date: 2019-10-22 18:05+0300\n" 12 | "Language: ru\n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 17 | "%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n" 18 | "%100>=11 && n%100<=14)? 2 : 3);\n" 19 | "Last-Translator: Sergey Panasenko \n" 20 | "Language-Team: \n" 21 | "X-Generator: Poedit 1.8.7.1\n" 22 | 23 | #: settings.py:137 24 | msgid "English" 25 | msgstr "Английский" 26 | 27 | #: settings.py:138 28 | msgid "Deutsch" 29 | msgstr "Немецкий" 30 | 31 | #: settings.py:139 settings.py:150 32 | msgid "Français" 33 | msgstr "Французский" 34 | 35 | #: settings.py:151 36 | msgid "Belgium french" 37 | msgstr "Бельгийский французский" 38 | 39 | #: templates/index.html:9 40 | msgid "SEO Options" 41 | msgstr "Параметры SEO" 42 | 43 | #: templates/index.html:10 44 | msgid "meta description" 45 | msgstr "Описание" 46 | 47 | #: templates/index.html:12 48 | msgid "meta keywords" 49 | msgstr "Ключевые слова" 50 | 51 | #: templates/index.html:40 52 | msgid "Login" 53 | msgstr "Вход" 54 | 55 | #: templates/index.html:49 56 | msgid "lead" 57 | msgstr "вводная часть" 58 | 59 | #: templates/index.html:56 60 | msgid "content" 61 | msgstr "содержимое" 62 | -------------------------------------------------------------------------------- /example/locale/uk/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/example/locale/uk/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /example/locale/uk/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2019-10-22 09:59-0500\n" 11 | "PO-Revision-Date: 2019-10-22 18:03+0300\n" 12 | "Language: uk\n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != " 17 | "11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % " 18 | "100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || " 19 | "(n % 100 >=11 && n % 100 <=14 )) ? 2: 3);\n" 20 | "Last-Translator: Sergey Panasenko \n" 21 | "Language-Team: \n" 22 | "X-Generator: Poedit 1.8.7.1\n" 23 | 24 | #: settings.py:137 25 | msgid "English" 26 | msgstr "Англійська" 27 | 28 | #: settings.py:138 29 | msgid "Deutsch" 30 | msgstr "Німецька" 31 | 32 | #: settings.py:139 settings.py:150 33 | msgid "Français" 34 | msgstr "Французька" 35 | 36 | #: settings.py:151 37 | msgid "Belgium french" 38 | msgstr "Бельгійська французька" 39 | 40 | #: templates/index.html:9 41 | msgid "SEO Options" 42 | msgstr "Параметри SEO" 43 | 44 | #: templates/index.html:10 45 | msgid "meta description" 46 | msgstr "опис" 47 | 48 | #: templates/index.html:12 49 | msgid "meta keywords" 50 | msgstr "ключові слова" 51 | 52 | #: templates/index.html:40 53 | msgid "Login" 54 | msgstr "Вхід" 55 | 56 | #: templates/index.html:49 57 | msgid "lead" 58 | msgstr "ввідна частина" 59 | 60 | #: templates/index.html:56 61 | msgid "content" 62 | msgstr "вміст" 63 | -------------------------------------------------------------------------------- /example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | example_dir = os.path.dirname(__file__) 7 | sys.path.insert(0, os.path.join(example_dir, '..')) 8 | 9 | def main(): 10 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") 11 | try: 12 | from django.core.management import execute_from_command_line 13 | except ImportError as exc: 14 | raise ImportError( 15 | "Couldn't import Django. Are you sure it's installed and " 16 | "available on your PYTHONPATH environment variable? Did you " 17 | "forget to activate a virtual environment?" 18 | ) from exc 19 | execute_from_command_line(sys.argv) 20 | 21 | 22 | if __name__ == '__main__': 23 | main() 24 | -------------------------------------------------------------------------------- /example/static/cms/blog.css: -------------------------------------------------------------------------------- 1 | 2 | .blog-lead-image { 3 | text-align: center; 4 | } 5 | 6 | .blog-lead-image img { 7 | width: 100%; 8 | max-width: 500px; 9 | } 10 | 11 | .blog-home .card img { 12 | width: 100%; 13 | border-top-left-radius: 2px; 14 | border-top-right-radius: 2px; 15 | } 16 | 17 | .blog-home .card { 18 | min-width: 25%; 19 | max-width: calc(33% - 30px); 20 | } 21 | 22 | .blog-meta { 23 | margin-bottom: 0; 24 | font-size: 14px; 25 | } 26 | 27 | .blog-home .card:first-child { 28 | min-width: 75%; 29 | max-width: 100%; 30 | flex-direction: row; 31 | } 32 | .blog-home .card:first-child .blog-lead-image { 33 | width: 33%; 34 | flex-shrink: 0; 35 | } 36 | 37 | .blog-post .blog-lead-image { 38 | margin-bottom: 30px; 39 | } 40 | 41 | @media (max-width: 768px) { 42 | .blog-home .card, .blog-home .card:first-child { 43 | min-width: 33%; 44 | max-width: 45%; 45 | flex-direction: column; 46 | } 47 | .blog-home .card:first-child .blog-lead-image { 48 | width: 100%; 49 | flex-shrink: 1; 50 | } 51 | } 52 | 53 | @media (max-width: 576px) { 54 | .blog-home .card, .blog-home .card:first-child { 55 | max-width: 100%; 56 | } 57 | } -------------------------------------------------------------------------------- /example/static/cms/main.css: -------------------------------------------------------------------------------- 1 | body, input, select { 2 | color: #000; 3 | font-family: "Avenir-Book", Helvetica, sans-serif; 4 | font-size: 16px; 5 | font-weight: 400; 6 | line-height: 1.65em; 7 | } 8 | 9 | .navbar-form { 10 | margin-left: 0; 11 | margin-right: 0; 12 | } 13 | 14 | nav { 15 | display: flex; 16 | } 17 | 18 | .arrow-down { 19 | width: 0; 20 | height: 0; 21 | border-left: 5px solid transparent; 22 | border-right: 5px solid transparent; 23 | border-top: 5px solid #000; 24 | } 25 | 26 | a, a:hover { 27 | color: #0786f0; 28 | } 29 | 30 | .language-selector-choices { 31 | position: absolute; 32 | background-color: #fff; 33 | box-shadow: 0 4px 6px #bbb; 34 | padding: 10px; 35 | margin-left: -12px; 36 | display: none; 37 | } 38 | 39 | .language-selector-choices a { 40 | padding: 6px; 41 | display: block; 42 | } 43 | 44 | .language-selector:hover .language-selector-choices, 45 | .language-selector:focus .language-selector-choices 46 | { 47 | display: block; 48 | } 49 | 50 | .language-selector { 51 | margin-right: 24px; 52 | } 53 | 54 | .nav-actions { 55 | display: flex; 56 | align-items: center; 57 | } -------------------------------------------------------------------------------- /example/templates/blog-card.html: -------------------------------------------------------------------------------- 1 | {% load pages_tags static i18n humanize thumbnail %} 2 |
3 | 4 | {% get_content page "lead-image" as image %} 5 | {% if image %} 6 | {% thumbnail image "320x240" crop="center" as img %} 7 | 8 | {% endthumbnail %} 9 | {% else %} 10 | 11 | {% endif %} 12 | 13 |
14 | 15 |

{% show_content page "title" %}

16 |
17 |

{% show_content page "lead" %}

18 | {% if forloop.first %} 19 | {% get_content page "content" as content %} 20 |

{{ content | striptags | safe | truncatechars:220 }}

21 | {% endif %} 22 |

Published {{ page.creation_date | naturalday }} 23 | {% if page.tags.count %} 24 | in the categories: 25 | {% for tag in page.tags.all %} 26 | {{ tag.name }}{% if not forloop.last %},{% endif %} 27 | {% endfor %} 28 | {% endif %} 29 | by {{ page.author.first_name }} {{ page.author.last_name }} 30 |

31 |
32 |
-------------------------------------------------------------------------------- /example/templates/blog-home.html: -------------------------------------------------------------------------------- 1 | {% extends 'index.html' %} 2 | {% load pages_tags static i18n humanize thumbnail %} 3 | 4 | {% block header %} 5 |
6 |

{% placeholder "title" %} {{ category }}

7 |

{% placeholder "lead" with Textarea %}

8 |
9 | {% endblock %} 10 | 11 | {% block content %} 12 |
13 | {% for page in blogs %} 14 | {% include "blog-card.html" %} 15 | {% endfor %} 16 |
17 | 18 | 35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /example/templates/blog-post.html: -------------------------------------------------------------------------------- 1 | {% extends 'index.html' %} 2 | {% load pages_tags static i18n humanize %} 3 | 4 | {% block header %} 5 |
6 |

{% placeholder "title" %}

7 |

{% placeholder "lead" with Textarea %}

8 |

Published {{ current_page.creation_date | naturalday }} 9 | {% if current_page.tags.count %} 10 | in the categories: 11 | {% for tag in current_page.tags.all %} 12 | {{ tag.name }}{% if not forloop.last %},{% endif %} 13 | {% endfor %} 14 | {% endif %} 15 | by {{ current_page.author.first_name }} {{ current_page.author.last_name }} 16 |

17 |
18 | {% endblock %} 19 | 20 | {% block content %} 21 |
22 |
23 | {% imageplaceholder 'lead-image' block %} 24 | {% if content %} 25 | 26 | {% endif %} 27 | {% endplaceholder %} 28 |
29 | 30 |
31 | {% placeholder "content" with RichTextarea %} 32 |
33 |
34 | {% endblock %} -------------------------------------------------------------------------------- /example/templates/index.html: -------------------------------------------------------------------------------- 1 | {% load pages_tags static i18n %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% placeholder 'title' %} 17 | 18 | {% pages_edit_media %} 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 44 | 45 |
46 | {% block header %} 47 |
48 |

{% placeholder "title" %}

49 | 50 |

{% placeholder "lead" with Textarea %}

51 |
52 | {% endblock %} 53 | 54 |
55 | {% block content %} 56 | 57 | {% placeholder "content" with RichTextarea %} 58 | {% endblock %} 59 |
60 | 61 | 62 | 99 |
100 | {% pages_edit_init %} 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /example/templates/language-selector.html: -------------------------------------------------------------------------------- 1 |
2 | {% for plang in PAGE_LANGUAGES %} 3 | {% if lang == plang.0 %} 4 | {{ plang.1 }} 5 | {% endif %} 6 | {% endfor %} 7 | 8 |
9 | {% for plang in PAGE_LANGUAGES %} 10 | {% if lang != plang.0 %} 11 | {{ plang.1 }} 12 | {% endif %} 13 | {% endfor %} 14 |
15 |
-------------------------------------------------------------------------------- /example/templates/search/indexes/pages/page_text.txt: -------------------------------------------------------------------------------- 1 | {{ object.expose_content }} -------------------------------------------------------------------------------- /example/templates/search/search.html: -------------------------------------------------------------------------------- 1 | {% extends 'index.html' %} 2 | 3 | {% block header %} 4 |

Search results

5 |

For "{{ query }}"

6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 | {% if query %} 11 |
    12 | {% for result in page.object_list %} 13 |
  • 14 | {{ result.object.title }} 15 | 16 | {{ result.object.expose_content }} 17 |
  • 18 | 19 | {% empty %} 20 |
  • No results found. Did you build the search index? (python manage.py rebuild_index)
  • 21 | {% endfor %} 22 |
23 | {% else %} 24 | {# Show some example queries to run, maybe query syntax, something else? #} 25 | {% endif %} 26 |
27 | {% endblock %} -------------------------------------------------------------------------------- /example/urls.py: -------------------------------------------------------------------------------- 1 | 2 | from django.conf import settings 3 | from django.conf.urls import url 4 | from django.urls import include, path, re_path 5 | from django.conf.urls.static import static 6 | from django.contrib import admin 7 | from pages import views 8 | 9 | from pages.urlconf_registry import register_urlconf 10 | register_urlconf('blog', 'blog.urls', label='Blog index') 11 | 12 | urlpatterns = [ 13 | url(r'^i18n/', include('django.conf.urls.i18n')), 14 | url(r'^admin/', admin.site.urls), 15 | ] 16 | 17 | try: 18 | import haystack 19 | urlpatterns += [url(r'^search/', include('haystack.urls'), name='haystack_search')] 20 | except ImportError: 21 | pass 22 | 23 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 24 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 25 | 26 | urlpatterns += [ 27 | url(r'^$', views.details, {'path': '', 'name': 'pages-root'}), 28 | url(r'^', include('pages.urls')), 29 | ] 30 | 31 | admin.site.site_header = 'Gerbi Admin' 32 | -------------------------------------------------------------------------------- /pages/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Django page CMS module.""" 3 | VERSION = (2, 0, 12) 4 | __version__ = '.'.join(map(str, VERSION)) 5 | __author__ = "Batiste Bieler" 6 | __contact__ = "batiste.bieler@gmail.com" 7 | __homepage__ = "https://github.com/batiste/django-page-cms" 8 | __docformat__ = "restructuredtext" 9 | __doc__ = 'A tree based Django CMS application' 10 | __license__ = 'BSD' 11 | __keywords__ = ['django', 'cms'] 12 | 13 | default_app_config = 'pages.app_config.BasicCmsConfig' 14 | 15 | -------------------------------------------------------------------------------- /pages/admin/utils.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/admin/utils.py -------------------------------------------------------------------------------- /pages/api.py: -------------------------------------------------------------------------------- 1 | from pages.serializers import PageSerializer, ContentSerializer 2 | from rest_framework import generics 3 | from rest_framework.permissions import IsAdminUser 4 | from pages.models import Page, Content 5 | 6 | 7 | class PageList(generics.ListCreateAPIView): 8 | queryset = Page.objects.all() 9 | serializer_class = PageSerializer 10 | permission_classes = (IsAdminUser,) 11 | paginate_by = 100 12 | 13 | 14 | class PageEdit(generics.RetrieveUpdateDestroyAPIView): 15 | queryset = Page.objects.all() 16 | serializer_class = PageSerializer 17 | permission_classes = (IsAdminUser,) 18 | paginate_by = 100 19 | 20 | 21 | class ContentList(generics.ListCreateAPIView): 22 | queryset = Content.objects.all() 23 | serializer_class = ContentSerializer 24 | permission_classes = (IsAdminUser,) 25 | paginate_by = 200 26 | 27 | 28 | class ContentEdit(generics.RetrieveUpdateDestroyAPIView): 29 | queryset = Content.objects.all() 30 | serializer_class = ContentSerializer 31 | permission_classes = (IsAdminUser,) 32 | paginate_by = 200 33 | 34 | -------------------------------------------------------------------------------- /pages/app_config.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import ugettext_lazy as _ 3 | 4 | 5 | class BasicCmsConfig(AppConfig): 6 | name = 'pages' 7 | verbose_name = _("Pages") 8 | 9 | def ready(self): 10 | from . import checks 11 | -------------------------------------------------------------------------------- /pages/cache.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.core.cache import caches 4 | from django.core.cache.backends.base import InvalidCacheBackendError 5 | 6 | try: 7 | cache = caches['pages'] 8 | except InvalidCacheBackendError: 9 | cache = caches['default'] 10 | -------------------------------------------------------------------------------- /pages/checks.py: -------------------------------------------------------------------------------- 1 | from django.core.checks import register 2 | from django.core import checks 3 | from django import template 4 | from django.template import loader 5 | from pages import settings 6 | 7 | 8 | @register() 9 | def page_templates_loading_check(app_configs, **kwargs): 10 | """ Check if any page template can't be loaded. """ 11 | errors = [] 12 | 13 | for page_template in settings.get_page_templates(): 14 | try: 15 | loader.get_template(page_template[0]) 16 | except template.TemplateDoesNotExist: 17 | errors.append(checks.Warning( 18 | 'Django cannot find template %s' % page_template[0], 19 | obj=page_template, id='pages.W001')) 20 | 21 | return errors 22 | -------------------------------------------------------------------------------- /pages/command_line.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import os 4 | import shutil 5 | import stat 6 | from subprocess import call 7 | import fileinput 8 | import random 9 | PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 10 | EXAMPLE_DIR = os.path.join(PROJECT_DIR, 'example') 11 | 12 | parser = argparse.ArgumentParser(description='Gerbi CMS console tool.') 13 | parser.add_argument('--create', type=str, 14 | help='Create a new CMS example website') 15 | 16 | 17 | def print_green(msg): 18 | print('\033[92m' + msg + '\033[0m') 19 | 20 | def secret_key(): 21 | return ''.join( 22 | [random.SystemRandom().choice( 23 | 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)') for i in range(50)] 24 | ) 25 | 26 | def new_secret_key_settings(filename): 27 | for line in fileinput.input(filename, inplace=True, backup='.bak'): 28 | if line.startswith('SECRET_KEY ='): 29 | print("SECRET_KEY = '{}'".format(secret_key())) 30 | else: 31 | print(line, end='') 32 | os.remove(filename + '.bak') 33 | 34 | def main(): 35 | args = parser.parse_args() 36 | 37 | if args.create: 38 | dirname = args.create 39 | absolute = os.path.join(os.getcwd(), dirname) 40 | print_green("Creating website {}".format(dirname)) 41 | print_green("Copying example files to {}".format(dirname)) 42 | ignore = shutil.ignore_patterns('*.db', '*.pyc', 'media', 'whoosh*') 43 | shutil.copytree(EXAMPLE_DIR, dirname, ignore=ignore) 44 | st = os.stat(os.path.join(dirname, 'manage.py')) 45 | os.chmod(os.path.join(dirname, 'manage.py'), st.st_mode | stat.S_IEXEC) 46 | new_secret_key_settings(os.path.join(absolute, 'settings.py')) 47 | ret = call(['./manage.py', 'migrate'], cwd=absolute) 48 | if ret != 0: 49 | return 50 | print_green('Migration done') 51 | _input = input("Would you like to create a superuser to connect to the admin? [N/y] ") 52 | if _input.lower() == 'y': 53 | call(['./manage.py', 'createsuperuser'], cwd=absolute) 54 | print_green('Creating demo pages') 55 | call(['./manage.py', 'pages_demo'], cwd=absolute) 56 | print_green('Rebuild search index') 57 | call(['./manage.py', 'rebuild_index', '--noinput'], cwd=absolute) 58 | print_green('Run webserver') 59 | call(['./manage.py', 'runserver'], cwd=absolute) 60 | else: 61 | parser.print_help() 62 | 63 | 64 | if __name__ == "__main__": 65 | main() -------------------------------------------------------------------------------- /pages/context_processors.py: -------------------------------------------------------------------------------- 1 | """Context processors for Page CMS.""" 2 | from pages import settings 3 | 4 | 5 | def media(request): 6 | """Adds media-related variables to the `context`.""" 7 | return { 8 | 'PAGES_MEDIA_URL': settings.PAGES_MEDIA_URL, 9 | 'PAGES_STATIC_URL': settings.PAGES_STATIC_URL, 10 | 'PAGE_USE_SITE_ID': settings.PAGE_USE_SITE_ID, 11 | 'PAGE_HIDE_SITES': settings.PAGE_HIDE_SITES, 12 | 'PAGE_LANGUAGES': settings.PAGE_LANGUAGES, 13 | } 14 | -------------------------------------------------------------------------------- /pages/fixtures/pages_tests.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "auth.user", 5 | "fields": { 6 | "username": "admin", 7 | "first_name": "", 8 | "last_name": "", 9 | "is_active": true, 10 | "is_superuser": true, 11 | "is_staff": true, 12 | "last_login": "2009-01-23 03:16:13+01:00", 13 | "groups": [], 14 | "user_permissions": [], 15 | "password": "sha1$a30c7$77c8300adae2295829eea32ed7a928c3d43b53f8", 16 | "email": "asd@asd.com", 17 | "date_joined": "2009-01-23 03:01:15+01:00" 18 | } 19 | }, 20 | { 21 | "pk": 2, 22 | "model": "auth.user", 23 | "fields": { 24 | "username": "nonstaff", 25 | "first_name": "", 26 | "last_name": "", 27 | "is_active": true, 28 | "is_superuser": false, 29 | "is_staff": false, 30 | "last_login": "2009-11-06 00:00:01+01:00", 31 | "groups": [], 32 | "user_permissions": [], 33 | "password": "sha1$a30c7$77c8300adae2295829eea32ed7a928c3d43b53f8", 34 | "email": "asd@asd.com", 35 | "date_joined": "2009-11-06 00:00:00+01:00" 36 | } 37 | }, 38 | { 39 | "pk": 3, 40 | "model": "auth.user", 41 | "fields": { 42 | "username": "staff", 43 | "first_name": "", 44 | "last_name": "", 45 | "is_active": true, 46 | "is_superuser": false, 47 | "is_staff": true, 48 | "last_login": "2009-11-06 00:00:01+01:00", 49 | "groups": [], 50 | "user_permissions": [], 51 | "password": "sha1$a30c7$77c8300adae2295829eea32ed7a928c3d43b53f8", 52 | "email": "asd@asd.com", 53 | "date_joined": "2009-11-06 00:00:00+01:00" 54 | } 55 | }, 56 | { 57 | "pk": 1, 58 | "model": "sites.site", 59 | "fields": { 60 | "domain": "127.0.0.1:8000", 61 | "name": "127.0.0.1:8000" 62 | } 63 | }, 64 | { 65 | "pk": 2, 66 | "model": "sites.site", 67 | "fields": { 68 | "domain": "testserver", 69 | "name": "Test server 1" 70 | } 71 | }, 72 | { 73 | "pk": 3, 74 | "model": "sites.site", 75 | "fields": { 76 | "domain": "testserver2", 77 | "name": "Test server 2" 78 | } 79 | } 80 | ] 81 | -------------------------------------------------------------------------------- /pages/locale/da/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/locale/da/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /pages/locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /pages/locale/de/LC_MESSAGES/djangojs.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/locale/de/LC_MESSAGES/djangojs.mo -------------------------------------------------------------------------------- /pages/locale/de/LC_MESSAGES/djangojs.po: -------------------------------------------------------------------------------- 1 | #, fuzzy 2 | msgid "" 3 | msgstr "" 4 | "Project-Id-Version: django-page-cms 0.2.pre\n" 5 | "Report-Msgid-Bugs-To: \n" 6 | "POT-Creation-Date: 2008-12-08 18:56+0100\n" 7 | "PO-Revision-Date: 2008-12-08 18:58+0100\n" 8 | "Last-Translator: Jannis Leidel \n" 9 | "Language-Team: LANGUAGE \n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | 14 | #: media/pages/javascript/change_form.js:10 15 | msgid "" 16 | "Are you sure you want to change the %(field_name)s without saving the page " 17 | "first?" 18 | msgstr "%(field_name)s ändern, ohne die Seite vorher zu speichern?" 19 | -------------------------------------------------------------------------------- /pages/locale/en/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/locale/en/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /pages/locale/es/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/locale/es/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /pages/locale/fr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/locale/fr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /pages/locale/he/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/locale/he/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /pages/locale/it/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/locale/it/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /pages/locale/nl/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/locale/nl/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /pages/locale/ru/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/locale/ru/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /pages/locale/ru/LC_MESSAGES/djangojs.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/locale/ru/LC_MESSAGES/djangojs.mo -------------------------------------------------------------------------------- /pages/locale/ru/LC_MESSAGES/djangojs.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: django-page-cms 1.0.2\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2019-10-22 13:34+0000\n" 6 | "PO-Revision-Date: 2019-10-22 16:10+0300\n" 7 | "Last-Translator: Sergey Panasenko \n" 8 | "Language-Team: AXION-RTI localization group \n" 9 | "Language: ru\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "X-Generator: Poedit 1.8.7.1\n" 14 | 15 | #: pages/static/pages/javascript/pages_form.js:42 16 | msgid "You may lose any changes you have done to the page. Are you sure?" 17 | msgstr "Вы можете потерять свои изменения этой страницы. Вы уверены?" 18 | 19 | #: pages/static/pages/javascript/pages_form.js:146 20 | msgid "Are you sure you want to delete this content?" 21 | msgstr "Вы уверены, что хотите удалить это содержимое?" 22 | 23 | #~ msgid "Insert as sibling" 24 | #~ msgstr "Вставить рядом" 25 | 26 | #~ msgid "Invalid move" 27 | #~ msgstr "Неверное перемещение" 28 | 29 | #~ msgid "Insert as child" 30 | #~ msgstr "Вставить подчиненной" 31 | -------------------------------------------------------------------------------- /pages/locale/sk/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/locale/sk/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /pages/locale/tr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/locale/tr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /pages/locale/uk/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/locale/uk/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /pages/locale/uk/LC_MESSAGES/djangojs.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/locale/uk/LC_MESSAGES/djangojs.mo -------------------------------------------------------------------------------- /pages/locale/uk/LC_MESSAGES/djangojs.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2019-10-22 13:34+0000\n" 11 | "PO-Revision-Date: 2019-10-22 16:36+0300\n" 12 | "Language: uk\n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != " 17 | "11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % " 18 | "100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || " 19 | "(n % 100 >=11 && n % 100 <=14 )) ? 2: 3);\n" 20 | "Last-Translator: Sergey Panasenko \n" 21 | "Language-Team: \n" 22 | "X-Generator: Poedit 1.8.7.1\n" 23 | 24 | #: pages/static/pages/javascript/pages_form.js:42 25 | msgid "You may lose any changes you have done to the page. Are you sure?" 26 | msgstr "Ви можете втратити будь-які зміни на сторінці. Ви впевнені?" 27 | 28 | #: pages/static/pages/javascript/pages_form.js:146 29 | msgid "Are you sure you want to delete this content?" 30 | msgstr "Ви впевнені, що хочете видалити цей контент?" 31 | -------------------------------------------------------------------------------- /pages/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/management/__init__.py -------------------------------------------------------------------------------- /pages/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/management/commands/__init__.py -------------------------------------------------------------------------------- /pages/management/commands/pages_demo.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand, CommandError 2 | from pages.models import Page 3 | from pages.tests.testcase import new_page 4 | from django.contrib.auth import get_user_model 5 | 6 | 7 | lorem = """Lorem ipsum dolor sit amet, consectetur adipiscing elit. 8 | Quisque tempus tellus enim, quis tempus dui pretium non. 9 | Cras eget enim vel magna fringilla cursus ut sit amet mi. 10 | Curabitur id pharetra turpis. Pellentesque quis eros nunc. 11 | Etiam interdum nisi ut sapien facilisis ornare. Mauris in tellus elit. 12 | Integer vulputate venenatis odio. Vivamus in diam vitae magna gravida 13 | sodales sit amet id ex. Aliquam commodo massa at mollis blandit. 14 | Donec auctor sapien et risus gravida, ultrices imperdiet est laoreet.""" 15 | 16 | 17 | class Command(BaseCommand): 18 | help = 'Create a couple of dummy pages for a demo' 19 | 20 | def handle(self, *args, **options): 21 | 22 | UserModel = get_user_model() 23 | if UserModel.objects.count() == 0: 24 | UserModel.objects.create(username='demo', password='demo') 25 | 26 | new_page(content={ 27 | 'title': 'Home', 'slug': 'home', 'lead': 'Welcome to the Gerbi CMS', 'content': lorem 28 | }, template='index.html') 29 | prod_page = new_page(content={ 30 | 'title': 'Products', 'slug': 'products', 'lead': 'Our products', 'content': lorem 31 | }, template='index.html') 32 | new_page(content={ 33 | 'title': 'Poney', 'slug': 'poney', 'lead': 'Our shiny poney', 'content': lorem}, 34 | parent=prod_page, 35 | template='index.html') 36 | new_page(content={ 37 | 'title': 'Hat', 'slug': 'hat', 'lead': 'Fedora for elegance', 'content': lorem}, 38 | parent=prod_page, 39 | template='index.html') 40 | blog = new_page(content={ 41 | 'title': 'Blog', 'slug': 'blog', 'lead': 'Blog example', 'content': lorem}, 42 | template='blog-home.html', delegate_to='blog') 43 | new_page(content={ 44 | 'title': 'Blog post 1', 'slug': 'blog-post-1', 'lead': 'Blog post example', 'content': lorem}, 45 | parent=blog, 46 | template='blog-post.html') 47 | new_page(content={ 48 | 'title': 'Blog post 2', 'slug': 'blog-post-2', 'lead': 'Blog post example', 'content': lorem}, 49 | parent=blog, 50 | template='blog-post.html') 51 | new_page(content={ 52 | 'title': 'Blog post 3', 'slug': 'blog-post-3', 'lead': 'Blog post example', 'content': lorem}, 53 | parent=blog, 54 | template='blog-post.html') 55 | new_page(content={ 56 | 'title': 'Blog post 4', 'slug': 'blog-post-4', 'lead': 'Blog post example', 'content': lorem}, 57 | parent=blog, 58 | template='blog-post.html') 59 | new_page(content={ 60 | 'title': 'Contact', 'slug': 'contact', 61 | 'lead': 'Contact us at gerbi@example.com', 'content':lorem 62 | }, template='index.html') 63 | 64 | -------------------------------------------------------------------------------- /pages/management/commands/pages_pull.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand, CommandError 2 | import requests 3 | import os 4 | import json 5 | from pages.management.utils import APICommand 6 | 7 | class Command(APICommand): 8 | help = 'Pull data from a Django Page CMS API' 9 | 10 | def handle(self, *args, **options): 11 | self.parse_options(options) 12 | 13 | self.cprint("Fetching page data on " + self.host) 14 | self.host = self.host + '?format=json' 15 | 16 | page_list = requests.get(self.host, auth=self.auth, timeout=5) 17 | if page_list.status_code != 200: 18 | self.http_error(response) 19 | data = json.loads(page_list.text) 20 | 21 | if not os.path.exists(os.path.dirname(self.filename)): 22 | os.makedirs(os.path.dirname(self.filename)) 23 | with open(self.filename, "w") as f: 24 | f.write(json.dumps(data)) 25 | self.cprint(self.filename + " written to disk") 26 | -------------------------------------------------------------------------------- /pages/management/commands/pages_push.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand, CommandError 2 | import requests 3 | import os 4 | import json 5 | from pages.management.utils import APICommand 6 | from tqdm import tqdm 7 | 8 | class Command(APICommand): 9 | help = 'Push data to a Django Page CMS API' 10 | 11 | def push_content(self, page, desc): 12 | page_id = str(page['id']) 13 | auth = self.auth 14 | headers = {'Content-Type': 'application/json'} 15 | for content in tqdm(page['content_set'], leave=True, desc=desc): 16 | content['page'] = page_id 17 | data = json.dumps(content) 18 | url = self.host + 'contents/' + str(content['id']) + '/' 19 | response = requests.put(url, data=data, auth=self.auth, headers=headers) 20 | if response.status_code == 404: 21 | url = self.host + 'contents/' 22 | response = requests.post(url, data=data, auth=self.auth, headers=headers) 23 | if response.status_code != 200 and response.status_code != 201: 24 | self.http_error(response) 25 | 26 | def push_page(self, page): 27 | page_id = str(page['id']) 28 | auth = self.auth 29 | 30 | headers = {'Content-Type': 'application/json'} 31 | 32 | server_page = self.uuid_mapping.get(page['uuid'], None) 33 | 34 | # we don't change the parent if for a reason or another it is 35 | # not present on the server 36 | if self.server_id_mapping.get(page['parent']): 37 | page['parent'] = self.server_id_mapping[page['parent']] 38 | else: 39 | del page['parent'] 40 | 41 | desc = None 42 | 43 | if server_page: 44 | self.server_id_mapping[page['id']] = server_page['id'] 45 | page['id'] = server_page['id'] 46 | desc = "Update page " + str(page['id']) 47 | url = self.host + 'pages/' + str(page['id']) + '/' 48 | data = json.dumps(page) 49 | response = requests.put(url, data=data, auth=self.auth, headers=headers) 50 | else: 51 | desc = "Create page " + str(page['id']) 52 | url = self.host 53 | data = json.dumps(page) 54 | response = requests.post(url, data=data, auth=self.auth, headers=headers) 55 | if response.status_code == 201: 56 | new_page = json.loads(response.text) 57 | new_page['content_set'] = page['content_set'] 58 | self.server_id_mapping[page['id']] = new_page['id'] 59 | self.uuid_mapping[new_page['uuid']] = new_page 60 | page = new_page 61 | 62 | if response.status_code != 200 and response.status_code != 201: 63 | self.http_error(response) 64 | 65 | self.push_content(page, desc) 66 | 67 | 68 | def handle(self, *args, **options): 69 | self.parse_options(options) 70 | 71 | self.uuid_mapping = {} 72 | self.server_id_mapping = {} 73 | 74 | self.cprint("Fetching the state of the pages on the server: " + self.host) 75 | host = self.host + '?format=json' 76 | response = requests.get(host, auth=self.auth) 77 | if response.status_code != 200: 78 | self.http_error(response) 79 | self.current_page_list = json.loads(response.text) 80 | self.cprint("Valid JSON document received.") 81 | 82 | for page in self.current_page_list: 83 | self.uuid_mapping[page['uuid']] = page 84 | 85 | with open(self.filename, "r") as f: 86 | data = f.read() 87 | pages = json.loads(data) 88 | for page in pages: 89 | self.push_page(page) 90 | -------------------------------------------------------------------------------- /pages/management/utils.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand, CommandError 2 | import requests 3 | import os 4 | import sys 5 | 6 | class APICommand(BaseCommand): 7 | help = 'Base API command' 8 | 9 | def parse_options(self, options): 10 | auth = options['auth'].split(':') 11 | self.auth = (auth[0], auth[1]) 12 | self.verbosity = options.get('verbosity', 1) 13 | host = options['host'] 14 | if not host.endswith('/'): 15 | host = host + '/' 16 | if not host.startswith('http://'): 17 | host = 'http://' + host 18 | self.host = host 19 | self.filename = options['filename'] 20 | 21 | def http_error(self, response): 22 | with open('error.html', "w") as f: 23 | f.write(response.text) 24 | raise ValueError("Error type " + str(response.status_code) + " file written: error.html") 25 | 26 | def cprint(self, msg): 27 | if self.verbosity > 0: 28 | print(msg) 29 | 30 | def cout(self, msg): 31 | if self.verbosity > 0: 32 | sys.stdout.write(msg) 33 | 34 | def add_arguments(self, parser): 35 | parser.add_argument('auth', type=str, 36 | help='authentication in the form user:password') 37 | parser.add_argument('--host', type=str, 38 | help='server to pull from', 39 | default='http://127.0.0.1:8000/api/') 40 | parser.add_argument('--filename', type=str, 41 | default="data/download.json") -------------------------------------------------------------------------------- /pages/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import pages.utils 6 | from django.conf import settings 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Content', 18 | fields=[ 19 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 20 | ('language', models.CharField(max_length=5, verbose_name='language')), 21 | ('body', models.TextField(verbose_name='body', blank=True)), 22 | ('type', models.CharField(max_length=100, verbose_name='type', db_index=True)), 23 | ('creation_date', models.DateTimeField(default=pages.utils.get_now, verbose_name='creation date', editable=False)), 24 | ], 25 | options={ 26 | 'get_latest_by': 'creation_date', 27 | 'verbose_name': 'content', 28 | 'verbose_name_plural': 'contents', 29 | }, 30 | ), 31 | migrations.CreateModel( 32 | name='Page', 33 | fields=[ 34 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 35 | ('creation_date', models.DateTimeField(default=pages.utils.get_now, verbose_name='creation date', editable=False)), 36 | ('publication_date', models.DateTimeField(help_text='When the page should go\n live. Status must be "Published" for page to go live.', null=True, verbose_name='publication date', blank=True)), 37 | ('publication_end_date', models.DateTimeField(help_text='When to expire the page.\n Leave empty to never expire.', null=True, verbose_name='publication end date', blank=True)), 38 | ('last_modification_date', models.DateTimeField(verbose_name='last modification date')), 39 | ('status', models.IntegerField(default=0, verbose_name='status', choices=[(1, 'Published'), (3, 'Hidden'), (0, 'Draft')])), 40 | ('template', models.CharField(max_length=100, null=True, verbose_name='template', blank=True)), 41 | ('delegate_to', models.CharField(max_length=100, null=True, verbose_name='delegate to', blank=True)), 42 | ('freeze_date', models.DateTimeField(help_text="Don't publish any content\n after this date.", null=True, verbose_name='freeze date', blank=True)), 43 | ('redirect_to_url', models.CharField(max_length=200, null=True, blank=True)), 44 | ('lft', models.PositiveIntegerField(editable=False, db_index=True)), 45 | ('rght', models.PositiveIntegerField(editable=False, db_index=True)), 46 | ('tree_id', models.PositiveIntegerField(editable=False, db_index=True)), 47 | ('level', models.PositiveIntegerField(editable=False, db_index=True)), 48 | ('author', models.ForeignKey(verbose_name='author', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), 49 | ('parent', models.ForeignKey(related_name='children', verbose_name='parent', blank=True, to='pages.Page', null=True, on_delete=models.SET_NULL)), 50 | ('redirect_to', models.ForeignKey(related_name='redirected_pages', blank=True, to='pages.Page', null=True, on_delete=models.SET_NULL)), 51 | ], 52 | options={ 53 | 'ordering': ['tree_id', 'lft'], 54 | 'get_latest_by': 'publication_date', 55 | 'verbose_name': 'page', 56 | 'verbose_name_plural': 'pages', 57 | 'permissions': [('can_freeze', 'Can freeze page'), ('can_publish', 'Can publish page'), ('can_manage_de', 'Manage German'), ('can_manage_fr_ch', 'Manage Français'), ('can_manage_en_us', 'Manage US English')], 58 | }, 59 | ), 60 | migrations.CreateModel( 61 | name='PageAlias', 62 | fields=[ 63 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 64 | ('url', models.CharField(unique=True, max_length=255)), 65 | ('page', models.ForeignKey(verbose_name='page', blank=True, to='pages.Page', null=True, on_delete=models.SET_NULL)), 66 | ], 67 | options={ 68 | 'verbose_name_plural': 'Aliases', 69 | }, 70 | ), 71 | migrations.AddField( 72 | model_name='content', 73 | name='page', 74 | field=models.ForeignKey(verbose_name='page', to='pages.Page', on_delete=models.CASCADE), 75 | ), 76 | ] 77 | -------------------------------------------------------------------------------- /pages/migrations/0002_page_sites.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('sites', '0001_initial'), 11 | ('pages', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='page', 17 | name='sites', 18 | field=models.ManyToManyField(default=[1], help_text='The site(s) the page is accessible at.', verbose_name='sites', to='sites.Site'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /pages/migrations/0003_page_uuid.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import uuid 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('pages', '0002_page_sites'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='page', 17 | name='uuid', 18 | field=models.UUIDField(default=uuid.uuid4, editable=False), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /pages/migrations/0004_auto_20161209_0648.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.9 on 2016-12-09 06:48 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('pages', '0003_page_uuid'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='content', 18 | name='page', 19 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='pages.Page', verbose_name='page'), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /pages/migrations/0005_media.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.12 on 2017-01-19 05:31 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('pages', '0004_auto_20161209_0648'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Media', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('title', models.CharField(blank=True, max_length=255)), 20 | ('description', models.TextField(blank=True)), 21 | ('url', models.FileField(upload_to='')), 22 | ('extension', models.CharField(blank=True, max_length=32)), 23 | ], 24 | options={ 25 | 'verbose_name_plural': 'Medias', 26 | }, 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /pages/migrations/0006_auto_20170119_0628.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.12 on 2017-01-19 06:28 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import pages.models 7 | import pages.utils 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('pages', '0005_media'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AddField( 18 | model_name='media', 19 | name='creation_date', 20 | field=models.DateTimeField(default=pages.utils.get_now, editable=False, verbose_name='creation date'), 21 | ), 22 | migrations.AlterField( 23 | model_name='media', 24 | name='url', 25 | field=models.FileField(upload_to=pages.models.media_filename), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /pages/migrations/0007_language_code.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('pages', '0006_auto_20170119_0628'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='content', 16 | name='language', 17 | field=models.CharField(max_length=7, verbose_name='language'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /pages/migrations/0008_auto_20181102_0818.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.3 on 2018-11-02 08:18 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import mptt.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('pages', '0007_language_code'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='media', 17 | name='extension', 18 | field=models.CharField(blank=True, editable=False, max_length=32), 19 | ), 20 | migrations.AlterField( 21 | model_name='page', 22 | name='parent', 23 | field=mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='pages.Page', verbose_name='parent'), 24 | ), 25 | migrations.AlterField( 26 | model_name='page', 27 | name='redirect_to', 28 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='redirected_pages', to='pages.Page', verbose_name='redirect to'), 29 | ), 30 | migrations.AlterField( 31 | model_name='page', 32 | name='redirect_to_url', 33 | field=models.CharField(blank=True, max_length=200, null=True, verbose_name='redirect to url'), 34 | ), 35 | migrations.AlterField( 36 | model_name='pagealias', 37 | name='page', 38 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pages.Page', verbose_name='page'), 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /pages/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/migrations/__init__.py -------------------------------------------------------------------------------- /pages/phttp.py: -------------------------------------------------------------------------------- 1 | """Page CMS functions related to the ``request`` object.""" 2 | from pages import settings 3 | 4 | 5 | LANGUAGE_KEYS = [key for (key, value) in settings.PAGE_LANGUAGES] 6 | 7 | 8 | def get_request_mock(): 9 | """Build a ``request`` mock up for tests""" 10 | from django.test.client import RequestFactory 11 | from django.core.handlers.base import BaseHandler 12 | factory = RequestFactory() 13 | 14 | request = factory.get('/') 15 | 16 | class FakeUser(): 17 | is_authenticated = False 18 | is_staff = False 19 | 20 | request.user = FakeUser() 21 | request.session = {} 22 | 23 | return request 24 | 25 | 26 | def get_slug(path): 27 | """ 28 | Return the page's slug 29 | 30 | >>> get_slug('/test/function/') 31 | function 32 | """ 33 | if path.endswith('/'): 34 | path = path[:-1] 35 | return path.split("/")[-1] 36 | 37 | 38 | def remove_slug(path): 39 | """ 40 | Return the remainin part of the path 41 | 42 | >>> remove_slug('/test/some/function/') 43 | test/some 44 | """ 45 | if path.endswith('/'): 46 | path = path[:-1] 47 | if path.startswith('/'): 48 | path = path[1:] 49 | if "/" not in path or not path: 50 | return None 51 | parts = path.split("/")[:-1] 52 | return "/".join(parts) 53 | 54 | 55 | def get_template_from_request(request, page=None): 56 | """ 57 | Gets a valid template from different sources or falls back to the 58 | default template. 59 | """ 60 | page_templates = settings.get_page_templates() 61 | if len(page_templates) == 0: 62 | return settings.PAGE_DEFAULT_TEMPLATE 63 | template = request.POST.get('template', request.GET.get('template', None)) 64 | if template is not None and \ 65 | (template in list(dict(page_templates).keys()) or 66 | template == settings.PAGE_DEFAULT_TEMPLATE): 67 | return template 68 | if page is not None: 69 | return page.get_template() 70 | return settings.PAGE_DEFAULT_TEMPLATE 71 | 72 | 73 | def get_language_from_request(request): 74 | """Return the most obvious language according the request.""" 75 | language = request.GET.get('language', None) 76 | if language: 77 | return language 78 | 79 | if hasattr(request, 'LANGUAGE_CODE'): 80 | lang = settings.PAGE_LANGUAGE_MAPPING(str(request.LANGUAGE_CODE)) 81 | if lang not in LANGUAGE_KEYS: 82 | return settings.PAGE_DEFAULT_LANGUAGE 83 | else: 84 | return lang 85 | else: 86 | return settings.PAGE_DEFAULT_LANGUAGE 87 | -------------------------------------------------------------------------------- /pages/plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/plugins/__init__.py -------------------------------------------------------------------------------- /pages/plugins/jsonexport/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/plugins/jsonexport/__init__.py -------------------------------------------------------------------------------- /pages/plugins/jsonexport/actions.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import ugettext_lazy as _ 2 | from django.http import HttpResponse 3 | from django.db import transaction 4 | from django.shortcuts import render 5 | from django.template import RequestContext 6 | 7 | from pages.phttp import get_language_from_request 8 | from pages.plugins.jsonexport.utils import pages_to_json, json_to_pages 9 | from pages.models import Page 10 | 11 | JSON_PAGE_EXPORT_FILENAME = 'cms_pages.json' 12 | 13 | 14 | def export_pages_as_json(modeladmin, request, queryset): 15 | response = HttpResponse(content_type="application/json") 16 | response['Content-Disposition'] = 'attachment; filename=%s' % ( 17 | JSON_PAGE_EXPORT_FILENAME,) 18 | response.write(pages_to_json(queryset)) 19 | return response 20 | export_pages_as_json.short_description = _("Export pages as JSON") 21 | 22 | 23 | @transaction.atomic 24 | def import_pages_from_json(modeladmin, request, queryset, 25 | template_name='admin/pages/page/import_pages.html'): 26 | 27 | try: 28 | j = request.FILES['json'] 29 | except KeyError: 30 | return render(request, template_name, { 31 | 'nofile': True, 32 | 'app_label': 'pages', 33 | 'opts': Page._meta, 34 | }, RequestContext(request)) 35 | 36 | errors, pages_created = json_to_pages(j.read(), request.user, 37 | get_language_from_request(request)) 38 | 39 | return render(request, template_name, { 40 | 'errors': errors, 41 | 'pages_created': pages_created, 42 | 'app_label': 'pages', 43 | 'opts': Page._meta, 44 | }, RequestContext(request)) 45 | 46 | import_pages_from_json.short_description = _("Import some pages from a JSON file") 47 | -------------------------------------------------------------------------------- /pages/plugins/jsonexport/admin_urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url, include 2 | 3 | urlpatterns = patterns('', 4 | url(r'^import-json/$', 5 | 'pages.plugins.jsonexport.actions.import_pages_from_json', 6 | name='import-pages-from-json'), 7 | ) -------------------------------------------------------------------------------- /pages/plugins/jsonexport/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/plugins/jsonexport/management/__init__.py -------------------------------------------------------------------------------- /pages/plugins/jsonexport/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/plugins/jsonexport/management/commands/__init__.py -------------------------------------------------------------------------------- /pages/plugins/jsonexport/management/commands/pages_export_json.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import ugettext as _ 2 | from django.core.management.base import BaseCommand, CommandError 3 | from pages.plugins.jsonexport.utils import (pages_to_json, 4 | monkeypatch_remove_pages_site_restrictions) 5 | from pages.models import Page 6 | 7 | from optparse import make_option 8 | import sys 9 | 10 | class Command(BaseCommand): 11 | args = '' 12 | option_list = BaseCommand.option_list + ( 13 | make_option('--all', 14 | action='store_true', 15 | dest='all_sites', 16 | default=False, 17 | help="export all sites from the database", 18 | ),) 19 | 20 | def handle(self, site=None, **options): 21 | """ 22 | Export pages in JSON format. Site may be specified by id or domain. 23 | Default: export pages from the current site specified by settings.SITE_ID. 24 | """ 25 | if options['all_sites']: 26 | monkeypatch_remove_pages_site_restrictions() 27 | qs = Page.objects.all() 28 | if site: 29 | for match in ('pk', 'domain'): 30 | try: 31 | s = Site.objects.get(**{match:site}) 32 | break 33 | except (Site.objects.DoesNotExist, ValueError): 34 | continue 35 | else: 36 | raise CommandError(_("Site with id/domain = '%s' not found") 37 | % site) 38 | qs.filter(site=s) 39 | 40 | sys.stdout.write(pages_to_json(qs)) 41 | 42 | -------------------------------------------------------------------------------- /pages/plugins/jsonexport/management/commands/pages_import_json.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import ugettext as _ 2 | from django.core.management.base import BaseCommand, CommandError 3 | from django.contrib.auth import get_user_model 4 | from pages.plugins.jsonexport.utils import (json_to_pages, 5 | monkeypatch_remove_pages_site_restrictions) 6 | 7 | import sys 8 | 9 | 10 | class Command(BaseCommand): 11 | args = '' 12 | 13 | def handle(self, user, **options): 14 | """ 15 | Import pages in JSON format. When creating a page and the original 16 | author does not exist, use user as the new author. User may be 17 | specified by username, id or email address. 18 | """ 19 | monkeypatch_remove_pages_site_restrictions() 20 | user_model = get_user_model() 21 | for match in ('username', 'pk', 'email'): 22 | try: 23 | u = user_model.objects.get(**{match: user}) 24 | break 25 | except (user_model.DoesNotExist, ValueError): 26 | continue 27 | else: 28 | raise CommandError(_("User with username/id/email = '%s' not found") 29 | % user) 30 | 31 | json = sys.stdin.read() 32 | errors, pages_created = json_to_pages(json, u) 33 | if errors: 34 | for e in errors: 35 | sys.stderr.write(e + '\n') 36 | raise CommandError(_("Errors encountered while importing JSON")) 37 | for page, created, messages in pages_created: 38 | print((_("%s created.") if created else _("%s modified.")) % ( 39 | page.get_complete_slug())) 40 | for m in messages: 41 | print(" - " + m) 42 | -------------------------------------------------------------------------------- /pages/plugins/jsonexport/models.py: -------------------------------------------------------------------------------- 1 | import pages.admin 2 | from pages.plugins.jsonexport.actions import export_pages_as_json 3 | from pages.plugins.jsonexport.actions import import_pages_from_json 4 | pages.admin.add_page_action(export_pages_as_json) 5 | pages.admin.add_page_action(import_pages_from_json) -------------------------------------------------------------------------------- /pages/plugins/jsonexport/templates/admin/pages/page/import_pages.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n %} 3 | 4 | {% block breadcrumbs %} 5 | 11 | {% endblock %} 12 | 13 | {% block content %} 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 |
22 | {% csrf_token %} 23 | 24 | 25 |
26 |
27 | 28 | {% if not nofile %} 29 | {% if errors %} 30 |

{% blocktrans %}Importing pages failed:{% endblocktrans %}

31 |
    32 | {% for e in errors %} 33 |
  • {{ e }}
  • 34 | {% endfor %} 35 |
36 | {% else %} 37 |

{% blocktrans %}Import completed.{% endblocktrans %}

38 |
    39 | {% for page, created, messages in pages_created %} 40 |
  • 41 | {{ page.get_complete_slug }} "{{ page.title }}" 42 | {% if created %} 43 | {% trans "created" %} 44 | {% else %} 45 | {% trans "modified" %} 46 | {% endif %} 47 | {% if messages %} 48 |
      49 | {% for m in messages %}
    • {{ m }}
    • {% endfor %} 50 |
    51 | {% endif %} 52 |
  • 53 | {% endfor %} 54 |
55 | {% endif %} 56 | {% endif %} 57 | 58 |

{% trans "Return to page list" %}

59 | {% endblock %} 60 | 61 | -------------------------------------------------------------------------------- /pages/plugins/jsonexport/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.test.utils import override_settings 3 | from django.contrib.auth.models import User 4 | from taggit.models import Tag 5 | 6 | from pages.models import Page 7 | from pages.tests.testcase import TestCase 8 | from pages.plugins.jsonexport.utils import pages_to_json, json_to_pages 9 | 10 | 11 | class JSONExportTestCase(TestCase): 12 | """Django page CMS JSON Export tests suite class.""" 13 | 14 | @override_settings(PAGE_TAGGING=True) 15 | def test_flow(self): 16 | # Export 17 | page1 = self.new_page(content={'title': 'page1', 'slug': 'slug1'}) 18 | tag1 = Tag.objects.create(name="t1") 19 | page1.tags.add(tag1) 20 | 21 | page2 = self.new_page(content={'title': 'page2', 'slug': 'slug2'}) 22 | tag2 = Tag.objects.create(name="t2") 23 | page2.tags.add(tag2) 24 | 25 | data = pages_to_json(Page.objects.all()) 26 | 27 | # Clear 28 | Page.objects.all().delete() 29 | Tag.objects.all().delete() 30 | 31 | # Import 32 | user = User.objects.create() 33 | json_to_pages(data, user) 34 | pages = Page.objects.all() 35 | self.assertEqual(pages.count(), 2) 36 | 37 | page1 = Page.objects.from_slug('slug1') 38 | self.assertEqual(page1.title(), 'page1') 39 | self.assertEqual([t.name for t in page1.tags.all()], ['t1']) 40 | 41 | page1 = Page.objects.from_slug('slug2') 42 | self.assertEqual(page1.title(), 'page2') 43 | self.assertEqual([t.name for t in page1.tags.all()], ['t2']) 44 | -------------------------------------------------------------------------------- /pages/plugins/pofiles/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/plugins/pofiles/__init__.py -------------------------------------------------------------------------------- /pages/plugins/pofiles/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/plugins/pofiles/management/__init__.py -------------------------------------------------------------------------------- /pages/plugins/pofiles/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/plugins/pofiles/management/commands/__init__.py -------------------------------------------------------------------------------- /pages/plugins/pofiles/management/commands/pages_export_po.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand, CommandError 2 | from pages.plugins.pofiles.utils import export_po_files 3 | 4 | 5 | class Command(BaseCommand): 6 | args = '' 7 | help = export_po_files.__doc__ 8 | 9 | def handle(self, *args, **options): 10 | export_po_files(*args) -------------------------------------------------------------------------------- /pages/plugins/pofiles/management/commands/pages_import_po.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand, CommandError 2 | from pages.plugins.pofiles.utils import import_po_files 3 | 4 | class Command(BaseCommand): 5 | args = '' 6 | help = import_po_files.__doc__ 7 | 8 | def handle(self, *args, **options): 9 | import_po_files(*args) -------------------------------------------------------------------------------- /pages/plugins/pofiles/utils.py: -------------------------------------------------------------------------------- 1 | import polib 2 | import os 3 | from pages.models import Page, Content 4 | import sys 5 | from pages import settings 6 | 7 | do_not_msg = "DO NOT MODIFIY BELOW THIS LINE" 8 | po_comment = """Page %s 9 | %s 10 | placeholder=%s 11 | page_id=%d 12 | content_id=%s""" 13 | 14 | 15 | def export_po_files(path='poexport', stdout=None): 16 | """ 17 | Export all the content from the published pages into 18 | po files. The files will be automatically updated 19 | with the new content if you run the command again. 20 | """ 21 | if stdout is None: 22 | stdout = sys.stdout 23 | if not path.endswith('/'): 24 | path += '/' 25 | 26 | source_language = settings.PAGE_DEFAULT_LANGUAGE 27 | source_list = [] 28 | for page in Page.objects.published(): 29 | source_list.extend(page.content_by_language(source_language)) 30 | 31 | for lang in settings.PAGE_LANGUAGES: 32 | if lang[0] != settings.PAGE_DEFAULT_LANGUAGE: 33 | try: 34 | os.mkdir(path) 35 | except OSError: 36 | pass 37 | po_path = path + lang[0] + '.po' 38 | stdout.write("Export language %s.\n" % lang[0]) 39 | po = polib.pofile(po_path) 40 | po.metadata['Content-Type'] = 'text/plain; charset=utf-8' 41 | 42 | for source_content in source_list: 43 | page = source_content.page 44 | try: 45 | target_content = Content.objects.get_content_object( 46 | page, lang[0], source_content.type) 47 | msgstr = target_content.body 48 | except Content.DoesNotExist: 49 | target_content = None 50 | msgstr = "" 51 | if source_content.body: 52 | if target_content: 53 | tc_id = str(target_content.id) 54 | else: 55 | tc_id = "" 56 | entry = polib.POEntry(msgid=source_content.body, 57 | msgstr=msgstr) 58 | entry.tcomment = po_comment % (page.title(), do_not_msg, 59 | source_content.type, page.id, tc_id) 60 | if entry not in po: 61 | po.append(entry) 62 | po.save(po_path) 63 | stdout.write("""Export finished. The files are available """ 64 | """in the %s directory.\n""" % path) 65 | 66 | 67 | def import_po_files(path='poexport', stdout=None): 68 | """ 69 | Import all the content updates from the po files into 70 | the pages. 71 | """ 72 | source_language = settings.PAGE_DEFAULT_LANGUAGE 73 | source_list = [] 74 | pages_to_invalidate = [] 75 | for page in Page.objects.published(): 76 | source_list.extend(page.content_by_language(source_language)) 77 | if stdout is None: 78 | stdout = sys.stdout 79 | if not path.endswith('/'): 80 | path += '/' 81 | 82 | for lang in settings.PAGE_LANGUAGES: 83 | if lang[0] != settings.PAGE_DEFAULT_LANGUAGE: 84 | stdout.write("Update language %s.\n" % lang[0]) 85 | po_path = path + lang[0] + '.po' 86 | po = polib.pofile(po_path) 87 | for entry in po: 88 | meta_data = entry.tcomment.split(do_not_msg)[1].split("\n") 89 | placeholder_name = meta_data[1].split('=')[1] 90 | page_id = int(meta_data[2].split('=')[1]) 91 | try: 92 | content_id = int(meta_data[3].split('=')[1]) 93 | except ValueError: 94 | content_id = None 95 | 96 | page = Page.objects.get(id=page_id) 97 | current_content = Content.objects.get_content(page, lang[0], 98 | placeholder_name) 99 | if current_content != entry.msgstr: 100 | stdout.write("Update page %d placeholder %s.\n" % (page_id, 101 | placeholder_name)) 102 | Content.objects.create_content_if_changed( 103 | page, lang[0], placeholder_name, entry.msgstr) 104 | if page not in pages_to_invalidate: 105 | pages_to_invalidate.append(page) 106 | 107 | for page in pages_to_invalidate: 108 | page.invalidate() 109 | stdout.write("Import finished from %s.\n" % path) -------------------------------------------------------------------------------- /pages/search_indexes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Django haystack `SearchIndex` module.""" 3 | from pages.models import Page 4 | from pages import settings 5 | 6 | from haystack.indexes import SearchIndex, CharField, DateTimeField, Indexable 7 | 8 | # This is obsolete if you use haystack 2.0, use the HAYSTACK_SIGNAL_PROCESSOR 9 | # setting instead 10 | if settings.PAGE_REAL_TIME_SEARCH: 11 | 12 | from haystack.indexes import RealTimeSearchIndex 13 | 14 | class RealTimePageIndex(RealTimeSearchIndex, Indexable): 15 | """Search index for pages content.""" 16 | text = CharField(document=True, use_template=True) 17 | title = CharField(model_attr='title') 18 | url = CharField(model_attr='get_absolute_url') 19 | publication_date = DateTimeField(model_attr='publication_date') 20 | 21 | def index_queryset(self, using=None): 22 | """Haystack 2.0 requires this method now""" 23 | return self.get_model().objects.published() 24 | 25 | def get_queryset(self): 26 | """Used when the entire index for model is updated.""" 27 | return Page.objects.published() 28 | 29 | def get_model(self): 30 | return Page 31 | 32 | def should_update(self, instance, **kwargs): 33 | return instance.status == Page.PUBLISHED 34 | 35 | else: 36 | 37 | class PageIndex(SearchIndex, Indexable): 38 | """Search index for pages content.""" 39 | text = CharField(document=True, use_template=True) 40 | title = CharField(model_attr='title') 41 | url = CharField(model_attr='get_absolute_url') 42 | publication_date = DateTimeField(model_attr='publication_date') 43 | 44 | def index_queryset(self, using=None): 45 | """Used when the entire index for model is updated.""" 46 | return Page.objects.published() 47 | 48 | def get_model(self): 49 | return Page 50 | 51 | def should_update(self, instance, **kwargs): 52 | return instance.status == Page.PUBLISHED 53 | 54 | -------------------------------------------------------------------------------- /pages/serializers.py: -------------------------------------------------------------------------------- 1 | from pages.models import Page, Content 2 | from rest_framework import serializers 3 | from django.contrib.auth import get_user_model 4 | from pages import settings 5 | from django.contrib.sites.models import Site 6 | 7 | 8 | class ContentSerializer(serializers.ModelSerializer): 9 | class Meta: 10 | model = Content 11 | fields = '__all__' 12 | 13 | 14 | class PageSerializer(serializers.ModelSerializer): 15 | content_set = ContentSerializer(many=True, read_only=True) 16 | creation_date = serializers.DateTimeField() 17 | uuid = serializers.UUIDField() 18 | 19 | class Meta: 20 | model = Page 21 | fields = '__all__' 22 | 23 | def create(self, validated_data): 24 | 25 | attributes = ( 26 | 'status', 'delegate_to', 'freeze_date', 'creation_date', 27 | 'publication_end_date', 'template', 'redirect_to_url', 28 | 'last_modification_date', 'publication_date', 'uuid') 29 | 30 | admin = get_user_model().objects.filter(is_superuser=True)[0] 31 | 32 | cleaned_data = {} 33 | for attribute in attributes: 34 | cleaned_data[attribute] = validated_data.get(attribute) 35 | 36 | if validated_data.get('parent', False): 37 | cleaned_data['parent'] = validated_data.get('parent') 38 | 39 | cleaned_data['author'] = admin 40 | 41 | page = Page.objects.create(**cleaned_data) 42 | if settings.PAGE_USE_SITE_ID: 43 | site = Site.objects.get_current() 44 | page.sites.add(site) 45 | 46 | page.invalidate() 47 | return page 48 | -------------------------------------------------------------------------------- /pages/static/pages/css/inline-edit.css: -------------------------------------------------------------------------------- 1 | 2 | [data-placeholder-name]:hover { 3 | outline: none; 4 | border-color: #88caed; 5 | box-shadow: 0 0 12px #88caed; 6 | cursor:pointer; 7 | } 8 | 9 | [data-placeholder-name] { 10 | border-color: #9ecaed; 11 | box-shadow: 0 0 5px #9ecaed; 12 | min-height: 12px; 13 | } 14 | 15 | #edit-overlay { 16 | position:absolute; 17 | background-color:#fff; 18 | box-shadow: 0 0 10px #9ecaed; 19 | padding:10px; 20 | } 21 | 22 | #edit-overlay form>p { 23 | display:none; 24 | } 25 | 26 | #edit-overlay input, #edit-overlay textarea { 27 | width: 80%; 28 | } 29 | 30 | #edit-overlay button { 31 | margin-left:4px; 32 | } 33 | 34 | #edit-overlay input[type=file] { 35 | border-width: 0; 36 | padding-left: 4px; 37 | } 38 | 39 | #edit-overlay form { 40 | margin:6px; 41 | } 42 | 43 | #edit-overlay label, #edit-overlay button { 44 | margin-bottom: .5rem; 45 | } 46 | 47 | #edit-overlay form label { 48 | display: flex; 49 | align-items: center; 50 | } 51 | 52 | #edit-overlay input[type=checkbox] { 53 | width: 2em; 54 | } 55 | 56 | #post-iframe { 57 | display:none; 58 | } 59 | 60 | #page-edit-bar { 61 | background-color: #fff; 62 | box-shadow: 0 4px 8px #bbb; 63 | padding: 8px; 64 | bottom: 10px; 65 | left: calc(50% - 150px); 66 | position: fixed; 67 | z-index:2000; 68 | } 69 | -------------------------------------------------------------------------------- /pages/static/pages/css/rte.css: -------------------------------------------------------------------------------- 1 | 2 | .rte { 3 | width:100%; 4 | } 5 | 6 | #frameBody { 7 | font-family: Roboto,"Lucida Grande","DejaVu Sans","Bitstream Vera Sans",Verdana,Arial,sans-serif; 8 | font-size:13px; 9 | font-weight:normal; 10 | margin:0; 11 | padding:8px; 12 | } 13 | 14 | #frameBody p { 15 | border-left:1px #bbb solid; 16 | padding-left:4px; 17 | } 18 | 19 | iframe.rte { 20 | width:98%; 21 | border:1px #ccc solid; 22 | } 23 | 24 | .rte-toolbar { 25 | overflow:hidden; 26 | width:100%; 27 | clear:both; 28 | margin-bottom:4px; 29 | } 30 | 31 | .rte-toolbar select { 32 | font-size:16px; 33 | padding: 5px 12px; 34 | } 35 | 36 | .rte-toolbar i { 37 | font-size:20px; 38 | margin: 1px 1px; 39 | display: inline-block; 40 | border: 1px solid #ccc; 41 | border-radius: 4px; 42 | padding: 5px 6px; 43 | vertical-align: bottom; 44 | } 45 | 46 | .rte-toolbar a, .rte-toolbar a img { 47 | border:0; 48 | } 49 | 50 | form .rte-toolbar p { 51 | float:left; 52 | margin:0; 53 | padding:0; 54 | padding-right:5px; 55 | } 56 | 57 | #frameBody #attributes { 58 | position:absolute; 59 | padding: 0.5em 0.4em; 60 | border:#bbb 1px solid; 61 | border-radius: 3px; 62 | z-index:1000; 63 | background: #fff; 64 | } 65 | 66 | #frameBody #attributes label { 67 | display:block; 68 | margin-bottom:4px; 69 | } 70 | 71 | #frameBody #attributes input { 72 | width:6em; 73 | } 74 | 75 | #frameBody #attributes label span { 76 | width:4em; 77 | display: inline-block; 78 | } 79 | 80 | #frameBody img, #frameBody #remove { 81 | cursor:pointer; 82 | } 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /pages/static/pages/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/static/pages/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /pages/static/pages/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/static/pages/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /pages/static/pages/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/static/pages/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /pages/static/pages/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/static/pages/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /pages/static/pages/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/static/pages/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /pages/static/pages/images/icons/add.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/static/pages/images/icons/add.gif -------------------------------------------------------------------------------- /pages/static/pages/images/icons/add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /pages/static/pages/images/icons/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pages/static/pages/images/icons/draft.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pages/static/pages/images/icons/edit.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/static/pages/images/icons/edit.gif -------------------------------------------------------------------------------- /pages/static/pages/images/icons/edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pages/static/pages/images/icons/hidden.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/static/pages/images/icons/hidden.gif -------------------------------------------------------------------------------- /pages/static/pages/images/icons/hidden.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pages/static/pages/images/icons/icons.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/static/pages/images/icons/icons.gif -------------------------------------------------------------------------------- /pages/static/pages/images/icons/insert.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/static/pages/images/icons/insert.gif -------------------------------------------------------------------------------- /pages/static/pages/images/icons/move.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/static/pages/images/icons/move.gif -------------------------------------------------------------------------------- /pages/static/pages/images/icons/published.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pages/static/pages/images/icons/redirect.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/static/pages/images/icons/redirect.gif -------------------------------------------------------------------------------- /pages/static/pages/images/icons/sort.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /pages/static/pages/images/icons/view.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/static/pages/images/icons/view.gif -------------------------------------------------------------------------------- /pages/static/pages/images/icons/view.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /pages/static/pages/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/static/pages/images/loading.gif -------------------------------------------------------------------------------- /pages/static/pages/images/rte/bold.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/static/pages/images/rte/bold.gif -------------------------------------------------------------------------------- /pages/static/pages/images/rte/close.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/static/pages/images/rte/close.gif -------------------------------------------------------------------------------- /pages/static/pages/images/rte/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/static/pages/images/rte/image.png -------------------------------------------------------------------------------- /pages/static/pages/images/rte/italic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/static/pages/images/rte/italic.gif -------------------------------------------------------------------------------- /pages/static/pages/images/rte/link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/static/pages/images/rte/link.png -------------------------------------------------------------------------------- /pages/static/pages/images/rte/unordered.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/static/pages/images/rte/unordered.gif -------------------------------------------------------------------------------- /pages/static/pages/img/icon_searchbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/static/pages/img/icon_searchbox.png -------------------------------------------------------------------------------- /pages/static/pages/javascript/iframe.rte.js: -------------------------------------------------------------------------------- 1 | 2 | $(function () { 3 | var attributes = $('
'+ 4 | ''+ 5 | ''+ 6 | ''+ 7 | '
'); 8 | var img, w, h; 9 | w = attributes.find('#width'); 10 | h = attributes.find('#height'); 11 | 12 | $(document).on('change keyup', '#width', function(e){ 13 | img.attr('width', w.val()); 14 | }); 15 | $(document).on('change keyup', '#height', function(e){ 16 | img.attr('height', h.val()); 17 | }); 18 | $(document).on('click', '#remove', function(e){ 19 | img.remove(); 20 | attributes.remove(); 21 | }); 22 | 23 | $(document).on('click', 'img', function(e) { 24 | img = $(e.target); 25 | w.val(img.attr('width')); 26 | h.val(img.attr('height')); 27 | attributes.css('top', e.pageY); 28 | attributes.css('left', e.pageX); 29 | $(document.body).append(attributes); 30 | e.preventDefault(); 31 | return false; 32 | }); 33 | 34 | $(document).on('click', '#attributes', function(e) { 35 | e.preventDefault(); 36 | return false; 37 | }); 38 | 39 | $(document).on('click', function(e) { 40 | attributes.remove(); 41 | }); 42 | 43 | }); -------------------------------------------------------------------------------- /pages/static/pages/javascript/jquery.ui.js: -------------------------------------------------------------------------------- 1 | eval(function(p,a,c,k,e,d){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('(3(C){C.a={1N:{19:3(E,F,H){6 G=C.a[E].j;1d(6 D 1O H){G.w[D]=G.w[D]||[];G.w[D].1P([F,H[D]])}},1b:3(D,F,E){6 H=D.w[F];5(!H){4}1d(6 G=0;G\').1i(D).f({1J:"1K",X:"-1f",1Q:"-1f",1R:"1X"}).1Y("13");C.a.n[D]=!!((!(/1Z|1W/).g(E.f("1V"))||(/^[1-9]/).g(E.f("1S"))||(/^[1-9]/).g(E.f("1G"))||!(/14/).g(E.f("1U"))||!(/20|1C\\(0, 0, 0, 0\\)/).g(E.f("1y"))));1w{C("13").1a(0).1z(E.1a(0))}1F(F){}4 C.a.n[D]},1A:3(D){D.l="1g";D.17=3(){4 7};5(D.r){D.r.15="14"}},1D:3(D){D.l="1B";D.17=3(){4 d};5(D.r){D.r.15=""}},1E:3(G,E){6 D=/X/.g(E||"X")?"23":"21",F=7;5(G[D]>0){4 d}G[D]=1;F=G[D]>0?d:7;G[D]=0;4 F}};6 B=C.P.p;C.P.p=3(){C("*",2).19(2).2m("p");4 B.q(2,1c)};3 A(E,F,G){6 D=C[E][F].2j||[];D=(R D=="N"?D.V(/,?\\s+/):D);4(C.2i(G,D)!=-1)}C.k=3(E,D){6 F=E.V(".")[0];E=E.V(".")[1];C.P[E]=3(J){6 H=(R J=="N"),I=2n.j.2o.1b(1c,1);5(H&&A(F,E,J)){6 G=C.M(2[0],E);4(G?G[J].q(G,I):2p)}4 2.2g(3(){6 K=C.M(2,E);5(H&&K&&C.2f(K[J])){K[J].q(K,I)}27{5(!H){C.M(2,E,24 C[F][E](2,J))}}})};C[F][E]=3(I,H){6 G=2;2.e=E;2.16=F+"-"+E;2.b=C.1e({},C.k.u,C[F][E].u,H);2.8=C(I).h("i."+E,3(L,J,K){4 G.i(J,K)}).h("O."+E,3(K,J){4 G.O(J)}).h("p",3(){4 G.1h()});2.18()};C[F][E].j=C.1e({},C.k.j,D)};C.k.j={18:3(){},1h:3(){2.8.2q(2.e)},O:3(D){4 2.b[D]},i:3(D,E){2.b[D]=E;5(D=="o"){2.8[E?"1i":"29"](2.16+"-o")}},22:3(){2.i("o",7)},25:3(){2.i("o",d)}};C.k.u={o:7};C.a.1n={2r:3(){6 D=2;2.8.h("2h."+2.e,3(E){4 D.1t(E)});5(C.z.y){2.1v=2.8.x("l");2.8.x("l","1g")}2.2l=7},2k:3(){2.8.S("."+2.e);(C.z.y&&2.8.x("l",2.1v))},1t:3(F){(2.c&&2.m(F));2.v=F;6 E=2,G=(F.26==1),D=(R 2.b.Y=="N"?C(F.28).2c(2.b.Y):7);5(!G||D||!2.1m(F)){4 d}2.t=!2.b.Q;5(!2.t){2.2b=2a(3(){E.t=d},2.b.Q)}5(2.T(F)&&2.U(F)){2.c=(2.Z(F)!==7);5(!2.c){F.2d();4 d}}2.10=3(H){4 E.1o(H)};2.12=3(H){4 E.m(H)};C(1q).h("1j."+2.e,2.10).h("1u."+2.e,2.12);4 7},1o:3(D){5(C.z.y&&!D.2s){4 2.m(D)}5(2.c){2.11(D);4 7}5(2.T(D)&&2.U(D)){2.c=(2.Z(2.v,D)!==7);(2.c?2.11(D):2.m(D))}4!2.c},m:3(D){C(1q).S("1j."+2.e,2.10).S("1u."+2.e,2.12);5(2.c){2.c=7;2.1k(D)}4 7},T:3(D){4(W.1T(W.1s(2.v.1r-D.1r),W.1s(2.v.1p-D.1p))>=2.b.1l)},U:3(D){4 2.t},Z:3(D){},11:3(D){},1k:3(D){},1m:3(D){4 d}};C.a.1n.u={Y:1x,1l:1,Q:0}})(2e);',62,153,'||this|function|return|if|var|false|element||ui|options|_mouseStarted|true|widgetName|css|test|bind|setData|prototype|widget|unselectable|mouseUp|cssCache|disabled|remove|apply|style||_mouseDelayMet|defaults|_mouseDownEvent|plugins|attr|msie|browser|||||||||||||data|string|getData|fn|delay|typeof|unbind|mouseDistanceMet|mouseDelayMet|split|Math|top|cancel|mouseStart|_mouseMoveDelegate|mouseDrag|_mouseUpDelegate|body|none|MozUserSelect|widgetBaseClass|onselectstart|init|add|get|call|arguments|for|extend|5000px|on|destroy|addClass|mousemove|mouseStop|distance|mouseCapture|mouse|mouseMove|pageY|document|pageX|abs|mouseDown|mouseup|_mouseUnselectable|try|null|backgroundColor|removeChild|disableSelection|off|rgba|enableSelection|hasScroll|catch|width|class|gen|position|absolute|div|length|plugin|in|push|left|display|height|max|backgroundImage|cursor|default|block|appendTo|auto|transparent|scrollLeft|enable|scrollTop|new|disable|which|else|target|removeClass|setTimeout|_mouseDelayTimer|is|preventDefault|jQuery|isFunction|each|mousedown|inArray|getter|mouseDestroy|started|trigger|Array|slice|undefined|removeData|mouseInit|button'.split('|'),0,{})) 2 | -------------------------------------------------------------------------------- /pages/templates/admin/pages/media/change_list.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_list.html" %} 2 | 3 | {% load admin_list i18n static %} 4 | 5 | {% block extrahead %}{{ block.super }} 6 | 7 | 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /pages/templates/admin/pages/page/change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %} 2 | {% load i18n admin_modify pages_tags static admin_urls %} 3 | 4 | {% block title %}{% trans "Edit" %}{% endblock %} 5 | 6 | {% block extrahead %}{{ block.super }} 7 | 8 | 9 | {% endblock %} 10 | 11 | {% block content %} 12 | 13 |
14 | 15 | {% if change %}{% if not is_popup %} 16 |
    17 | {% block object-tools-items %} 18 | {% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %} 19 |
  • {% trans "History" %}
  • 20 | {% if has_absolute_url %}
  • {% trans "View on site" %}
  • {% endif%} 21 | {% endblock %} 22 |
23 | {% endif %}{% endif %} 24 | 25 | 26 |
27 | {% csrf_token %} 28 | 36 |
37 | {% endif %} 38 | {% endfor %} 39 | 40 | 41 |
42 | {% for inline_admin_formset in inline_admin_formsets %} 43 | {% include inline_admin_formset.opts.template %} 44 | {% endfor %} 45 |
46 | 47 |
{% comment %}Close content{% endcomment %} 48 | 49 | {% submit_row %} 50 |
51 |
52 | 55 | 56 | {% endblock %} 57 | -------------------------------------------------------------------------------- /pages/templates/admin/pages/page/change_list.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_list.html" %} 2 | 3 | {% load admin_list i18n pages_tags static %} 4 | 5 | {% block coltype %}flex{% endblock %} 6 | 7 | {% block extrahead %}{{ block.super }} 8 | 9 | {% endblock %} 10 | 11 | {% block bodyclass %}{{ block.super }} change-list-pages{% endblock %} 12 | 13 | {% block result_list %} 14 |
15 | 23 |
24 | 25 |
26 | {% csrf_token %} 27 | {% if action_form and actions_on_top and cl.full_result_count %}{% admin_actions %}{% endif %} 28 | 29 |
30 | {% include "admin/pages/page/change_list_table.html" %} 31 |
32 | {% if action_form and actions_on_bottom and cl.full_result_count %}{% admin_actions %}{% endif %} 33 |
34 | 35 | {% endblock %} 36 | 37 | {% block pagination %}{% endblock %} 38 | -------------------------------------------------------------------------------- /pages/templates/admin/pages/page/change_list_table.html: -------------------------------------------------------------------------------- 1 | {% load i18n pages_tags static %} 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {% for page in pages %} 19 | {% pages_admin_menu page %} 20 | {% endfor %} 21 | 22 |
7 |
8 |
{% trans "title" %}
{% trans "languages" %}
{% trans "last modification" %}
{% trans "published" %}
{% trans "template" %}
{% trans "author" %}
23 | 24 |
25 | 26 | 31 | -------------------------------------------------------------------------------- /pages/templates/admin/pages/page/includes/fieldset.html: -------------------------------------------------------------------------------- 1 | {% load i18n pages_tags %} 2 |
3 | {% if fieldset.name %}

{{ fieldset.name }}

{% endif %} 4 | {% if fieldset.description %}
{{ fieldset.description|safe }}
{% endif %} 5 | 6 | {% for line in fieldset %} 7 |
8 | {% for field in line %} 9 |
10 | {{ field.errors }} 11 | {% if field.is_checkbox %} 12 | {{ field.field }}{{ field.label_tag }} 13 | {% else %} 14 | {{ field.label_tag }} 15 | {% if field.is_readonly %} 16 |

{{ field.contents }}

17 | {% else %} 18 | {{ field.field }} 19 | {% endif %} 20 | {% endif %} 21 | {% if field.field.help_text %} 22 |

{{ field.field.help_text|safe }}

23 | {% endif %} 24 | {% for p in placeholders %} 25 | {% ifequal p.ctype field.field.name %} 26 | {% if not add %} 27 | {% show_revisions page p.ctype language %} 28 | {% endif %} 29 | {% endifequal %} 30 | {% endfor %} 31 |
32 | {% endfor %} 33 |
34 | {% endfor %} 35 |
36 | -------------------------------------------------------------------------------- /pages/templates/admin/pages/page/sub_menu.html: -------------------------------------------------------------------------------- 1 | {% load pages_tags %} 2 | {% for page in pages %} 3 | {% include "admin/pages/page/menu.html" %} 4 | {% endfor %} -------------------------------------------------------------------------------- /pages/templates/pages/breadcrumb.html: -------------------------------------------------------------------------------- 1 | {% load pages_tags cache %} 2 | {% for page in pages_navigation %} 3 | {% show_content page "title" %} » 4 | {% endfor %}{% show_content page "title" %} -------------------------------------------------------------------------------- /pages/templates/pages/contact.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 |
3 | {{ form.as_p }} 4 | 5 |
-------------------------------------------------------------------------------- /pages/templates/pages/content.html: -------------------------------------------------------------------------------- 1 | {{ content|safe }} -------------------------------------------------------------------------------- /pages/templates/pages/dynamic_tree_menu.html: -------------------------------------------------------------------------------- 1 | {% load pages_tags cache %} 2 | {% if page.calculated_status %} 3 |
  • 4 | {% show_content page "title" %} 5 | {% if children %} 6 |
      {% for child in children %}{% pages_dynamic_tree_menu child url %}{% endfor %}
    7 | {% endif %} 8 |
  • 9 | {% endif %} -------------------------------------------------------------------------------- /pages/templates/pages/embed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /pages/templates/pages/examples/ckeditor.html: -------------------------------------------------------------------------------- 1 | {% extends "pages/examples/index.html" %} 2 | {% load ckeditor_placeholder %} 3 | {% load pages_tags %} 4 | 5 | {% block title %}CKEditor template{% endblock %} 6 | 7 | {% block content %} 8 |
    9 |
    content 10 |
    11 | {% ckeditor_placeholder "welcome_text" with ckeditor:minimal %} 12 |
    13 |
    14 |
    15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /pages/templates/pages/examples/cool.html: -------------------------------------------------------------------------------- 1 | {% extends "pages/examples/index.html" %} 2 | {% load pages_tags %} 3 | 4 | {% block title %}Cool template{% endblock %} 5 | 6 | {% block content %}{{ block.super }} 7 |
    8 |
    fancy content 9 |
    10 | {% placeholder fancy-content %} 11 |
    12 | {% placeholder "hello world" %} 13 | {% placeholder "hello" %} 14 |
    15 |
    16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /pages/templates/pages/examples/debug.html: -------------------------------------------------------------------------------- 1 | {% if sql_queries %} 2 | Show SQL queries 3 |
    4 |

    5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% for query in sql_queries %} 23 | 24 | 25 | 26 | 27 | 28 | {% endfor %} 29 | 30 |
    #SQLTime
    {{ sql_queries|length }}
    {{ forloop.counter }}{{ query.sql|escape }}{{ query.time }}
    31 |
    32 | 39 | {% endif %} 40 | -------------------------------------------------------------------------------- /pages/templates/pages/examples/files.html: -------------------------------------------------------------------------------- 1 | {% extends "pages/examples/index.html" %} 2 | {% load pages_tags %} 3 | 4 | {% block title %}Cool template{% endblock %} 5 | 6 | {% block content %} 7 |
    8 |
    HTML Content editor 9 |
    10 | {% fileplaceholder file1 %} 11 | {% fileplaceholder file2 %} 12 | {% fileplaceholder file3 %} 13 |
    14 |
    15 |
    16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /pages/templates/pages/examples/nice.html: -------------------------------------------------------------------------------- 1 | {% extends "pages/examples/index.html" %} 2 | {% load pages_tags %} 3 | 4 | {% block title %}Nice template{% endblock %} 5 | 6 | {% block content %}{{ block.super }} 7 |
    8 |
    fancy content 9 |
    10 | {% placeholder fancy-content %} 11 |
    12 |
    13 | {% placeholder right-column with Textarea parsed as right_column %} 14 | 15 | {% markdownplaceholder mark %} 16 |
    17 | 18 | {% endblock %} 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /pages/templates/pages/inline-edit.html: -------------------------------------------------------------------------------- 1 | {% load static admin_urls %} 2 |
    3 | Logout 4 | Edit in admin 5 | {% if request.COOKIES.enable_edit_mode %} 6 | 7 | {% else %} 8 | 9 | {% endif %} 10 |
    11 | 12 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 113 | -------------------------------------------------------------------------------- /pages/templates/pages/menu.html: -------------------------------------------------------------------------------- 1 | {% load pages_tags cache %} 2 | {% if page.status %} 3 |
  • 4 | {% show_content page "title" %} {% ifequal page current_page %}selected{% endifequal %} 5 | {% if children %} 6 |
      7 | {% for child in children %} 8 | {% pages_menu child %} 9 | {% endfor %} 10 |
    11 | {% endif %} 12 |
  • 13 | {% endif %} 14 | -------------------------------------------------------------------------------- /pages/templates/pages/revisions.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% if revisions %} 3 | 9 | {% endif %} 10 | -------------------------------------------------------------------------------- /pages/templates/pages/sub_menu.html: -------------------------------------------------------------------------------- 1 | {% load pages_tags %} 2 | {% for child in children %} 3 | {% pages_menu child %} 4 | {% endfor %} 5 | 6 | -------------------------------------------------------------------------------- /pages/templates/pages/tests/auto_render.txt: -------------------------------------------------------------------------------- 1 | template_name: '{{template_name}}', only_context: '{{only_context}}' 2 | -------------------------------------------------------------------------------- /pages/templates/pages/tests/auto_render2.txt: -------------------------------------------------------------------------------- 1 | alternate template_name: '{{template_name}}', only_context: '{{only_context}}' 2 | -------------------------------------------------------------------------------- /pages/templates/pages/tests/base.html: -------------------------------------------------------------------------------- 1 | {% load pages_tags %} 2 | {% block parent %} 3 | {% block child %} 4 | {% placeholder one on current_page %} 5 | {% placeholder two on current_page %} 6 | {% endblock %} 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /pages/templates/pages/tests/block.html: -------------------------------------------------------------------------------- 1 | {% load pages_tags %} 2 | {% block content %} 3 |
    4 | {% placeholder body %} 5 |
    6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /pages/templates/pages/tests/block2.html: -------------------------------------------------------------------------------- 1 | {% extends "pages/tests/block.html" %} 2 | {% load pages_tags %} 3 | {% block content %} 4 | {% if 1 == 1 %} 5 | {{ block.super }} 6 | {% endif %} 7 | {% placeholder body2 %} 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /pages/templates/pages/tests/block3.html: -------------------------------------------------------------------------------- 1 | {% extends "pages/tests/block.html" %} 2 | {% load pages_tags %} 3 | {% block toto %}{% endblock %} 4 | {% block content %} 5 |
    6 | {% placeholder test %} 7 |
    8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /pages/templates/pages/tests/extends.html: -------------------------------------------------------------------------------- 1 | {% extends "pages/tests/base.html" %} 2 | {% load pages_tags %} 3 | {% block parent %} 4 | {% block child %} 5 | {% placeholder one on current_page %} 6 | {% placeholder two on current_page %} 7 | {% endblock %} 8 | {% endblock %} -------------------------------------------------------------------------------- /pages/templates/pages/tests/fileinput.html: -------------------------------------------------------------------------------- 1 | {% load pages_tags %} 2 | 3 | {% placeholder "file" with FileInput %} 4 | -------------------------------------------------------------------------------- /pages/templates/pages/tests/test1.html: -------------------------------------------------------------------------------- 1 | {% load pages_tags %} 2 | {% load_pages %} 3 | {% show_content current_page "title" %} 4 | {% placeholder body with RichTextarea %} 5 | -------------------------------------------------------------------------------- /pages/templates/pages/tests/test2.html: -------------------------------------------------------------------------------- 1 | {% load cache pages_tags %} 2 | {% load_pages %} 3 | {% for page in pages_navigation %} 4 | {{ page.slug }} 5 | {% endfor %} 6 | 7 | -------------------------------------------------------------------------------- /pages/templates/pages/tests/test3.html: -------------------------------------------------------------------------------- 1 | {% load cache pages_tags %} 2 | 3 | {% pages_breadcrumb page %} 4 | 5 | {% pages_dynamic_tree_menu page %} 6 | 7 | -------------------------------------------------------------------------------- /pages/templates/pages/tests/test4.html: -------------------------------------------------------------------------------- 1 | {% load pages_tags %} 2 | 3 | {% show_content 1 "title" %} 4 | 5 | {% show_content "1" "title" %} 6 | 7 | -------------------------------------------------------------------------------- /pages/templates/pages/tests/test5.html: -------------------------------------------------------------------------------- 1 | {% load pages_tags %} 2 | {% load_pages %} 3 | 4 | {% pages_menu page %} 5 | 6 | {% pages_sub_menu page %} 7 | 8 | {% pages_breadcrumb page %} 9 | -------------------------------------------------------------------------------- /pages/templates/pages/tests/test6.html: -------------------------------------------------------------------------------- 1 | {% load pages_tags %} 2 | 3 | t1_{% url "pages-details-by-path" current_page.get_complete_slug %} 4 | t2_{% show_absolute_url current_page %} 5 | t3_{% show_absolute_url current_page.slug %} 6 | t4_{% show_content current_page.slug "slug" %} 7 | {% get_content current_page.slug "slug" as content %} 8 | t5_{{ content }} 9 | -------------------------------------------------------------------------------- /pages/templates/pages/tests/test7.html: -------------------------------------------------------------------------------- 1 | {% load pages_tags %}{% placeholder inher inherited %} -------------------------------------------------------------------------------- /pages/templates/pages/tests/untranslated.html: -------------------------------------------------------------------------------- 1 | {% load pages_tags %}{% placeholder untrans untranslated %} -------------------------------------------------------------------------------- /pages/templates/pages/traduction_helper.html: -------------------------------------------------------------------------------- 1 | {% load pages_tags i18n %} 2 | 3 | {% if language_error %}

    4 | {% trans "Wanted language has not been translated yet." %}

    5 | {% else %} 6 | {% for p in placeholders %} 7 |

    {{ p.name }}

    8 |
    {% show_content page p.ctype lang 0 %}
    9 | {% endfor %} 10 | {% endif %} -------------------------------------------------------------------------------- /pages/templates/pages/widgets/file_input.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 4 | please_save_msg = _('Please save the page to show the file field') 5 | delete_msg = _('Delete file') 6 | 7 | def render(self, name, value, attrs=None, **kwargs): 8 | if not self.page: 9 | field_content = self.please_save_msg 10 | else: 11 | field_content = '' 12 | if value: 13 | field_content += _('Current file: %s
    ') % value 14 | field_content += super(FileInput, self).render(name, attrs) 15 | if value: 16 | field_content += '''
    17 | 19 | ''' % (name, self.delete_msg, name, name) 20 | 21 | 22 |
    23 | -------------------------------------------------------------------------------- /pages/templates/pages/widgets/languages.html: -------------------------------------------------------------------------------- 1 | {% load i18n pages_tags %} 2 |
    3 | 4 |
      5 | {% for lang in page_languages %} 6 | {% ifequal value lang.0 %} 7 |
    • {{ lang.0 }}
    • 8 | {% else %} 9 | {% if page|has_content_in:lang.0 %} 10 |
    • {{ lang.0 }}
    • 11 | {% else %} 12 |
    • {{ lang.0 }}
    • 13 | {% endif %} 14 | {% endifequal %} 15 | {% endfor %} 16 |
    17 | {% if page|has_content_in:value %} 18 |

    {% trans "Delete this translation" %}

    20 | {% endif %} 21 |
    22 | -------------------------------------------------------------------------------- /pages/templates/pages/widgets/richtextarea.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /pages/templates/search/indexes/pages/page_text.txt: -------------------------------------------------------------------------------- 1 | {{ object.expose_content }} -------------------------------------------------------------------------------- /pages/templates/search/search.html: -------------------------------------------------------------------------------- 1 | {% extends 'pages/examples/index.html' %} 2 | 3 | {% block content %} 4 | 5 |
    6 | 7 | {{ form.as_table }} 8 | 9 | 10 | 13 | 14 |
      11 | 12 |
    15 | 16 | {% if query %} 17 |

    Search results

    18 | 19 | {% for result in page.object_list %} 20 |

    21 | {{ result.object.title }} 22 |

    23 | {% empty %} 24 |

    No results found. Did you build the search index? (python manage.py rebuild_index)

    25 | {% endfor %} 26 | {% else %} 27 | {# Show some example queries to run, maybe query syntax, something else? #} 28 | {% endif %} 29 |
    30 | {% endblock %} -------------------------------------------------------------------------------- /pages/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | """Page CMS template tag module""" -------------------------------------------------------------------------------- /pages/templatetags/ckeditor_placeholder.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.forms import Textarea 3 | from django.utils import translation 4 | from django.conf import settings 5 | from django.contrib.admin.widgets import AdminTextareaWidget 6 | 7 | from pages.placeholders import PlaceholderNode 8 | from pages.placeholders import parse_placeholder 9 | 10 | try: 11 | from ckeditor.widgets import CKEditorWidget 12 | except ImportError: 13 | CKEditorWidget = AdminTextareaWidget 14 | 15 | register = template.Library() 16 | 17 | class CKEditorPlaceholderNode(PlaceholderNode): 18 | def get_widget(self, page, language, fallback=Textarea): 19 | if 'ckeditor' not in settings.INSTALLED_APPS: 20 | return fallback 21 | 22 | with_stmt = self.widget # name of the widget as called in template like... 23 | # {% ckeditor_placeholder "welcome" with text_wysiwym_widget:default%} 24 | splitted = with_stmt.split(":") 25 | 26 | if len(splitted) == 1: 27 | ck = CKEditorWidget(config_name='default') 28 | elif len(splitted) > 1: 29 | ck = CKEditorWidget(config_name=splitted[1]) 30 | 31 | if not ck.config.get('language'): 32 | ck.config['language'] = translation.get_language() 33 | 34 | return ck 35 | 36 | def do_ckeditorplaceholder(parser, token): 37 | name, params = parse_placeholder(parser, token) 38 | return CKEditorPlaceholderNode(name, **params) 39 | 40 | register.tag('ckeditor_placeholder', do_ckeditorplaceholder) 41 | -------------------------------------------------------------------------------- /pages/test_runner.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pages.testproj.test_settings") 5 | current_dirname = os.path.dirname(__file__) 6 | sys.path.insert(0, os.path.join(current_dirname, '..')) 7 | 8 | import django 9 | django.setup() 10 | 11 | from django.test.runner import DiscoverRunner 12 | from django.core.management import call_command 13 | 14 | class PageTestSuiteRunner(DiscoverRunner): 15 | 16 | def run_tests(self, test_labels=('pages',), extra_tests=None): 17 | call_command('collectstatic', '--noinput') 18 | results = DiscoverRunner.run_tests(self, test_labels, extra_tests) 19 | sys.exit(results) 20 | 21 | if __name__ == '__main__': 22 | runner = PageTestSuiteRunner(failfast=True) 23 | if len(sys.argv) > 1: 24 | runner.run_tests(test_labels=(sys.argv[1], )) 25 | else: 26 | runner.run_tests() 27 | -------------------------------------------------------------------------------- /pages/testproj/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/testproj/__init__.py -------------------------------------------------------------------------------- /pages/testproj/documents/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/testproj/documents/__init__.py -------------------------------------------------------------------------------- /pages/testproj/documents/admin.py: -------------------------------------------------------------------------------- 1 | # Admin bindings 2 | from django.contrib import admin 3 | from pages.testproj.documents.models import Document 4 | 5 | 6 | class DocumentAdmin(admin.ModelAdmin): 7 | 8 | list_display = ('title', 'page',) 9 | 10 | admin.site.register(Document, DocumentAdmin) 11 | -------------------------------------------------------------------------------- /pages/testproj/documents/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.forms import ModelForm 3 | from pages.models import Page 4 | from django.utils.translation import ugettext_lazy as _ 5 | 6 | 7 | class Document(models.Model): 8 | "A dummy model used to illustrate the use of linked models in django-page-cms" 9 | 10 | title = models.CharField(_('title'), max_length=100, blank=False) 11 | text = models.TextField(_('text'), blank=True) 12 | 13 | # the foreign key _must_ be called page 14 | page = models.ForeignKey(Page, on_delete=models.CASCADE) 15 | 16 | 17 | class DocumentForm(ModelForm): 18 | class Meta: 19 | model = Document 20 | fields = "__all__" 21 | -------------------------------------------------------------------------------- /pages/testproj/documents/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from pages.testproj.documents.views import document_view 3 | 4 | urlpatterns = [ 5 | url(r'^doc-(?P[0-9]+)$', document_view, name='document_details'), 6 | url(r'^$', document_view, name='document_root'), 7 | ] 8 | -------------------------------------------------------------------------------- /pages/testproj/documents/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from pages.testproj.documents.models import Document 3 | 4 | 5 | def document_view(request, *args, **kwargs): 6 | context = kwargs 7 | if kwargs.get('current_page', False): 8 | documents = Document.objects.filter(page=kwargs['current_page']) 9 | context['documents'] = documents 10 | if 'document_id' in kwargs: 11 | document = Document.objects.get(pk=int(kwargs['document_id'])) 12 | context['document'] = document 13 | context['in_document_view'] = True 14 | 15 | return render( 16 | request, 'pages/examples/index.html', 17 | context) 18 | -------------------------------------------------------------------------------- /pages/testproj/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os, sys 3 | current_dirname = os.path.dirname(__file__) 4 | sys.path.insert(0, os.path.join(current_dirname, '..')) 5 | sys.path.insert(0, os.path.join(current_dirname, '../..')) 6 | 7 | if __name__ == "__main__": 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_settings") 9 | 10 | from django.core.management import execute_from_command_line 11 | 12 | execute_from_command_line(sys.argv) 13 | -------------------------------------------------------------------------------- /pages/testproj/search_sites.py: -------------------------------------------------------------------------------- 1 | import haystack 2 | haystack.autodiscover() -------------------------------------------------------------------------------- /pages/testproj/templates/404.html: -------------------------------------------------------------------------------- 1 | {% load cache pages_tags %} 2 | 3 | 4 | 5 | Page not found. 6 | 7 | 8 | 9 |

    Page not found.

    10 | 11 | 12 | -------------------------------------------------------------------------------- /pages/testproj/templates/block_placeholder.html: -------------------------------------------------------------------------------- 1 | {% load pages_tags %} 2 | {% placeholder "block 1" block %} 3 | {% if content %} 4 | block 1:{{ content }}; 5 | {% endif %} 6 | {% endplaceholder %} 7 | 8 | {% placeholder "block 2" block %} 9 | block 2:{{ content }}; 10 | {% endplaceholder %} -------------------------------------------------------------------------------- /pages/testproj/templates/slow_template_parsing.html: -------------------------------------------------------------------------------- 1 | {% load pages_tags %} 2 | 3 | {% placeholder name1 %} 4 | 5 | {% placeholder name2 %} 6 | 7 | {% placeholder name3 %} 8 | 9 | {% placeholder name4 %} 10 | 11 | {% placeholder name5 %} 12 | 13 | {% placeholder name6 block %} 14 | 15 | {% endplaceholder %} 16 | 17 | {% placeholder name7 %} 18 | 19 | {% placeholder name8 block %} 20 | 21 | {% endplaceholder %} 22 | 23 | {% placeholder name9 %} 24 | 25 | {% placeholder name10 %} 26 | 27 | {% placeholder name11 %} 28 | 29 | {% placeholder name12 %} 30 | 31 | {% placeholder name13 block %} 32 | 33 | {% endplaceholder %} -------------------------------------------------------------------------------- /pages/testproj/templates/syntax_error.html: -------------------------------------------------------------------------------- 1 | {% I'm not a template tag. %} 2 | -------------------------------------------------------------------------------- /pages/testproj/urls.py: -------------------------------------------------------------------------------- 1 | 2 | from django.conf.urls import url, include 3 | from django.conf.urls import handler404, handler500 4 | from django.contrib import admin 5 | from django.conf import settings 6 | from django.contrib.sitemaps.views import sitemap 7 | from pages.views import PageSitemap, MultiLanguagePageSitemap 8 | 9 | 10 | admin.autodiscover() 11 | 12 | urlpatterns = [ 13 | url(r'^i18n/', include('django.conf.urls.i18n')), 14 | 15 | url(r'^pages/', include('pages.urls')), 16 | 17 | # this is only used to enable the reverse url to work with documents 18 | url(r'^pages/(?P.*)', include('pages.testproj.documents.urls')), 19 | 20 | url(r'^admin/', admin.site.urls), 21 | # make tests fail if a backend is not present on the system 22 | #(r'^search/', include('haystack.urls')), 23 | 24 | url(r'^sitemap\.xml$', sitemap, 25 | {'sitemaps': {'pages':PageSitemap}}), 26 | 27 | url(r'^sitemap2\.xml$', sitemap, 28 | {'sitemaps': {'pages':MultiLanguagePageSitemap}}) 29 | ] 30 | -------------------------------------------------------------------------------- /pages/testproj/views.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-page-cms/7dfa24df4d30525a0e8d060f23e5f6f91e47bfb4/pages/testproj/views.py -------------------------------------------------------------------------------- /pages/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Django page CMS test suite module""" 2 | -------------------------------------------------------------------------------- /pages/tests/test_checks.py: -------------------------------------------------------------------------------- 1 | from pages.checks import page_templates_loading_check 2 | 3 | from django.test import TestCase 4 | from django.core.checks import Warning 5 | from django.template import TemplateSyntaxError 6 | 7 | 8 | class PageTemplatesLoadingCheckTestCase(TestCase): 9 | def test_check_detects_unexistant_template(self): 10 | unexistant = ('does_not_exists.html', 'foo') 11 | with self.settings(PAGE_TEMPLATES=[unexistant]): 12 | errors = page_templates_loading_check([]) 13 | 14 | self.assertEqual(errors, [Warning( 15 | 'Django cannot find template does_not_exists.html', 16 | obj=unexistant, id='pages.W001')]) 17 | 18 | def test_check_doesnt_warn_on_existing_templates(self): 19 | with self.settings(PAGE_TEMPLATES=[('pages/contact.html', 'bas')]): 20 | errors = page_templates_loading_check([]) 21 | 22 | self.assertEqual(errors, []) 23 | 24 | def test_template_syntax_error_is_not_silenced(self): 25 | with self.settings(PAGE_TEMPLATES=[('syntax_error.html', 'fail')]): 26 | with self.assertRaises(TemplateSyntaxError): 27 | page_templates_loading_check([]) 28 | -------------------------------------------------------------------------------- /pages/tests/test_commands.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Django page CMS command tests suite module.""" 3 | from pages.models import Page, Content, PageAlias 4 | from pages.tests.testcase import TestCase 5 | from django.core.management import call_command 6 | from django.test import LiveServerTestCase 7 | import json 8 | 9 | from django.test.testcases import LiveServerThread, QuietWSGIRequestHandler 10 | from django.core.servers.basehttp import WSGIServer 11 | 12 | class LiveServerSingleThread(LiveServerThread): 13 | """Runs a single threaded server rather than multi threaded. Reverts https://github.com/django/django/pull/7832""" 14 | 15 | def _create_server(self): 16 | return WSGIServer((self.host, self.port), QuietWSGIRequestHandler, allow_reuse_address=False) 17 | 18 | 19 | class LiveServerSingleThreadedTestCase(LiveServerTestCase): 20 | "A thin sub-class which only sets the single-threaded server as a class" 21 | server_thread_class = LiveServerSingleThread 22 | 23 | 24 | class CommandTestCase(TestCase, LiveServerSingleThreadedTestCase): 25 | """Django page CMS command tests suite class.""" 26 | 27 | def test_pull(self): 28 | """Pull command get the correct data""" 29 | self.new_page(content={'title': 'pull-page', 'slug': 'pull-slug'}) 30 | url = self.live_server_url + '/pages/api/' 31 | filename = '/tmp/test' 32 | call_command('pages_pull', 'admin:b', filename=filename, host=url, verbosity=0) 33 | with open(filename, "r") as f: 34 | data = f.read() 35 | pages = json.loads(data) 36 | for content in pages[0]['content_set']: 37 | self.assertTrue(content['body'] in ['pull-page', 'pull-slug']) 38 | 39 | def test_push(self): 40 | """Push command put back the content properly""" 41 | url = self.live_server_url + '/pages/api/' 42 | page1 = self.new_page(content={'title': 'pull-page', 'slug': 'pull-slug'}) 43 | page2 = self.new_page(content={'title': 'pull-page-2', 'slug': 'pull-slug-2'}) 44 | call_command('pages_pull', 'admin:b', filename='/tmp/test', host=url, verbosity=0) 45 | page1.delete() 46 | self.assertEqual(Page.objects.all().count(), 1) 47 | call_command('pages_push', 'admin:b', filename='/tmp/test', host=url, verbosity=0) 48 | self.assertEqual(Page.objects.all().count(), 2) 49 | 50 | def test_tree(self): 51 | """Push command" restore the tree properly""" 52 | url = self.live_server_url + '/pages/api/' 53 | filename = '/tmp/test' 54 | page1 = self.new_page(content={'title': 'pull-page-1', 'slug': 'pull-slug-1'}) 55 | page2 = self.new_page(content={'title': 'pull-page-2', 'slug': 'pull-slug-2'}, parent=page1) 56 | page3 = self.new_page(content={'title': 'pull-page-3', 'slug': 'pull-slug-3'}, parent=page2) 57 | 58 | self.assertSequenceEqual(page1.get_children(), [page2]) 59 | self.assertSequenceEqual(page2.get_children(), [page3]) 60 | 61 | call_command('pages_pull', 'admin:b', filename=filename, host=url, verbosity=0) 62 | page2.move_to(page1, 'left') 63 | 64 | self.assertSequenceEqual(page1.get_children(), []) 65 | self.assertSequenceEqual(page2.get_children(), [page3]) 66 | 67 | call_command('pages_push', 'admin:b', filename='/tmp/test', host=url, verbosity=0) 68 | 69 | self.assertSequenceEqual(page1.get_children(), [page2]) 70 | self.assertSequenceEqual(page2.get_children(), [page3]) 71 | 72 | def test_tree_delete(self): 73 | """Push command tree delete""" 74 | url = self.live_server_url + '/pages/api/' 75 | filename = '/tmp/test' 76 | page1 = self.new_page(content={'title': 'pull-page-1', 'slug': 'pull-slug-1'}) 77 | page2 = self.new_page(content={'title': 'pull-page-2', 'slug': 'pull-slug-2'}, parent=page1) 78 | page3 = self.new_page(content={'title': 'pull-page-3', 'slug': 'pull-slug-3'}, parent=page2) 79 | 80 | self.assertSequenceEqual(page1.get_children(), [page2]) 81 | self.assertSequenceEqual(page2.get_children(), [page3]) 82 | 83 | 84 | call_command('pages_pull', 'admin:b', filename=filename, host=url, verbosity=0) 85 | 86 | page4 = self.new_page(content={'title': 'pull-page-4', 'slug': 'pull-slug-4'}, parent=page1) 87 | self.assertSequenceEqual(page1.get_children(), [page2, page4]) 88 | page2.delete() 89 | self.assertSequenceEqual(page1.get_children(), [page4]) 90 | 91 | call_command('pages_push', 'admin:b', filename='/tmp/test', host=url, verbosity=0) 92 | 93 | page2 = Page.objects.from_slug('pull-slug-2') 94 | self.assertSequenceEqual(page1.get_children(), [page4, page2]) 95 | 96 | -------------------------------------------------------------------------------- /pages/tests/test_plugins.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Django page CMS plugin test suite module.""" 3 | from pages.tests.testcase import TestCase 4 | from pages.plugins.jsonexport import utils 5 | from pages.models import Page 6 | import json 7 | from pages.plugins.jsonexport.tests import JSONExportTestCase 8 | 9 | 10 | class Dummy(JSONExportTestCase): 11 | pass 12 | 13 | 14 | class PluginTestCase(TestCase): 15 | """Django page CMS plugin tests.""" 16 | 17 | def test_json_parsing(self): 18 | """Test page date ordering feature.""" 19 | self.new_page({'slug': 'p1'}) 20 | self.new_page({'slug': 'p2'}) 21 | jsondata = utils.pages_to_json(Page.objects.all()) 22 | 23 | self.assertIn("p1", jsondata) 24 | self.assertIn("p2", jsondata) 25 | data = json.loads(jsondata) 26 | self.assertEqual(len(data['pages']), 2) 27 | -------------------------------------------------------------------------------- /pages/tests/test_selenium.py: -------------------------------------------------------------------------------- 1 | # # -*- coding: utf-8 -*- 2 | # """Django page CMS selemium test module""" 3 | # from unittest import SkipTest 4 | 5 | # from django.contrib import auth 6 | # from django.urls import reverse 7 | # from django.test import LiveServerTestCase 8 | # from pages import settings 9 | # from pages.models import Page 10 | # from pages.tests.testcase import TestCase 11 | # from selenium import webdriver 12 | # from selenium.common.exceptions import TimeoutException 13 | # from selenium.webdriver.support.ui import WebDriverWait 14 | 15 | # screenshot_nb = 1 16 | # TIMEOUT = 10 17 | 18 | 19 | # class SeleniumTestCase(TestCase, LiveServerTestCase): 20 | 21 | # def setUp(self): 22 | # self.browser = webdriver.PhantomJS() 23 | # self.browser.set_page_load_timeout(TIMEOUT) 24 | # self.get_admin_client() 25 | 26 | # # setUp is where you instantiate the selenium webdriver and loads the browser. 27 | # auth.models.User.objects.create_superuser( 28 | # username='admin_s', 29 | # password='admin', 30 | # email='admin_s@example.com' 31 | # ) 32 | 33 | # self.browser.get('%s%s' % (self.live_server_url, reverse("admin:index"))) 34 | 35 | # super(SeleniumTestCase, self).setUp() 36 | 37 | # def screenshot(self): 38 | # global screenshot_nb 39 | # if settings.PAGE_TESTS_SAVE_SCREENSHOTS: 40 | # self.browser.save_screenshot('screenshot_%d.png' % screenshot_nb) 41 | # screenshot_nb += 1 42 | 43 | # def select_option(self, select, option_id): 44 | # for option in select.find_elements_by_tag_name('option'): 45 | # if option.get_attribute('value') == str(option_id): 46 | # option.click() 47 | 48 | # def visit(self, url): 49 | # # Open the django admin page. 50 | # # DjangoLiveServerTestCase provides a live server url attribute 51 | # # to access the base url in tests 52 | # url = '%s%s' % (self.live_server_url, url) 53 | # try: 54 | # return self.browser.get(url) 55 | # except TimeoutException: 56 | # raise SkipTest("Timeout: get({0})".format(repr(url))) 57 | 58 | # def find_element_by_css_selector(self, selector): 59 | # return self.timeout('find_element_by_css_selector', selector) 60 | 61 | # def find_elements_by_css_selector(self, selector): 62 | # return self.timeout('find_elements_by_css_selector', selector) 63 | 64 | # def find_element_by_id(self, id): 65 | # return self.timeout('find_element_by_id', id) 66 | 67 | # def timeout(self, command, param, timeout=TIMEOUT): 68 | # wait = WebDriverWait(self.browser, timeout) 69 | # try: 70 | # return wait.until(lambda b: getattr(b, command)(param)) 71 | # except TimeoutException: 72 | # raise SkipTest("Timeout: {0}({1})".format(command, repr(param))) 73 | 74 | # def click(self, selector): 75 | # return self.browser.find_element_by_css_selector(selector).click() 76 | 77 | # def login(self): 78 | # self.visit(reverse("admin:index")) 79 | # # Fill login information of admin 80 | # username = self.find_element_by_id("id_username") 81 | # username.send_keys("admin_s") 82 | # password = self.find_element_by_id("id_password") 83 | # password.send_keys("admin") 84 | # self.click("input[type='submit']") 85 | 86 | # def tearDown(self): 87 | # self.browser.close() 88 | # try: 89 | # self.browser.quit() 90 | # except OSError: 91 | # # http://stackoverflow.com/questions/42705674/python-selenium-phantomjs-quit-error 92 | # pass 93 | # super(SeleniumTestCase, self).tearDown() 94 | 95 | # def url_change(self, id): 96 | # return reverse('admin:pages_page_change', args=[id]) 97 | 98 | # def test_admin_select(self): 99 | # self.login() 100 | # page = self.new_page() 101 | # self.visit(self.url_change(page.id)) 102 | # status = self.find_element_by_id('id_status') 103 | # self.assertEqual(status.get_attribute('value'), str(page.status)) 104 | 105 | # self.select_option(status, str(Page.DRAFT)) 106 | # self.assertEqual(status.get_attribute('value'), str(Page.DRAFT)) 107 | 108 | # src = self.find_element_by_css_selector('.field-status img [src$="draft.gif"]') 109 | 110 | # def test_admin_move_page(self): 111 | # self.login() 112 | # page_1 = self.new_page({'slug': 'p1'}) 113 | # page_2 = self.new_page({'slug': 'p2'}) 114 | # self.visit(reverse('admin:pages_page_changelist')) 115 | 116 | # rows = self.find_elements_by_css_selector('#page-list tbody tr') 117 | # row_1 = rows[0] 118 | # row_2 = rows[1] 119 | 120 | # self.assertEqual(row_1.get_attribute('id'), 'page-row-%d' % page_1.id) 121 | # self.assertEqual(row_2.get_attribute('id'), 'page-row-%d' % page_2.id) 122 | 123 | # page_3 = self.new_page({'slug': 'p3'}) 124 | 125 | # self.click('#move-link-%d' % page_2.id) 126 | # self.click('#move-target-%d .move-target.left' % page_1.id) 127 | # self.visit(reverse('admin:pages_page_changelist')) 128 | 129 | # self.find_element_by_id('page-row-%d' % page_3.id) 130 | 131 | # def test_admin_export_json(self): 132 | # self.login() 133 | # self.new_page({'slug': 'p1'}) 134 | # self.new_page({'slug': 'p2'}) 135 | # self.visit(reverse('admin:pages_page_changelist')) 136 | 137 | # self.find_elements_by_css_selector('#action-toggle')[0].click() 138 | 139 | # action_select = self.find_elements_by_css_selector( 140 | # '[name="action"]')[0] 141 | # self.select_option(action_select, 'export_pages_as_json') 142 | 143 | # self.find_elements_by_css_selector('[name="index"]')[0].click() 144 | 145 | # # apparently there is no easy way to test a download? 146 | -------------------------------------------------------------------------------- /pages/tests/testcase.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from pages.cache import cache 3 | from pages.models import Page, Content 4 | from pages import settings as pages_settings 5 | from pages.testproj import test_settings 6 | from django.conf import settings 7 | from django.contrib.auth import get_user_model 8 | from django.urls import reverse, clear_url_caches 9 | from django.template import TemplateDoesNotExist, Engine 10 | from django.contrib.sites.models import Site 11 | from importlib import import_module, reload 12 | 13 | 14 | class MockRequest: 15 | 16 | def __init__(self): 17 | self.REQUEST = {'language': 'en'} 18 | self.GET = {} 19 | self.META = {} 20 | self.COOKIES = {} 21 | 22 | 23 | class Error404Expected(Exception): 24 | """ 25 | A 404 error was expected." 26 | """ 27 | pass 28 | 29 | 30 | def new_page(content={'title': 'test-page', 'slug': 'test-page-slug'}, 31 | parent=None, delegate_to=None, language='en', template='pages/examples/index.html'): 32 | author = get_user_model().objects.all()[0] 33 | page = Page.objects.create(author=author, status=Page.PUBLISHED, 34 | template=template, parent=parent, delegate_to=delegate_to) 35 | if pages_settings.PAGE_USE_SITE_ID: 36 | page.sites.add(Site.objects.get(id=1)) 37 | # necessary to clear old URL cache 38 | page.invalidate() 39 | for key, value in list(content.items()): 40 | Content(page=page, language='en', type=key, body=value).save() 41 | return page 42 | 43 | 44 | class TestCase(TestCase): 45 | """Django page CMS test suite class""" 46 | fixtures = ['pages_tests.json'] 47 | counter = 1 48 | settings_to_reset = {} 49 | old_url_conf = None 50 | 51 | def setUp(self): 52 | # useful to make sure the tests will be properly 53 | # executed in an exotic project. 54 | self.set_setting('PAGE_TEMPLATES', 55 | test_settings.PAGE_TEMPLATES) 56 | self.set_setting('PAGE_DEFAULT_TEMPLATE', 57 | test_settings.PAGE_DEFAULT_TEMPLATE) 58 | 59 | self.old_url_conf = getattr(settings, 'ROOT_URLCONF') 60 | setattr(settings, 'ROOT_URLCONF', 'pages.testproj.urls') 61 | clear_url_caches() 62 | cache.clear() 63 | 64 | def tearDown(self): 65 | setattr(settings, 'ROOT_URLCONF', self.old_url_conf) 66 | for name, value in list(self.settings_to_reset.items()): 67 | setattr(pages_settings, name, value) 68 | self.reset_urlconf() 69 | self.settings_to_reset = {} 70 | 71 | def set_setting(self, name, value): 72 | old_value = getattr(pages_settings, name) 73 | setattr(pages_settings, name, value) 74 | if name == 'PAGE_USE_LANGUAGE_PREFIX': 75 | self.reset_urlconf() 76 | if name not in self.settings_to_reset: 77 | self.settings_to_reset[name] = old_value 78 | 79 | def assert404(self, func): 80 | try: 81 | response = func() 82 | if response.status_code != 404: 83 | raise Error404Expected 84 | except TemplateDoesNotExist: 85 | pass 86 | 87 | def get_admin_client(self): 88 | from django.test.client import Client 89 | client = Client() 90 | client.login(username='admin', password='b') 91 | return client 92 | 93 | def get_page_url(self, path=''): 94 | return reverse('pages-details-by-path', args=[path]) 95 | 96 | def get_template_from_string(self, tpl): 97 | return Engine.get_default().from_string(tpl) 98 | 99 | def reset_urlconf(self): 100 | url_conf = getattr(settings, 'ROOT_URLCONF', False) 101 | if url_conf: 102 | try: 103 | reload(import_module(url_conf)) 104 | except: 105 | pass 106 | reload(import_module('pages.urls')) 107 | reload(import_module('pages.testproj.urls')) 108 | clear_url_caches() 109 | 110 | def get_new_page_data(self, draft=False): 111 | """Helper method for creating page datas""" 112 | page_data = { 113 | 'title': 'test page %d' % self.counter, 114 | 'slug': 'test-page-%d' % self.counter, 'language': 'en', 115 | 'sites': [1], 'status': Page.DRAFT if draft else Page.PUBLISHED, 116 | # used to disable an error with connected models 117 | 'document_set-TOTAL_FORMS': 0, 'document_set-INITIAL_FORMS': 0, 118 | } 119 | self.counter = self.counter + 1 120 | return page_data 121 | 122 | def new_page(self, *args, **kwargs): 123 | return new_page(*args, **kwargs) 124 | 125 | def create_new_page(self, client=None, draft=False): 126 | if not client: 127 | client = self.get_admin_client() 128 | page_data = self.get_new_page_data(draft=draft) 129 | response = client.post(reverse("admin:pages_page_add"), page_data) 130 | self.assertRedirects(response, reverse("admin:pages_page_changelist")) 131 | slug_content = Content.objects.get_content_slug_by_slug( 132 | page_data['slug']) 133 | return slug_content.page 134 | -------------------------------------------------------------------------------- /pages/urlconf_registry.py: -------------------------------------------------------------------------------- 1 | """Django page CMS urlconf registry.""" 2 | from django.utils.translation import ugettext as _ 3 | 4 | 5 | class UrlconfAlreadyRegistered(Exception): 6 | """ 7 | An attempt was made to register a urlconf for Django page CMS more 8 | than once. 9 | """ 10 | 11 | 12 | class UrlconfNotFound(Exception): 13 | """ 14 | The requested urlconf was not found 15 | """ 16 | 17 | 18 | registry = [] 19 | 20 | 21 | def get_choices(): 22 | choices = [('', 'No delegation')] 23 | for reg in registry: 24 | if reg[2]: 25 | label = reg[2] 26 | else: 27 | label = reg[0] 28 | choices.append((reg[0], label)) 29 | return choices 30 | 31 | 32 | def register_urlconf(name, urlconf, label=None): 33 | for urlconf_tuple in registry: 34 | if urlconf_tuple[0] == name: 35 | raise UrlconfAlreadyRegistered( 36 | _('The urlconf %s has already been registered.') % name) 37 | urlconf_tuple = (name, urlconf, label, urlconf) 38 | registry.append(urlconf_tuple) 39 | 40 | 41 | def get_urlconf(name): 42 | for urlconf_tuple in registry: 43 | if urlconf_tuple[0] == name: 44 | return urlconf_tuple[1] 45 | raise UrlconfNotFound( 46 | _('The urlconf %s has not been registered.') % name) 47 | -------------------------------------------------------------------------------- /pages/urls.py: -------------------------------------------------------------------------------- 1 | """Django page CMS urls module.""" 2 | 3 | from django.conf.urls import url 4 | from pages import views 5 | from pages import settings 6 | 7 | urlpatterns = [] 8 | 9 | if settings.PAGE_API_ENABLED: 10 | try: 11 | from pages import api 12 | except ImportError as detail: 13 | print("API not present because of import error: %s" % detail) 14 | else: 15 | urlpatterns += [ 16 | url(r'^api/$', api.PageList.as_view()), 17 | url(r'^api/pages/$', api.PageList.as_view()), 18 | url(r'^api/pages/(?P[0-9]+)/$', api.PageEdit.as_view()), 19 | url(r'^api/contents/$', api.ContentList.as_view()), 20 | url(r'^api/contents/(?P[0-9]+)/$', api.ContentEdit.as_view()) 21 | ] 22 | 23 | if settings.PAGE_USE_LANGUAGE_PREFIX: 24 | urlpatterns += [ 25 | url(r'^(?P[-\w]+)/(?P.*)$', views.details, 26 | name='pages-details-by-path'), 27 | url(r'^$', views.details, {'path': '', 'name': 'pages-root'}), 28 | ] 29 | else: 30 | urlpatterns += [ 31 | url(r'^(?P.*)$', views.details, name='pages-details-by-path'), 32 | url(r'^$', views.details, {'path': '', 'name': 'pages-root'}), 33 | ] 34 | -------------------------------------------------------------------------------- /pages/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """A collection of functions for Page CMS""" 3 | 4 | import re 5 | import unicodedata 6 | 7 | from django.conf import settings as django_settings 8 | from django.utils import timezone 9 | from django.template import Context 10 | from django import template 11 | from django.utils.encoding import force_text 12 | from django.utils.safestring import SafeText, mark_safe 13 | 14 | from datetime import datetime 15 | 16 | dummy_context = Context() 17 | 18 | 19 | def get_now(): 20 | if django_settings.USE_TZ: 21 | return datetime.utcnow().replace(tzinfo=timezone.utc) 22 | else: 23 | return datetime.now() 24 | 25 | 26 | def get_placeholders(template_name): 27 | """Return a list of PlaceholderNode found in the given template. 28 | 29 | :param template_name: the name of the template file 30 | """ 31 | dummy_context.template = template.Template("") 32 | try: 33 | temp_wrapper = template.loader.get_template(template_name) 34 | except template.TemplateDoesNotExist: 35 | return [] 36 | 37 | plist, blist = [], [] 38 | temp = temp_wrapper.template 39 | _placeholders_recursif(temp.nodelist, plist, blist) 40 | 41 | previous = {} 42 | block_to_remove = [] 43 | for block in blist: 44 | if block.name in previous: 45 | if not hasattr(block, 'has_super_var'): 46 | block_to_remove.append(previous[block.name]) 47 | previous[block.name] = block 48 | 49 | def keep(p): 50 | return p.found_in_block not in block_to_remove 51 | 52 | placeholders = [p for p in plist if keep(p)] 53 | names = [] 54 | pfiltered = [] 55 | for p in placeholders: 56 | if p.ctype not in names: 57 | pfiltered.append(p) 58 | names.append(p.ctype) 59 | 60 | return pfiltered 61 | 62 | 63 | def _placeholders_recursif(nodelist, plist, blist): 64 | """Recursively search into a template node list for PlaceholderNode 65 | node.""" 66 | # I needed to do this lazy import to compile the documentation 67 | from django.template.loader_tags import BlockNode 68 | 69 | if len(blist): 70 | block = blist[-1] 71 | else: 72 | block = None 73 | 74 | for node in nodelist: 75 | 76 | if isinstance(node, BlockNode): 77 | if node not in blist: 78 | blist.append(node) 79 | if not block: 80 | block = node 81 | 82 | if block: 83 | if isinstance(node, template.base.VariableNode): 84 | if(node.filter_expression.var.var == u'block.super'): 85 | block.has_super_var = True 86 | 87 | # extends node? 88 | if hasattr(node, 'parent_name'): 89 | # I do not know why I did this... but the tests are guarding it 90 | dummy_context2 = Context() 91 | dummy_context2.template = template.Template("") 92 | _placeholders_recursif(node.get_parent(dummy_context2).nodelist, 93 | plist, blist) 94 | # include node? 95 | elif hasattr(node, 'template') and hasattr(node.template, 'nodelist'): 96 | _placeholders_recursif(node.template.nodelist, plist, blist) 97 | 98 | # Is it a placeholder? 99 | if hasattr(node, 'page') and hasattr(node, 'parsed') and \ 100 | hasattr(node, 'as_varname') and hasattr(node, 'name') \ 101 | and hasattr(node, 'section'): 102 | if block: 103 | node.found_in_block = block 104 | plist.append(node) 105 | node.render(dummy_context) 106 | 107 | for key in ('nodelist', 'nodelist_true', 'nodelist_false'): 108 | 109 | if hasattr(node, key): 110 | try: 111 | _placeholders_recursif(getattr(node, key), plist, blist) 112 | except: 113 | pass 114 | 115 | 116 | def normalize_url(url): 117 | """Return a normalized url with trailing and without leading slash. 118 | 119 | >>> normalize_url(None) 120 | '/' 121 | >>> normalize_url('/') 122 | '/' 123 | >>> normalize_url('/foo/bar') 124 | '/foo/bar' 125 | >>> normalize_url('foo/bar') 126 | '/foo/bar' 127 | >>> normalize_url('/foo/bar/') 128 | '/foo/bar' 129 | """ 130 | if not url or len(url) == 0: 131 | return '/' 132 | if not url.startswith('/'): 133 | url = '/' + url 134 | if len(url) > 1 and url.endswith('/'): 135 | url = url[0:len(url) - 1] 136 | return url 137 | 138 | 139 | def slugify(value, allow_unicode=False): 140 | """ 141 | Convert to ASCII if 'allow_unicode' is False. Convert spaces to hyphens. 142 | Remove characters that aren't alphanumerics, underscores, or hyphens. 143 | Convert to lowercase. Also strip leading and trailing whitespace. 144 | Copyright: https://docs.djangoproject.com/en/1.9/_modules/django/utils/text/#slugify 145 | TODO: replace after stopping support for Django 1.8 146 | """ 147 | value = force_text(value) 148 | if allow_unicode: 149 | value = unicodedata.normalize('NFKC', value) 150 | value = re.sub('[^\w\s-]', '', value, flags=re.U).strip().lower() 151 | return mark_safe(re.sub('[-\s]+', '-', value, flags=re.U)) 152 | value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii') 153 | value = re.sub('[^\w\s-]', '', value).strip().lower() 154 | return mark_safe(re.sub('[-\s]+', '-', value)) 155 | -------------------------------------------------------------------------------- /pages/widgets_registry.py: -------------------------------------------------------------------------------- 1 | """Django page CMS widget registry.""" 2 | from django.utils.translation import ugettext as _ 3 | 4 | __all__ = ('register_widget',) 5 | 6 | 7 | class WidgetAlreadyRegistered(Exception): 8 | """ 9 | An attempt was made to register a widget for Django page CMS more 10 | than once. 11 | """ 12 | pass 13 | 14 | 15 | class WidgetNotFound(Exception): 16 | """ 17 | The requested widget was not found 18 | """ 19 | pass 20 | 21 | registry = [] 22 | 23 | 24 | def register_widget(widget): 25 | """ 26 | Register the given widget as a candidate to use in placeholder. 27 | """ 28 | if widget in registry: 29 | raise WidgetAlreadyRegistered( 30 | _('The widget %s has already been registered.') % widget.__name__) 31 | registry.append(widget) 32 | 33 | 34 | def get_widget(name): 35 | """ 36 | Give back a widget class according to his name. 37 | """ 38 | for widget in registry: 39 | if widget.__name__ == name: 40 | return widget 41 | raise WidgetNotFound( 42 | _('The widget %s has not been registered.') % name) 43 | -------------------------------------------------------------------------------- /requirements-doc.txt: -------------------------------------------------------------------------------- 1 | # Read the docs loves `requirements.txt` files. 2 | Django==4.2.21 3 | 4 | .[docs] -------------------------------------------------------------------------------- /requirements-frozen.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.8.1 2 | certifi==2025.1.31 3 | charset-normalizer==3.4.1 4 | Django==4.2.21 5 | django-ckeditor==6.7.2 6 | django-haystack==3.3.0 7 | django-js-asset==2.2.0 8 | django-mptt==0.17.0 9 | django-page-cms @ file:///code 10 | django-taggit==4.0.0 11 | djangorestframework==3.15.1 12 | idna==3.10 13 | Markdown==3.7 14 | packaging==24.2 15 | pillow==11.1.0 16 | polib==1.2.0 17 | pytz==2025.2 18 | requests==2.32.3 19 | sorl-thumbnail==12.11.0 20 | sqlparse==0.5.3 21 | tqdm==4.67.1 22 | urllib3==2.3.0 23 | Whoosh==2.7.4 24 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | 4 | [build_sphinx] 5 | source-dir = doc 6 | build-dir = doc/build 7 | all_files = 1 8 | 9 | [upload_sphinx] 10 | upload-dir = doc/build/html -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from setuptools import setup, find_packages 3 | import os 4 | import pages 5 | 6 | package_name = 'django-page-cms' 7 | 8 | base = os.path.dirname(__file__) 9 | 10 | 11 | def local_open(fname): 12 | return open(os.path.join(base, fname), 'r') 13 | 14 | 15 | data_dirs = [] 16 | for directory in os.walk('pages/templates'): 17 | data_dirs.append(directory[0][6:] + '/*.*') 18 | 19 | for directory in os.walk('pages/media'): 20 | data_dirs.append(directory[0][6:] + '/*.*') 21 | 22 | for directory in os.walk('pages/static'): 23 | data_dirs.append(directory[0][6:] + '/*.*') 24 | 25 | for directory in os.walk('pages/locale'): 26 | data_dirs.append(directory[0][6:] + '/*.*') 27 | 28 | for directory in os.walk('pages/fixtures'): 29 | data_dirs.append(directory[0][6:] + '/*.*') 30 | 31 | for directory in os.walk('pages/plugins/jsonexport/templates'): 32 | data_dirs.append(directory[0][6:] + '/*.*') 33 | 34 | example_dirs = [] 35 | for directory in os.walk('example/templates'): 36 | example_dirs.append(directory[0][8:] + '/*.*') 37 | 38 | for directory in os.walk('example/static'): 39 | example_dirs.append(directory[0][8:] + '/*.*') 40 | 41 | url_schema = 'http://pypi.python.org/packages/source/d/%s/%s-%s.tar.gz' 42 | download_url = url_schema % (package_name, package_name, pages.__version__) 43 | 44 | install_requires = [ 45 | 'Django>=2.1.5,<4', 46 | 'django-mptt>=0.9', 47 | 'django-taggit>=1.1.0', 48 | 'Pillow>=3.2', 49 | 'requests>=2.20', 50 | 'tqdm>=4.4.1', 51 | ] 52 | 53 | extra = [ 54 | 'django-ckeditor>=5.0.3', 55 | 'django-haystack>=2.8', 56 | 'djangorestframework>=3.8', 57 | 'Markdown>=2.6.6', 58 | 'polib>=1.0.7', 59 | 'Whoosh>=2.7.4', 60 | 'sorl-thumbnail>=12.5.0', 61 | ] 62 | 63 | tests_require = [ 64 | 'coverage>=4.2.0', 65 | 'selenium>=3.0.1', 66 | ] 67 | 68 | docs_require = [ 69 | 'Sphinx>=2.4.0', 70 | 'sphinx-better-theme', 71 | 'Sphinx-PyPI-upload', 72 | ] 73 | 74 | extras_require = { 75 | 'docs': install_requires + extra + docs_require, 76 | 'extra': extra, 77 | 'full': install_requires + extra, 78 | 'tests': install_requires + extra + tests_require, 79 | } 80 | 81 | setup( 82 | name=package_name, 83 | test_suite='pages.test_runner.build_suite', 84 | version=pages.__version__, 85 | description=pages.__doc__, 86 | author=pages.__author__, 87 | author_email=pages.__contact__, 88 | url=pages.__homepage__, 89 | license=pages.__license__, 90 | keywords=pages.__keywords__, 91 | long_description=local_open('README.rst').read(), 92 | download_url=download_url, 93 | install_requires=install_requires, 94 | extras_require=extras_require, 95 | tests_require=tests_require, 96 | packages=find_packages(), 97 | # very important for the binary distribution to include the templates. 98 | package_data={'pages': data_dirs, 'example': example_dirs}, 99 | # include_package_data=True, # include package data under svn source control 100 | zip_safe=False, 101 | classifiers=[ 102 | 'Development Status :: 5 - Production/Stable', 103 | 'Environment :: Web Environment', 104 | 'Intended Audience :: Developers', 105 | 'License :: OSI Approved :: BSD License', 106 | 'Operating System :: OS Independent', 107 | 'Programming Language :: Python', 108 | 'Framework :: Django', 109 | 'Programming Language :: Python :: 3.3', 110 | 'Programming Language :: Python :: 3.4', 111 | 'Programming Language :: Python :: 3.5', 112 | 'Programming Language :: Python :: 3.6', 113 | 'Programming Language :: JavaScript', 114 | 'Topic :: Internet :: WWW/HTTP :: Site Management' 115 | ], 116 | entry_points={ 117 | 'console_scripts': ['gerbi=pages.command_line:main'], 118 | } 119 | ) 120 | --------------------------------------------------------------------------------