├── .coveragerc ├── .github └── workflows │ └── python-package.yml ├── .gitignore ├── AUTHORS ├── CHANGELOG ├── CONTRIBUTING ├── INSTALL ├── LICENSE ├── MANIFEST.in ├── README.rst ├── demo ├── Dockerfile ├── README.rst ├── db.sqlite3 ├── demo │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── middleware.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── sitetrees.py │ ├── static │ │ ├── bootstrap │ │ │ ├── bootstrap.min.css │ │ │ └── bootstrap.min.js │ │ ├── bootstrap3 │ │ │ ├── bootstrap.min.css │ │ │ └── bootstrap.min.js │ │ ├── bootstrap4 │ │ │ ├── bootstrap.bundle.min.js │ │ │ └── bootstrap.min.css │ │ ├── foundation │ │ │ ├── app.js │ │ │ ├── foundation.min.css │ │ │ └── foundation.min.js │ │ ├── jquery-1.12.4.min.js │ │ └── semantic │ │ │ ├── semantic.min.css │ │ │ └── semantic.min.js │ ├── templates │ │ ├── _base.html │ │ ├── _head_bootstrap.html │ │ ├── _head_bootstrap3.html │ │ ├── _head_bootstrap4.html │ │ ├── _head_foundation.html │ │ ├── _head_none.html │ │ ├── _head_semantic.html │ │ ├── _listing_contents.html │ │ ├── demo_nav.html │ │ ├── index.html │ │ ├── listing.html │ │ ├── listing_bootstrap.html │ │ ├── listing_bootstrap3.html │ │ ├── listing_bootstrap4.html │ │ ├── listing_foundation.html │ │ ├── listing_none.html │ │ └── listing_semantic.html │ ├── urls.py │ ├── utils.py │ └── views.py ├── docker-compose.yml ├── manage.py ├── requirements.txt └── settings │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── docs ├── Makefile ├── build │ └── .gitignore └── source │ ├── addons.rst │ ├── admin.rst │ ├── apps.rst │ ├── conf.py │ ├── customization.rst │ ├── forms.rst │ ├── i18n.rst │ ├── index.rst │ ├── management.rst │ ├── models.rst │ ├── performance.rst │ ├── quickstart.rst │ ├── tags.rst │ ├── tagsadv.rst │ ├── templatesmod.rst │ └── thirdparty.rst ├── pytest.ini ├── setup.cfg ├── setup.py ├── sitetree ├── __init__.py ├── admin.py ├── apps.py ├── compat.py ├── exceptions.py ├── fields.py ├── forms.py ├── locale │ ├── de │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── en │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── es │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fa │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fr │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── ja │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── nb │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── ru │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── uk │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── sitetree_resync_apps.py │ │ ├── sitetreedump.py │ │ └── sitetreeload.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_alter_treeitem_parent_alter_treeitem_tree.py │ └── __init__.py ├── models.py ├── settings.py ├── sitetreeapp.py ├── static │ └── css │ │ └── sitetree_bootstrap_submenu.css ├── templates │ ├── admin │ │ └── sitetree │ │ │ ├── tree │ │ │ ├── change_form.html │ │ │ ├── change_list_.html │ │ │ ├── tree.html │ │ │ └── tree_combo.html │ │ │ └── treeitem │ │ │ ├── breadcrumbs.html │ │ │ ├── change_form.html │ │ │ ├── delete_confirmation.html │ │ │ └── object_history.html │ └── sitetree │ │ ├── breadcrumbs-title.html │ │ ├── breadcrumbs.html │ │ ├── breadcrumbs_bootstrap.html │ │ ├── breadcrumbs_bootstrap3.html │ │ ├── breadcrumbs_bootstrap4.html │ │ ├── breadcrumbs_foundation.html │ │ ├── breadcrumbs_semantic.html │ │ ├── menu.html │ │ ├── menu_bootstrap.html │ │ ├── menu_bootstrap3.html │ │ ├── menu_bootstrap3_deep.html │ │ ├── menu_bootstrap3_deep_dropdown.html │ │ ├── menu_bootstrap3_dropdown.html │ │ ├── menu_bootstrap3_navpills-stacked.html │ │ ├── menu_bootstrap3_navpills.html │ │ ├── menu_bootstrap4.html │ │ ├── menu_bootstrap4_dropdown.html │ │ ├── menu_bootstrap4_navpills-stacked.html │ │ ├── menu_bootstrap4_navpills.html │ │ ├── menu_bootstrap5.html │ │ ├── menu_bootstrap5_dropdown.html │ │ ├── menu_bootstrap_dropdown.html │ │ ├── menu_bootstrap_navlist.html │ │ ├── menu_foundation-vertical.html │ │ ├── menu_foundation.html │ │ ├── menu_foundation_flyout.html │ │ ├── menu_foundation_sidenav.html │ │ ├── menu_semantic-vertical.html │ │ ├── menu_semantic.html │ │ ├── menu_semantic_dropdown.html │ │ └── tree.html ├── templatetags │ ├── __init__.py │ └── sitetree.py ├── tests │ ├── __init__.py │ ├── conftest.py │ ├── test_admin.py │ ├── test_dynamic.py │ ├── test_forms.py │ ├── test_management.py │ ├── test_migrations.py │ ├── test_models.py │ ├── test_other.py │ ├── test_templatetags.py │ ├── test_utils.py │ └── testapp │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── conf.py │ │ ├── models.py │ │ ├── mysitetree.py │ │ ├── sitetrees.py │ │ ├── templates │ │ ├── my500.html │ │ └── mymodel.html │ │ └── urls.py ├── toolbox.py └── utils.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | include = sitetree/* 3 | omit = sitetree/migrations/*, sitetree/south_migrations/*, sitetree/tests/*, sitetree/runtests.py, sitetree/config.py 4 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | python-version: [3.7, 3.8, 3.9, "3.10", 3.11] 18 | django-version: [2.0, 2.1, 2.2, 3.0, 3.1, 3.2, 4.0, 5.0, 5.1] 19 | 20 | exclude: 21 | 22 | - python-version: 3.11 23 | django-version: 2.1 24 | 25 | - python-version: 3.9 26 | django-version: 5.0 27 | 28 | - python-version: 3.9 29 | django-version: 5.1 30 | 31 | - python-version: 3.8 32 | django-version: 5.0 33 | 34 | - python-version: 3.8 35 | django-version: 5.1 36 | 37 | - python-version: 3.7 38 | django-version: 5.0 39 | 40 | - python-version: 3.7 41 | django-version: 5.1 42 | 43 | - python-version: 3.7 44 | django-version: 4.0 45 | 46 | steps: 47 | - uses: actions/checkout@v2 48 | - name: Set up Python ${{ matrix.python-version }} & Django ${{ matrix.django-version }} 49 | uses: actions/setup-python@v2 50 | with: 51 | python-version: ${{ matrix.python-version }} 52 | - name: Install deps 53 | run: | 54 | python -m pip install pytest coverage coveralls "Django~=${{ matrix.django-version }}.0" 55 | - name: Run tests 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.github_token }} 58 | run: | 59 | coverage run --source=sitetree setup.py test 60 | coveralls --service=github 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .project 3 | .pydevproject 4 | .idea 5 | .tox 6 | django_sitetree.egg-info 7 | docs/build 8 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | django-sitetree Authors 2 | ======================= 3 | 4 | Created by Igor `idle sign` Starikov. 5 | 6 | 7 | Contributors 8 | ------------ 9 | 10 | Anatoly Kudinov 11 | clincher 12 | Andrey Chibisov 13 | Vladimir Tartynskyi 14 | Arcady Usov 15 | Pavel Shiryaev 16 | Alexander Koshelev 17 | Danilo Bargen 18 | Silveron 19 | Brendtron5000 20 | Dmitry Slepichev 21 | Arturs Vonda 22 | Jeff Triplett 23 | Jacob Kaplan-Moss 24 | Sanja Zivotic 25 | Roberto Abdelkader 26 | Scott Adams 27 | Rob Charlwood 28 | thenewguy 29 | Erika Reinhardt 30 | Dmitry Voronin 31 | Dave Pretty 32 | Alexander Artemenko 33 | ibooj 34 | Patrick Altman 35 | Ben Cole 36 | Vitaliy Ivanov 37 | Sergey Maranchuk 38 | Martey Dodoo 39 | Michał Suszko 40 | Piter Vergara 41 | Chris Lamb 42 | stop5 43 | PetrDlouhy 44 | Richard Price 45 | Walter Lorenzetti 46 | Ramon Saraiva 47 | Jon Kiparsky 48 | Thomas Güttler 49 | Bart van der Schoor 50 | Eduardo Garcia Cebollero 51 | Kishor Kunal Raj 52 | Ben Finney 53 | witwar 54 | Jon Kiparsky 55 | Jeffrey de Lange 56 | Simon Klein 57 | 58 | 59 | Translators 60 | ----------- 61 | 62 | Russian: Igor Starikov 63 | Ukranian: Sergiy Gavrylov 64 | German: Danilo Bargen 65 | German: Markus Maurer 66 | Persian: Ali Javadi 67 | Spanish: Adrián López Calvo 68 | Norwegian: Eirik Krogstad 69 | French: Jean Traullé 70 | Japanese: Hajime Nishida 71 | Japanese: ToitaYuka 72 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | django-sitetree contributing 2 | ============================ 3 | 4 | 5 | 6 | Submit issues 7 | ------------- 8 | 9 | If you spotted something weird in application behavior or want to propose a feature 10 | you can do that at https://github.com/idlesign/django-sitetree/issues 11 | 12 | 13 | Write code 14 | ---------- 15 | 16 | If you are eager to participate in application development, fork it at https://github.com/idlesign/django-sitetree, 17 | write your code, whether it should be a bugfix or a feature implementation, and make a pull request right 18 | from the forked project page. 19 | 20 | Translate 21 | --------- 22 | 23 | If want to translate the application into your native language use Transifex: 24 | https://www.transifex.net/projects/p/django-sitetree/. 25 | 26 | 27 | Spread the word 28 | --------------- 29 | 30 | If you have some tips and tricks or any other words that you think might be of interest for the others — publish it 31 | wherever you find convenient. 32 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | django-sitetree installation 2 | ============================ 3 | 4 | 5 | Python ``pip`` package is required to install ``django-sitetree``. 6 | 7 | 8 | From sources 9 | ------------ 10 | 11 | Use the following command line to install ``django-sitetree`` from sources directory (containing setup.py): 12 | 13 | pip install . 14 | 15 | or 16 | 17 | python setup.py install 18 | 19 | 20 | From PyPI 21 | --------- 22 | 23 | Alternatively you can install ``django-sitetree`` from PyPI: 24 | 25 | pip install django-sitetree 26 | 27 | 28 | Use `-U` flag for upgrade: 29 | 30 | pip install -U django-sitetree 31 | 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2023, django-sitetree project 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the django-sitetree nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include AUTHORS 3 | include LICENSE 4 | include CHANGELOG 5 | 6 | include docs/Makefile 7 | recursive-include docs *.rst 8 | recursive-include docs *.py 9 | recursive-include tests * 10 | 11 | recursive-include sitetree/locale * 12 | recursive-include sitetree/static * 13 | recursive-include sitetree/migrations *.py 14 | recursive-include sitetree/templates *.html 15 | recursive-include sitetree/templatetags *.py 16 | recursive-include sitetree/management *.py 17 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-sitetree 2 | =============== 3 | http://github.com/idlesign/django-sitetree 4 | 5 | |release| |lic| |coverage| 6 | 7 | .. |release| image:: https://img.shields.io/pypi/v/django-sitetree.svg 8 | :target: https://pypi.python.org/pypi/django-sitetree 9 | 10 | .. |lic| image:: https://img.shields.io/pypi/l/django-sitetree.svg 11 | :target: https://pypi.python.org/pypi/django-sitetree 12 | 13 | .. |coverage| image:: https://img.shields.io/coveralls/idlesign/django-sitetree/master.svg 14 | :target: https://coveralls.io/r/idlesign/django-sitetree 15 | 16 | 17 | What's that 18 | ----------- 19 | 20 | *django-sitetree is a reusable application for Django, introducing site tree, menu and breadcrumbs navigation elements.* 21 | 22 | Site structure in django-sitetree is described through Django admin interface in a so called site trees. 23 | Every item of such a tree describes a page or a set of pages through the relation of URI or URL to human-friendly title. Eg. using site tree editor in Django admin:: 24 | 25 | URI Title 26 | / - Site Root 27 | |_users/ - Site Users 28 | |_users/13/ - Definite User 29 | 30 | 31 | Alas the example above makes a little sense if you have more than just a few users, that's why django-sitetree supports Django template tags in item titles and Django named URLs in item URIs. 32 | If we define a named URL for user personal page in urls.py, for example, 'users-personal', we could change a scheme in the following way:: 33 | 34 | URI Title 35 | / - Site Root 36 | |_users/ - Site Users 37 | |_users-personal user.id - User Called {{ user.first_name }} 38 | 39 | After setting up site structure as a sitetree you should be able to use convenient and highly customizable site navigation means (menus, breadcrumbs and full site trees). 40 | 41 | User access to certain sitetree items can be restricted to authenticated users or more accurately with the help of Django permissions system (Auth contrib package). 42 | 43 | Sitetree also allows you to define dynamic trees in your code instead of Admin interface. And even more: you can combine those two types of trees in more sophisticated ways. 44 | 45 | 46 | Documentation 47 | ------------- 48 | 49 | http://django-sitetree.readthedocs.org/ 50 | -------------------------------------------------------------------------------- /demo/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-alpine 2 | 3 | ADD . /code 4 | WORKDIR /code 5 | 6 | ADD requirements.txt /requirements.txt 7 | RUN pip install -r /requirements.txt -------------------------------------------------------------------------------- /demo/README.rst: -------------------------------------------------------------------------------- 1 | django-sitetree demo 2 | ==================== 3 | http://github.com/idlesign/django-sitetree 4 | 5 | 6 | This Django project demonstrates sitetree basic features and bundled templates. 7 | 8 | Expects Django 1.11+ 9 | 10 | 11 | How to run 12 | ---------- 13 | 14 | Docker 15 | ~~~~~~ 16 | 17 | 1. Run `docker-compose up` 18 | 2. Go to http://localhost:8000 19 | 20 | Manually 21 | ~~~~~~~~ 22 | 23 | 1. Install the requirements with `pip install -r requirements.txt` 24 | 2. Run the server `python manage.py runserver` 25 | 3. Go to http://localhost:8000 26 | 27 | Admin 28 | ~~~~~ 29 | 30 | Admin (http://localhost:8000) credentials: 31 | 32 | Login: `demo` 33 | Password: `demodemo` 34 | -------------------------------------------------------------------------------- /demo/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idlesign/django-sitetree/37a0c347eaac91f570aa5bf73c55c75035414660/demo/db.sqlite3 -------------------------------------------------------------------------------- /demo/demo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idlesign/django-sitetree/37a0c347eaac91f570aa5bf73c55c75035414660/demo/demo/__init__.py -------------------------------------------------------------------------------- /demo/demo/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.contrib import admin 5 | 6 | from .models import Article 7 | 8 | 9 | admin.site.register(Article) 10 | 11 | 12 | customized_sitetree_admin = False 13 | 14 | if customized_sitetree_admin: 15 | 16 | from sitetree.admin import TreeItemAdmin, TreeAdmin, override_tree_admin, override_item_admin 17 | 18 | class CustomTreeItemAdmin(TreeItemAdmin): 19 | 20 | fieldsets = None 21 | 22 | 23 | class CustomTreeAdmin(TreeAdmin): 24 | 25 | exclude = ('title',) 26 | 27 | override_item_admin(CustomTreeItemAdmin) 28 | override_tree_admin(CustomTreeAdmin) 29 | -------------------------------------------------------------------------------- /demo/demo/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.apps import AppConfig 5 | 6 | 7 | class DemoConfig(AppConfig): 8 | 9 | name = 'demo' 10 | -------------------------------------------------------------------------------- /demo/demo/middleware.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import activate 2 | 3 | 4 | def language_activator(get_response): 5 | 6 | def middleware(request): 7 | 8 | lang = request.GET.get('lang', 'en') 9 | activate(lang) 10 | 11 | request.lang = lang 12 | 13 | return get_response(request) 14 | 15 | return middleware 16 | 17 | 18 | def theme_activator(get_response): 19 | 20 | def middleware(request): 21 | 22 | theme = request.GET.get('theme', 'none') 23 | if theme: 24 | theme = '_' + theme 25 | 26 | request.theme = theme 27 | 28 | return get_response(request) 29 | 30 | return middleware 31 | -------------------------------------------------------------------------------- /demo/demo/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2017-04-08 09:47 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Article', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('date_created', models.DateTimeField(auto_created=True, verbose_name='Created')), 21 | ('title', models.CharField(max_length=200, verbose_name='Title')), 22 | ('contents', models.TextField(verbose_name='Contents')), 23 | ], 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /demo/demo/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idlesign/django-sitetree/37a0c347eaac91f570aa5bf73c55c75035414660/demo/demo/migrations/__init__.py -------------------------------------------------------------------------------- /demo/demo/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models 5 | 6 | 7 | class Article(models.Model): 8 | 9 | title = models.CharField('Title', max_length=200, blank=False) 10 | date_created = models.DateTimeField('Created', auto_created=True) 11 | contents = models.TextField('Contents') 12 | 13 | def __str__(self): 14 | return self.title 15 | 16 | 17 | customized_sitetree_models = False 18 | 19 | if customized_sitetree_models: 20 | 21 | from sitetree.models import TreeItemBase, TreeBase 22 | 23 | 24 | class MyTree(TreeBase): 25 | 26 | custom_field = models.CharField('Custom tree field', max_length=50, null=True, blank=True) 27 | 28 | 29 | class MyTreeItem(TreeItemBase): 30 | 31 | custom_field = models.IntegerField('Custom item field', default=42) 32 | -------------------------------------------------------------------------------- /demo/demo/sitetrees.py: -------------------------------------------------------------------------------- 1 | from sitetree.utils import tree, item 2 | 3 | 4 | sitetrees = ( 5 | tree('books', items=[ 6 | item('Books', '/books/', url_as_pattern=False, children=[ 7 | item('{{ book.title }}', 'books-details', in_menu=False, in_sitetree=False), 8 | item('Add a book', 'books-add'), 9 | ]) 10 | ]), 11 | tree('other', items=[ 12 | item('Item', '/item/', url_as_pattern=False, access_guest=False) 13 | ]), 14 | ) 15 | -------------------------------------------------------------------------------- /demo/demo/static/foundation/app.js: -------------------------------------------------------------------------------- 1 | ;(function ($, window, undefined) { 2 | 'use strict'; 3 | 4 | var $doc = $(document), 5 | Modernizr = window.Modernizr; 6 | 7 | $(document).ready(function() { 8 | $.fn.foundationAlerts ? $doc.foundationAlerts() : null; 9 | $.fn.foundationButtons ? $doc.foundationButtons() : null; 10 | $.fn.foundationAccordion ? $doc.foundationAccordion() : null; 11 | $.fn.foundationNavigation ? $doc.foundationNavigation() : null; 12 | $.fn.foundationTopBar ? $doc.foundationTopBar() : null; 13 | $.fn.foundationCustomForms ? $doc.foundationCustomForms() : null; 14 | $.fn.foundationMediaQueryViewer ? $doc.foundationMediaQueryViewer() : null; 15 | $.fn.foundationTabs ? $doc.foundationTabs({callback : $.foundation.customForms.appendCustomMarkup}) : null; 16 | $.fn.foundationTooltips ? $doc.foundationTooltips() : null; 17 | $.fn.foundationMagellan ? $doc.foundationMagellan() : null; 18 | $.fn.foundationClearing ? $doc.foundationClearing() : null; 19 | 20 | $.fn.placeholder ? $('input, textarea').placeholder() : null; 21 | }); 22 | 23 | // UNCOMMENT THE LINE YOU WANT BELOW IF YOU WANT IE8 SUPPORT AND ARE USING .block-grids 24 | // $('.block-grid.two-up>li:nth-child(2n+1)').css({clear: 'both'}); 25 | // $('.block-grid.three-up>li:nth-child(3n+1)').css({clear: 'both'}); 26 | // $('.block-grid.four-up>li:nth-child(4n+1)').css({clear: 'both'}); 27 | // $('.block-grid.five-up>li:nth-child(5n+1)').css({clear: 'both'}); 28 | 29 | // Hide address bar on mobile devices (except if #hash present, so we don't mess up deep linking). 30 | if (Modernizr.touch && !window.location.hash) { 31 | $(window).load(function () { 32 | setTimeout(function () { 33 | window.scrollTo(0, 1); 34 | }, 0); 35 | }); 36 | } 37 | 38 | })(jQuery, this); 39 | -------------------------------------------------------------------------------- /demo/demo/templates/_base.html: -------------------------------------------------------------------------------- 1 | {% load sitetree static i18n %} 2 | 3 | 4 | 5 | 6 | django-sitetree demo > {% sitetree_page_title from "main" %} ({% get_current_language as LANGUAGE_CODE %}{{LANGUAGE_CODE}}) 7 | 8 | 9 | 10 | {% include tpl_head %} 11 | 12 | 13 | 23 | 24 | 25 | 26 | 27 |
28 | django-sitetree 29 |
30 | 31 | {% block body %}{% endblock %} 32 | 33 | -------------------------------------------------------------------------------- /demo/demo/templates/_head_bootstrap.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /demo/demo/templates/_head_bootstrap3.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /demo/demo/templates/_head_bootstrap4.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /demo/demo/templates/_head_foundation.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /demo/demo/templates/_head_none.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/demo/templates/_head_semantic.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /demo/demo/templates/_listing_contents.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | 3 |
4 |

{% sitetree_page_title from "main" %}

5 | 6 | 11 |
12 | 13 |
14 | 15 |

Site structure

16 | {% sitetree_tree from "main" %} 17 | 18 |
19 | {% include 'demo_nav.html' %} -------------------------------------------------------------------------------- /demo/demo/templates/demo_nav.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | 3 |
4 |

Demo navigation

5 | {% sitetree_tree from "demo" %} 6 |
-------------------------------------------------------------------------------- /demo/demo/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends '_base.html' %} 2 | {% load sitetree %} 3 | 4 | {% block body %} 5 | {% include 'demo_nav.html' %} 6 | {% endblock %} -------------------------------------------------------------------------------- /demo/demo/templates/listing.html: -------------------------------------------------------------------------------- 1 | {% extends '_base.html' %} 2 | {% load sitetree %} 3 | 4 | 5 | {% block body %} 6 | 7 | {% include tpl_realm %} 8 | 9 | {% endblock %} -------------------------------------------------------------------------------- /demo/demo/templates/listing_bootstrap.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 |
3 | 4 | 11 | 12 |
13 |
14 | {% sitetree_menu from "main" include "this-siblings" template "sitetree/menu_bootstrap_navlist.html" %} 15 |
16 |
17 | {% sitetree_breadcrumbs from "main" template "sitetree/breadcrumbs_bootstrap.html" %} 18 | 19 | {% include '_listing_contents.html' %} 20 |
21 |
22 |
-------------------------------------------------------------------------------- /demo/demo/templates/listing_bootstrap3.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | 3 |
4 | 5 | 12 | 13 |
14 |
15 | {% sitetree_menu from "main" include "this-siblings" template "sitetree/menu_bootstrap3_navpills.html" %} 16 |
17 | {% sitetree_menu from "main" include "this-siblings" template "sitetree/menu_bootstrap3_navpills-stacked.html" %} 18 |
19 |
20 | {% sitetree_breadcrumbs from "main" template "sitetree/breadcrumbs_bootstrap3.html" %} 21 | 22 | {% include '_listing_contents.html' %} 23 |
24 |
25 | 26 |
27 | -------------------------------------------------------------------------------- /demo/demo/templates/listing_bootstrap4.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | 3 |
4 | 5 | 8 | 9 |
10 |
11 | {% sitetree_menu from "main" include "this-siblings" template "sitetree/menu_bootstrap4_navpills.html" %} 12 |
13 | {% sitetree_menu from "main" include "this-siblings" template "sitetree/menu_bootstrap4_navpills-stacked.html" %} 14 |
15 |
16 | {% sitetree_breadcrumbs from "main" template "sitetree/breadcrumbs_bootstrap4.html" %} 17 | 18 | {% include '_listing_contents.html' %} 19 |
20 |
21 | 22 |
23 | -------------------------------------------------------------------------------- /demo/demo/templates/listing_foundation.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | 3 |
4 | 5 | {% sitetree_menu from "main" include "trunk" template "sitetree/menu_foundation.html" %} 6 | 7 |
8 | 9 |
10 |
11 | 12 | {% sitetree_menu from "main" include "trunk" template "sitetree/menu_foundation-vertical.html" %} 13 | 14 | {% sitetree_menu from "main" include "this-siblings" template "sitetree/menu_foundation_sidenav.html" %} 15 | 16 |
17 |
18 | 19 | {% sitetree_breadcrumbs from "main" template "sitetree/breadcrumbs_foundation.html" %} 20 | 21 | {% include '_listing_contents.html' %} 22 | 23 |
24 |
25 | -------------------------------------------------------------------------------- /demo/demo/templates/listing_none.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | 3 | 4 |
5 | {% sitetree_menu from "main" include "trunk" %} 6 |
7 |
8 | {% sitetree_menu from "main" include "this-siblings" %} 9 |
10 |
11 | {% sitetree_breadcrumbs from "main" %} 12 | {% include '_listing_contents.html' %} 13 |
14 |
15 |
-------------------------------------------------------------------------------- /demo/demo/templates/listing_semantic.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | 3 | 7 | 8 | 9 |
10 |
11 | {% sitetree_menu from "main" include "this-siblings" template "sitetree/menu_semantic-vertical.html" %} 12 |
13 |
14 | {% sitetree_breadcrumbs from "main" template "sitetree/breadcrumbs_semantic.html" %} 15 | 16 | {% include '_listing_contents.html' %} 17 |
18 |
19 | -------------------------------------------------------------------------------- /demo/demo/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | 3 | from .views import index, listing, detailed 4 | 5 | 6 | urlpatterns = [ 7 | re_path(r'^$', index, name='index'), 8 | re_path(r'^articles/$', listing, name='articles-listing'), 9 | re_path(r'^articles/(?P\d+)/$', detailed, name='articles-detailed'), 10 | ] 11 | -------------------------------------------------------------------------------- /demo/demo/utils.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | 4 | def render_themed(request, view_type, context): 5 | theme = request.theme 6 | context.update({ 7 | 'tpl_head': '_head%s.html' % theme, 8 | 'tpl_realm': '%s%s.html' % (view_type, theme) 9 | }) 10 | return render(request, '%s.html' % view_type, context) 11 | -------------------------------------------------------------------------------- /demo/demo/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.shortcuts import get_list_or_404, redirect 5 | 6 | from .models import Article 7 | from .utils import render_themed 8 | 9 | from sitetree.toolbox import register_i18n_trees 10 | 11 | 12 | register_i18n_trees(['main']) 13 | 14 | 15 | def index(request): 16 | return render_themed(request, 'index', {}) 17 | 18 | 19 | def listing(request): 20 | return render_themed(request, 'listing', {'articles': get_list_or_404(Article)}) 21 | 22 | 23 | def detailed(request, article_id): 24 | return redirect('demo:articles-listing') 25 | -------------------------------------------------------------------------------- /demo/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | web: 5 | restart: always 6 | build: . 7 | command: sh -c "python manage.py migrate && python manage.py runserver 0.0.0.0:8000" 8 | volumes: 9 | - .:/code 10 | ports: 11 | - "8000:8000" -------------------------------------------------------------------------------- /demo/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | 6 | PATH_DEMO = os.path.dirname(__file__) 7 | PATH_SITETREE = os.path.dirname(PATH_DEMO) 8 | 9 | sys.path = [PATH_DEMO, PATH_SITETREE] + sys.path 10 | 11 | if __name__ == "__main__": 12 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.settings") 13 | try: 14 | from django.core.management import execute_from_command_line 15 | except ImportError: 16 | # The above import may fail for some other reason. Ensure that the 17 | # issue is really that Django is missing to avoid masking other 18 | # exceptions on Python 2. 19 | try: 20 | import django 21 | except ImportError: 22 | raise ImportError( 23 | "Couldn't import Django. Are you sure it's installed and " 24 | "available on your PYTHONPATH environment variable? Did you " 25 | "forget to activate a virtual environment?" 26 | ) 27 | raise 28 | execute_from_command_line(sys.argv) 29 | -------------------------------------------------------------------------------- /demo/requirements.txt: -------------------------------------------------------------------------------- 1 | Django==4.2.21 2 | django-sitetree==1.14 3 | -------------------------------------------------------------------------------- /demo/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idlesign/django-sitetree/37a0c347eaac91f570aa5bf73c55c75035414660/demo/settings/__init__.py -------------------------------------------------------------------------------- /demo/settings/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | USE_DEBUG_TOOLBAR = False 5 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 6 | SECRET_KEY = 'not-a-secret' 7 | DEBUG = True 8 | ALLOWED_HOSTS = [] 9 | INTERNAL_IPS = ['127.0.0.1'] 10 | 11 | 12 | # SITETREE_MODEL_TREE = 'demo.MyTree' 13 | # SITETREE_MODEL_TREE_ITEM = 'demo.MyTreeItem' 14 | 15 | 16 | INSTALLED_APPS = [ 17 | 'django.contrib.admin', 18 | 'django.contrib.auth', 19 | 'django.contrib.contenttypes', 20 | 'django.contrib.sessions', 21 | 'django.contrib.messages', 22 | 'django.contrib.staticfiles', 23 | 24 | 'sitetree', 25 | 26 | 'demo', 27 | ] 28 | 29 | MIDDLEWARE = [ 30 | 'django.middleware.security.SecurityMiddleware', 31 | 'django.contrib.sessions.middleware.SessionMiddleware', 32 | 'django.middleware.common.CommonMiddleware', 33 | 'django.middleware.csrf.CsrfViewMiddleware', 34 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 35 | 'django.contrib.messages.middleware.MessageMiddleware', 36 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 37 | 38 | 'demo.middleware.language_activator', 39 | 'demo.middleware.theme_activator', 40 | ] 41 | 42 | ROOT_URLCONF = 'settings.urls' 43 | 44 | TEMPLATES = [ 45 | { 46 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 47 | 'DIRS': [], 48 | 'APP_DIRS': True, 49 | 'OPTIONS': { 50 | 'context_processors': [ 51 | 'django.template.context_processors.debug', 52 | 'django.template.context_processors.request', 53 | 'django.contrib.auth.context_processors.auth', 54 | 'django.contrib.messages.context_processors.messages', 55 | ], 56 | }, 57 | }, 58 | ] 59 | 60 | WSGI_APPLICATION = 'settings.wsgi.application' 61 | 62 | DATABASES = { 63 | 'default': { 64 | 'ENGINE': 'django.db.backends.sqlite3', 65 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 66 | } 67 | } 68 | 69 | LANGUAGE_CODE = 'en-us' 70 | TIME_ZONE = 'UTC' 71 | USE_I18N = True 72 | USE_L10N = True 73 | USE_TZ = True 74 | STATIC_URL = '/static/' 75 | 76 | 77 | CACHES = { 78 | 'default': { 79 | 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', 80 | } 81 | } 82 | 83 | 84 | if USE_DEBUG_TOOLBAR: 85 | INSTALLED_APPS.append('debug_toolbar') 86 | MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware') 87 | -------------------------------------------------------------------------------- /demo/settings/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.urls import path, include 3 | from django.contrib import admin 4 | 5 | 6 | urlpatterns = [ 7 | path('admin/', admin.site.urls), 8 | path('', include(('demo.urls', 'demo'), namespace='demo')), 9 | ] 10 | 11 | 12 | if settings.DEBUG and settings.USE_DEBUG_TOOLBAR: 13 | import debug_toolbar 14 | urlpatterns = [ 15 | path('__debug__/', include(debug_toolbar.urls)), 16 | ] + urlpatterns 17 | -------------------------------------------------------------------------------- /demo/settings/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for settings project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /docs/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 | BUILDDIR = build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-sitetree.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-sitetree.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-sitetree" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-sitetree" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/build/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idlesign/django-sitetree/37a0c347eaac91f570aa5bf73c55c75035414660/docs/build/.gitignore -------------------------------------------------------------------------------- /docs/source/addons.rst: -------------------------------------------------------------------------------- 1 | Thirdparty addons 2 | ================= 3 | 4 | Here are listed addons, helpers, tools that work in a conjunction with SiteTree. 5 | 6 | Skip through, maybe you find there something interesting. 7 | 8 | 9 | django-nav-tree 10 | --------------- 11 | 12 | Application providing a better way to set sitetree item URLs in Django Admin using popup window. 13 | 14 | https://github.com/ikresoft/django-nav-tree 15 | 16 | -------------------------------------------------------------------------------- /docs/source/admin.rst: -------------------------------------------------------------------------------- 1 | Overriding SiteTree Admin representation 2 | ======================================== 3 | 4 | SiteTree allows you to override tree and tree item representation in Django Admin interface. 5 | 6 | That could be used not only for the purpose of enhancement of visual design but also 7 | for integration with other applications, using admin inlines. 8 | 9 | .. _admin-ext: 10 | 11 | 12 | The following functions from `sitetree.admin` could be used to override tree and tree item representation: 13 | 14 | * `override_tree_admin()` is used to customize tree representation. 15 | * `override_item_admin()` is used to customize tree item representation. 16 | 17 | 18 | Example: 19 | 20 | .. code-block:: python 21 | 22 | # Supposing we are in admin.py of your own application. 23 | 24 | # Import two helper functions and two admin models to inherit our custom model from. 25 | from sitetree.admin import TreeItemAdmin, TreeAdmin, override_tree_admin, override_item_admin 26 | 27 | # This is our custom tree admin model. 28 | class CustomTreeAdmin(TreeAdmin): 29 | exclude = ('title',) # Here we exclude `title` field from form. 30 | 31 | # And our custom tree item admin model. 32 | class CustomTreeItemAdmin(TreeItemAdmin): 33 | # That will turn a tree item representation from the default variant 34 | # with collapsible groupings into a flat one. 35 | fieldsets= None 36 | 37 | # Now we tell the SiteTree to replace generic representations with custom. 38 | override_tree_admin(CustomTreeAdmin) 39 | override_item_admin(CustomTreeItemAdmin) 40 | 41 | 42 | .. note:: 43 | 44 | You might also be interested in using :ref:`Tree handler customization `. 45 | 46 | 47 | Inlines override example 48 | ------------------------ 49 | 50 | In the example below we'll use django-seo application from https://github.com/willhardy/django-seo 51 | 52 | According to django-seo documentation it allows an addition of custom metadata fields to your models, 53 | so we use it to connect metadata to sitetree items. 54 | 55 | That's how one might render django-seo inline form on sitetree item create and edit pages: 56 | 57 | .. code-block:: python 58 | 59 | from rollyourown.seo.admin import get_inline 60 | from sitetree.admin import TreeItemAdmin, TreeAdmin, override_tree_admin, override_item_admin 61 | # Let's suppose our application contains seo.py with django-seo metadata class defined. 62 | from myapp.seo import CustomMeta 63 | 64 | 65 | class CustomTreeItemAdmin(TreeItemAdmin): 66 | inlines = [get_inline(CustomMeta)] 67 | 68 | override_item_admin(CustomTreeItemAdmin) 69 | 70 | -------------------------------------------------------------------------------- /docs/source/apps.rst: -------------------------------------------------------------------------------- 1 | Shipping sitetrees with your apps 2 | ================================= 3 | 4 | SiteTree allows you to define sitetrees within your apps. 5 | 6 | 7 | Defining a sitetree 8 | ------------------- 9 | 10 | Let's suppose you have `books` application and want do define a sitetree for it. 11 | 12 | * First create `sitetrees.py` in the directory of `books` app. 13 | 14 | * Then define a sitetree with the help of `tree` and `item` functions from `sitetree.utils` module 15 | and assign it to `sitetrees` module attribute 16 | 17 | .. code-block:: python 18 | 19 | from sitetree.utils import tree, item 20 | 21 | # Be sure you defined `sitetrees` in your module. 22 | sitetrees = ( 23 | # Define a tree with `tree` function. 24 | tree('books', items=[ 25 | # Then define items and their children with `item` function. 26 | item('Books', 'books-listing', children=[ 27 | item('Book named "{{ book.title }}"', 'books-details', in_menu=False, in_sitetree=False), 28 | item('Add a book', 'books-add', access_by_perms=['booksapp.allow_add']), 29 | item('Edit "{{ book.title }}"', 'books-edit', in_menu=False, in_sitetree=False) 30 | ]) 31 | ]), 32 | # ... You can define more than one tree for your app. 33 | ) 34 | 35 | 36 | Please see `tree` and `item` signatures for possible options. 37 | 38 | .. note:: 39 | 40 | If you added extra fields to the Tree and TreeItem models, 41 | then you can specify their values when instantiating `item` see :ref:`custom-model-sitetree` 42 | 43 | 44 | Export sitetree to DB 45 | --------------------- 46 | 47 | Now when your app has a defined sitetree you can use `sitetree_resync_apps` management command 48 | to instantly move sitetrees from every (or certain) applications into DB:: 49 | 50 | python manage.py sitetree_resync_apps 51 | 52 | 53 | Or solely for `books` application:: 54 | 55 | python manage.py sitetree_resync_apps books 56 | 57 | 58 | 59 | 60 | Dynamic sitetree structuring 61 | ---------------------------- 62 | 63 | Optionally you can structure app-defined sitetrees into existing or new trees runtime. 64 | 65 | Basically one should compose a dynamic tree with ``compose_dynamic_tree()`` and register it with ``register_dynamic_trees()``. 66 | 67 | Let's suppose the following code somewhere where app registry is already created, e.g. ``config.ready()`` or even 68 | in ``urls.py`` of your project. 69 | 70 | 71 | .. code-block:: python 72 | 73 | from sitetree.sitetreeapp import register_dynamic_trees, compose_dynamic_tree 74 | from sitetree.utils import tree, item 75 | 76 | 77 | register_dynamic_trees( 78 | 79 | # Gather all the trees from `books`, 80 | compose_dynamic_tree('books'), 81 | 82 | # or gather all the trees from `books` and attach them to `main` tree root, 83 | compose_dynamic_tree('books', target_tree_alias='main'), 84 | 85 | # or gather all the trees from `books` and attach them to `for_books` aliased item in `main` tree, 86 | compose_dynamic_tree('books', target_tree_alias='main', parent_tree_item_alias='for_books'), 87 | 88 | # or even define a tree right at the process of registration. 89 | compose_dynamic_tree(( 90 | tree('dynamic', items=( 91 | item('dynamic_1', 'dynamic_1_url', children=( 92 | item('dynamic_1_sub_1', 'dynamic_1_sub_1_url'), 93 | )), 94 | item('dynamic_2', 'dynamic_2_url'), 95 | )), 96 | )), 97 | 98 | # Line below tells sitetree to drop and recreate cache, so that all newly registered 99 | # dynamic trees are rendered immediately. 100 | reset_cache=True 101 | ) 102 | 103 | 104 | .. note:: If you use only dynamic trees you can set ``SITETREE_DYNAMIC_ONLY = True`` to prevent the application 105 | from querying trees and items stored in DB. 106 | 107 | 108 | Access check for dynamic items 109 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 110 | 111 | For dynamic trees you can implement access on per tree item basis. 112 | 113 | Pass an access checking function in ``access_check`` argument. 114 | 115 | .. note:: This function must accept ``tree`` argument and support pickling (e.g. be exposed on a module level). 116 | 117 | 118 | .. code-block:: python 119 | 120 | def check_user_is_staff(tree): 121 | return tree.current_request.user.is_staff 122 | 123 | ... 124 | 125 | item('dynamic_2', 'dynamic_2_url', access_check=check_user_is_staff), 126 | 127 | ... 128 | 129 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # django-sitetree documentation build configuration file, created by 4 | # sphinx-quickstart on Tue May 17 21:16:38 2011. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.insert(0, os.path.abspath('../../')) 20 | from sitetree import VERSION 21 | 22 | # -- General configuration ----------------------------------------------------- 23 | 24 | # If your documentation needs a minimal Sphinx version, state it here. 25 | #needs_sphinx = '1.0' 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be extensions 28 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 29 | extensions = ['sphinx.ext.autodoc'] 30 | 31 | # Add any paths that contain templates here, relative to this directory. 32 | templates_path = ['_templates'] 33 | 34 | # The suffix of source filenames. 35 | source_suffix = '.rst' 36 | 37 | # The encoding of source files. 38 | #source_encoding = 'utf-8-sig' 39 | 40 | # The master toctree document. 41 | master_doc = 'index' 42 | 43 | # General information about the project. 44 | project = u'django-sitetree' 45 | copyright = u'2011-2023, Igor \'idle sign\' Starikov' 46 | 47 | # The version info for the project you're documenting, acts as replacement for 48 | # |version| and |release|, also used in various other places throughout the 49 | # built documents. 50 | # 51 | # The short X.Y version. 52 | version = '.'.join(map(str, VERSION)) 53 | # The full version, including alpha/beta/rc tags. 54 | release = '.'.join(map(str, VERSION)) 55 | 56 | # The language for content autogenerated by Sphinx. Refer to documentation 57 | # for a list of supported languages. 58 | #language = None 59 | 60 | # There are two options for replacing |today|: either, you set today to some 61 | # non-false value, then it is used: 62 | #today = '' 63 | # Else, today_fmt is used as the format for a strftime call. 64 | #today_fmt = '%B %d, %Y' 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | exclude_patterns = [] 69 | 70 | # The reST default role (used for this markup: `text`) to use for all documents. 71 | #default_role = None 72 | 73 | # If true, '()' will be appended to :func: etc. cross-reference text. 74 | #add_function_parentheses = True 75 | 76 | # If true, the current module name will be prepended to all description 77 | # unit titles (such as .. function::). 78 | #add_module_names = True 79 | 80 | # If true, sectionauthor and moduleauthor directives will be shown in the 81 | # output. They are ignored by default. 82 | #show_authors = False 83 | 84 | # The name of the Pygments (syntax highlighting) style to use. 85 | pygments_style = 'sphinx' 86 | 87 | # A list of ignored prefixes for module index sorting. 88 | #modindex_common_prefix = [] 89 | 90 | 91 | # -- Options for HTML output --------------------------------------------------- 92 | 93 | # The theme to use for HTML and HTML Help pages. See the documentation for 94 | # a list of builtin themes. 95 | html_theme = 'default' 96 | 97 | # Theme options are theme-specific and customize the look and feel of a theme 98 | # further. For a list of options available for each theme, see the 99 | # documentation. 100 | #html_theme_options = {} 101 | 102 | # Add any paths that contain custom themes here, relative to this directory. 103 | #html_theme_path = [] 104 | 105 | # The name for this set of Sphinx documents. If None, it defaults to 106 | # " v documentation". 107 | #html_title = None 108 | 109 | # A shorter title for the navigation bar. Default is the same as html_title. 110 | #html_short_title = None 111 | 112 | # The name of an image file (relative to this directory) to place at the top 113 | # of the sidebar. 114 | #html_logo = None 115 | 116 | # The name of an image file (within the static path) to use as favicon of the 117 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 118 | # pixels large. 119 | #html_favicon = None 120 | 121 | # Add any paths that contain custom static files (such as style sheets) here, 122 | # relative to this directory. They are copied after the builtin static files, 123 | # so a file named "default.css" will overwrite the builtin "default.css". 124 | html_static_path = ['_static'] 125 | 126 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 127 | # using the given strftime format. 128 | #html_last_updated_fmt = '%b %d, %Y' 129 | 130 | # If true, SmartyPants will be used to convert quotes and dashes to 131 | # typographically correct entities. 132 | #html_use_smartypants = True 133 | 134 | # Custom sidebar templates, maps document names to template names. 135 | #html_sidebars = {} 136 | 137 | # Additional templates that should be rendered to pages, maps page names to 138 | # template names. 139 | #html_additional_pages = {} 140 | 141 | # If false, no module index is generated. 142 | #html_domain_indices = True 143 | 144 | # If false, no index is generated. 145 | #html_use_index = True 146 | 147 | # If true, the index is split into individual pages for each letter. 148 | #html_split_index = False 149 | 150 | # If true, links to the reST sources are added to the pages. 151 | #html_show_sourcelink = True 152 | 153 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 154 | #html_show_sphinx = True 155 | 156 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 157 | #html_show_copyright = True 158 | 159 | # If true, an OpenSearch description file will be output, and all pages will 160 | # contain a tag referring to it. The value of this option must be the 161 | # base URL from which the finished HTML is served. 162 | #html_use_opensearch = '' 163 | 164 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 165 | #html_file_suffix = None 166 | 167 | # Output file base name for HTML help builder. 168 | htmlhelp_basename = 'django-sitetreedoc' 169 | 170 | 171 | # -- Options for LaTeX output -------------------------------------------------- 172 | 173 | # The paper size ('letter' or 'a4'). 174 | #latex_paper_size = 'letter' 175 | 176 | # The font size ('10pt', '11pt' or '12pt'). 177 | #latex_font_size = '10pt' 178 | 179 | # Grouping the document tree into LaTeX files. List of tuples 180 | # (source start file, target name, title, author, documentclass [howto/manual]). 181 | latex_documents = [ 182 | ('index', 'django-sitetree.tex', u'django-sitetree Documentation', 183 | u'Igor \'idle sign\' Starikov', 'manual'), 184 | ] 185 | 186 | # The name of an image file (relative to this directory) to place at the top of 187 | # the title page. 188 | #latex_logo = None 189 | 190 | # For "manual" documents, if this is true, then toplevel headings are parts, 191 | # not chapters. 192 | #latex_use_parts = False 193 | 194 | # If true, show page references after internal links. 195 | #latex_show_pagerefs = False 196 | 197 | # If true, show URL addresses after external links. 198 | #latex_show_urls = False 199 | 200 | # Additional stuff for the LaTeX preamble. 201 | #latex_preamble = '' 202 | 203 | # Documents to append as an appendix to all manuals. 204 | #latex_appendices = [] 205 | 206 | # If false, no module index is generated. 207 | #latex_domain_indices = True 208 | 209 | 210 | # -- Options for manual page output -------------------------------------------- 211 | 212 | # One entry per manual page. List of tuples 213 | # (source start file, name, description, authors, manual section). 214 | man_pages = [ 215 | ('index', 'django-sitetree', u'django-sitetree Documentation', 216 | [u'Igor \'idle sign\' Starikov'], 1) 217 | ] 218 | -------------------------------------------------------------------------------- /docs/source/customization.rst: -------------------------------------------------------------------------------- 1 | Tree handler customization 2 | ========================== 3 | 4 | What to do if a time comes and you need some fancy stuff done to tree items that 5 | *django-sitetree* does not support? 6 | 7 | .. _tree-custom: 8 | 9 | It might be that you need some special tree items ordering in a menu, or you want to render 10 | a huge site tree with all articles titles that are described by one tree item in Django admin, 11 | or god knows what else. 12 | 13 | *django-sitetree* can facilitate on that as it allows tree handler customization 14 | with the help of `SITETREE_CLS` setting. 15 | 16 | 1. Subclass ``sitetreeapp.SiteTree`` and place that class into a separate module for convenience. 17 | 2. You may now override ``.apply_hook()`` to manipulate tree items before render, or any other method to customize handler to your exact needs. 18 | 3. Define ``SITETREE_CLS`` in ``settings.py`` of your project, showing it a dotted path to the subclass. 19 | 20 | 21 | Example: 22 | 23 | .. code-block:: python 24 | 25 | # myapp/mysitetree.py 26 | from sitetree.sitetreeapp import SiteTree 27 | 28 | 29 | class MySiteTree(SiteTree): 30 | """Custom tree handler to test deep customization abilities.""" 31 | 32 | def apply_hook(self, tree_items, sender): 33 | # Suppose we want to process only menu child items. 34 | if tree_sender == 'menu.children': 35 | # Lets add 'Hooked: ' to resolved titles of every item. 36 | for item in tree_items: 37 | item.title_resolved = 'Hooked: %s' % item.title_resolved 38 | # Return items list mutated or not. 39 | return tree_items 40 | 41 | # pyproject/settings.py 42 | ... 43 | 44 | SITETREE_CLS = 'myapp.mysitetree.MySiteTree' 45 | 46 | ... 47 | 48 | 49 | 50 | .. note:: 51 | 52 | You might also be interested in the notes on :ref:`Overriding SiteTree Admin representation `. 53 | -------------------------------------------------------------------------------- /docs/source/forms.rst: -------------------------------------------------------------------------------- 1 | SiteTree Forms and Fields 2 | ========================= 3 | 4 | Ocasionally you may want to link some site entities (e.g. Polls, Articles) to certain sitetree items (as to categorize 5 | them). You can achieve it with the help of generic forms and fields shipped with SiteTree. 6 | 7 | 8 | 9 | .. _forms: 10 | 11 | TreeItemForm 12 | ------------ 13 | 14 | You can inherit from that form to have a dropdown with tree items for a certain tree: 15 | 16 | .. code-block:: python 17 | 18 | from sitetree.forms import TreeItemForm 19 | 20 | 21 | class MyTreeItemForm(TreeItemForm): 22 | """We inherit from TreeItemForm to allow user link some title to sitetree item. 23 | This form besides `title` field will have `tree_item` dropdown. 24 | 25 | """ 26 | 27 | title = forms.CharField() 28 | 29 | # We instruct our form to work with `main` aliased sitetree. 30 | # And we set tree item with ID = 2 as initial. 31 | my_form = MyTreeItemForm(tree='main', tree_item=2) 32 | 33 | 34 | You can also use a well known `initial={'tree_item': 2}` approach to set an initial sitetree item. 35 | 36 | After that deal with that form just as usual. 37 | 38 | 39 | 40 | .. _fields: 41 | 42 | TreeItemChoiceField 43 | ------------------- 44 | 45 | `TreeItemChoiceField` is what `TreeItemForm` uses internally to represent sitetree items dropdown, 46 | and what used in Admin contrib on sitetree item create/edit pages. 47 | 48 | You can inherit from it (and customized it) or use it as it is in your own forms: 49 | 50 | .. code-block:: python 51 | 52 | from sitetree.fields import TreeItemChoiceField 53 | 54 | 55 | class MyField(TreeItemChoiceField): 56 | 57 | # We override template used to build select choices. 58 | template = 'my_templates/tree_combo.html' 59 | # And override root item representation. 60 | root_title = '-** Root item **-' 61 | 62 | 63 | -------------------------------------------------------------------------------- /docs/source/i18n.rst: -------------------------------------------------------------------------------- 1 | Internationalization 2 | ==================== 3 | 4 | With django-sitetree it is possible to render different trees for different active 5 | locales still addressing them by the same alias from a template. 6 | 7 | ``register_i18n_trees(aliases)`` function registers aliases of internationalized sitetrees. 8 | Internationalized sitetrees are those, which are dubbed by other trees having 9 | locale identifying suffixes in their aliases. 10 | 11 | Lets suppose ``my_tree`` is the alias of a generic tree. This tree is the one 12 | that we call by its alias in templates, and it is the one which is used 13 | if no i18n version of that tree is found. 14 | 15 | Given that ``my_tree_en``, ``my_tree_ru`` and other ``my_tree_{locale-id}``-like 16 | trees are considered internationalization sitetrees. These are used (if available) 17 | in accordance with current locale used in project. 18 | 19 | Example: 20 | 21 | .. code-block:: python 22 | 23 | # This code usually belongs to urls.py (or `ready` method of a user defined 24 | # sitetree application config if Django 1.7+). 25 | 26 | # First import the register function. 27 | from sitetree.sitetreeapp import register_i18n_trees 28 | 29 | 30 | # Now register i18n trees. 31 | register_i18n_trees(['my_tree', 'my_another_tree']) 32 | 33 | # After that you need to create trees for languages supported 34 | # in your project, e.g.: `my_tree_en`, `my_tree_ru`, `my_tree_pt-br`. 35 | 36 | # Then when we address ``my_tree`` from a template django-sitetree will render 37 | # an appropriate tree for locale currently active in your project. 38 | # See ``activate`` function from ``django.utils.translation`` 39 | # and https://docs.djangoproject.com/en/dev/topics/i18n/ 40 | # for more information. 41 | 42 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | django-sitetree documentation 2 | ============================= 3 | 4 | *django-sitetree is a reusable application for Django, introducing site tree, menu and breadcrumbs navigation elements.* 5 | 6 | Site structure in django-sitetree is described through Django admin interface in a so called site trees. 7 | Every item of such a tree describes a page or a set of pages through the relation of URI or URL to human-friendly title. Eg. using site tree editor in Django admin:: 8 | 9 | URI Title 10 | / - Site Root 11 | |_users/ - Site Users 12 | |_users/13/ - Definite User 13 | 14 | 15 | Alas the example above makes a little sense if you have more than just a few users, that's why django-sitetree supports Django template tags in item titles and Django named URLs in item URIs. 16 | 17 | If we define a named URL for user personal page in urls.py, for example, 'users-personal', we could change a scheme in the following way:: 18 | 19 | URI Title 20 | / - Site Root 21 | |_users/ - Site Users 22 | |_users-personal user.id - User Called {{ user.first_name }} 23 | 24 | After setting up site structure as a sitetree you should be able to use convenient and highly customizable site navigation means (menus, breadcrumbs and full site trees). 25 | 26 | User access to certain sitetree items can be restricted to authenticated users or more accurately with the help of Django permissions system (Auth contrib package). 27 | 28 | Sitetree also allows you to define dynamic trees in your code instead of Admin interface. And even more: you can combine those two types of trees in more sophisticated ways. 29 | 30 | 31 | Requirements 32 | ------------ 33 | 34 | 1. Python 3.7+ 35 | 2. Django 2.0+ 36 | 3. Auth Django contrib package 37 | 4. Admin site Django contrib package (optional) 38 | 39 | 40 | Table of Contents 41 | ----------------- 42 | 43 | .. toctree:: 44 | :maxdepth: 2 45 | 46 | quickstart 47 | tags 48 | i18n 49 | apps 50 | management 51 | templatesmod 52 | tagsadv 53 | customization 54 | admin 55 | forms 56 | models 57 | performance 58 | addons 59 | thirdparty 60 | 61 | 62 | See also 63 | -------- 64 | 65 | If the application is not what you want for site navigation, you might be interested in considering the other choices — http://djangopackages.com/grids/g/navigation/ 66 | -------------------------------------------------------------------------------- /docs/source/management.rst: -------------------------------------------------------------------------------- 1 | Management commands 2 | =================== 3 | 4 | SiteTree comes with two management commands which can facilitate development and deployment processes. 5 | 6 | 7 | sitetreedump 8 | ------------ 9 | 10 | Sends sitetrees from database as a fixture in JSON format to output. 11 | 12 | Output all trees and items into `treedump.json` file example:: 13 | 14 | python manage.py sitetreedump > treedump.json 15 | 16 | 17 | You can export only trees that you need by supplying their aliases separated with spaces:: 18 | 19 | python manage.py sitetreedump my_tree my_another_tree > treedump.json 20 | 21 | 22 | If you need to export only tree items without trees use ``--items_only`` command switch:: 23 | 24 | python manage.py sitetreedump --items_only my_tree > items_only_dump.json 25 | 26 | 27 | Use ``--help`` command switch to get quick help on the command:: 28 | 29 | python manage.py sitetreedump --help 30 | 31 | 32 | 33 | sitetreeload 34 | ------------ 35 | 36 | This command loads sitetrees from a fixture in JSON format into database. 37 | 38 | .. warning:: 39 | 40 | `sitetreeload` won't even try to restore permissions for sitetree items, as those should probably 41 | be tuned in production rather than exported from dev. 42 | 43 | If required you can use Django's `loaddata` management command with `sitetreedump` created dump, 44 | or the `dumpscript` from `django-extensions` to restore the permissions. 45 | 46 | 47 | Command makes use of ``--mode`` command switch to control import strategy. 48 | 49 | a) `append` (default) mode should be used when you need to extend sitetree data 50 | that is now in DB with that from a fixture. 51 | 52 | **Note:** In this mode trees and tree items identifiers from a fixture will be changed 53 | to fit existing tree structure. 54 | 55 | b) `replace` mode should be used when you need to remove all sitetree data existing 56 | in DB and replace it with that from a fixture. 57 | 58 | **Warning:** Replacement is irreversible. You should probably dump sitetree data 59 | if you think that you might need it someday. 60 | 61 | Using `replace` mode:: 62 | 63 | python manage.py sitetreeload --mode=replace treedump.json 64 | 65 | 66 | Import all trees and items from `treedump.json` file example:: 67 | 68 | python manage.py sitetreeload treedump.json 69 | 70 | 71 | Use ``--items_into_tree`` command switch and alias of target tree to import all tree 72 | items from a fixture there. This will not respect any trees information from fixture file - 73 | only tree items will be considered. **Keep in mind** also that this switch will automatically 74 | change `sitetreeload` commmand into `append` mode:: 75 | 76 | python manage.py sitetreeload --items_into_tree=my_tree items_only_dump.json 77 | 78 | 79 | Use ``--help`` command switch to get quick help on the command:: 80 | 81 | python manage.py sitetreeload --help 82 | 83 | -------------------------------------------------------------------------------- /docs/source/models.rst: -------------------------------------------------------------------------------- 1 | SiteTree Models 2 | =============== 3 | 4 | SiteTree comes with Tree and Tree item built-in models to store sitetree data. 5 | 6 | 7 | .. _models_customization: 8 | 9 | Models customization 10 | -------------------- 11 | 12 | Now let's pretend you are not satisfied with SiteTree built-in models and want to customize them. 13 | 14 | 1. First thing you should do is to define your own `tree` and `tree item` models inherited from `TreeBase` 15 | and `TreeItemBase` classes respectively: 16 | 17 | .. code-block:: python 18 | 19 | # Suppose you have `myapp` application. 20 | # In its `models.py` you define your customized models. 21 | from sitetree.models import TreeItemBase, TreeBase 22 | 23 | 24 | class MyTree(TreeBase): 25 | """This is your custom tree model. 26 | And here you add `my_tree_field` to all fields existing in `TreeBase`. 27 | 28 | """ 29 | my_tree_field = models.CharField('My tree field', max_length=50, null=True, blank=True) 30 | 31 | 32 | class MyTreeItem(TreeItemBase): 33 | """And that's a tree item model with additional `css_class` field.""" 34 | css_class = models.CharField('Tree item CSS class', max_length=50) 35 | 36 | 37 | 38 | 2. Now when `models.py` in your `myapp` application has the definitions of custom sitetree models, you need 39 | to instruct Django to use them for your project instead of built-in ones: 40 | 41 | .. code-block:: python 42 | 43 | # Somewhere in your settings.py do the following. 44 | # Here `myapp` is the name of your application, `MyTree` and `MyTreeItem` 45 | # are the names of your customized models. 46 | 47 | SITETREE_MODEL_TREE = 'myapp.MyTree' 48 | SITETREE_MODEL_TREE_ITEM = 'myapp.MyTreeItem' 49 | 50 | 51 | 3. Run `manage.py syncdb` to install your customized models into DB. 52 | 53 | 54 | .. note:: 55 | 56 | As you've added new fields to your models, you'll probably need to tune their Django Admin representation. 57 | See :ref:`Overriding SiteTree Admin representation ` for more information. 58 | 59 | 60 | .. _custom-model-sitetree: 61 | 62 | Sitetree definition with custom models 63 | -------------------------------------- 64 | 65 | Given the example model given above, you can now use the extra fields when defining a sitetree programmatically: 66 | 67 | .. code-block:: python 68 | 69 | from sitetree.utils import tree, item 70 | 71 | # Be sure you defined `sitetrees` in your module. 72 | sitetrees = ( 73 | # Define a tree with `tree` function. 74 | tree('books', items=[ 75 | # Then define items and their children with `item` function. 76 | item('Books', 'books-listing', children=[ 77 | item('Book named "{{ book.title }}"', 78 | 'books-details', 79 | in_menu=False, 80 | in_sitetree=False, 81 | css_class='book-detail'), 82 | item('Add a book', 83 | 'books-add', 84 | css_class='book-add'), 85 | item('Edit "{{ book.title }}"', 86 | 'books-edit', 87 | in_menu=False, 88 | in_sitetree=False, 89 | css_class='book-edit') 90 | ]) 91 | ], title='My books tree'), 92 | # ... You can define more than one tree for your app. 93 | ) 94 | 95 | .. _models_referencing: 96 | 97 | Models referencing 98 | ------------------ 99 | 100 | You can reference sitetree models (including customized) from other models, with the help 101 | of `MODEL_TREE`, `MODEL_TREE_ITEM` settings: 102 | 103 | 104 | .. code-block:: python 105 | 106 | from sitetree.settings import MODEL_TREE, MODEL_TREE_ITEM 107 | 108 | # As taken from the above given examples 109 | # MODEL_TREE will contain `myapp.MyTree`, MODEL_TREE_ITEM - `myapp.MyTreeItem` 110 | 111 | 112 | 113 | If you need to get current `tree` or `tree item` classes use `get_tree_model` and `get_tree_item_model` functions: 114 | 115 | .. code-block:: python 116 | 117 | from sitetree.utils import get_tree_model, get_tree_item_model 118 | 119 | current_tree_class = get_tree_model() # MyTree from myapp.models (from the example above) 120 | current_tree_item_class = get_tree_item_model() # MyTreeItem from myapp.models (from the example above) 121 | 122 | -------------------------------------------------------------------------------- /docs/source/performance.rst: -------------------------------------------------------------------------------- 1 | Performance notes 2 | ================= 3 | 4 | 5 | To avoid performance hits on large sitetrees try to simplify them, and/or reduce number of sitetree items: 6 | 7 | * Restructure (unify) sitetree items where appropriate. E.g.:: 8 | 9 | Home 10 | |-- Category "Photo" 11 | | |-- Item "{{ item.title }}" 12 | | 13 | |-- Category "Audio" 14 | | |-- Item "{{ item.title }}" 15 | | 16 | |-- etc. 17 | 18 | 19 | could be restructured into:: 20 | 21 | Home 22 | |-- Category "{{ category.title }}" 23 | | |-- Item "{{ item.title }}" 24 | | 25 | |-- etc. 26 | 27 | 28 | * Do not use ``URL as Pattern`` sitetree item option. Instead you may use hardcoded URLs. 29 | 30 | * Do not use access permissions restrictions (access rights) where not required. 31 | 32 | * Use Django templates caching machinery. 33 | 34 | * Use fast Django cache backend. 35 | 36 | .. note:: 37 | 38 | Sitetree uses Django cache framework to store trees data, but keep in mind that 39 | Django's default is `Local-memory caching `_ 40 | that is known not playing well with multiple processes (which will eventually cause sitetree to render navigation 41 | in different states for different processes), so you're advised to use the other choices. 42 | 43 | You can specify the cache backend to use, setting the ``SITETREE_CACHE_NAME`` on the django settings to specify the name 44 | of the cache to use. 45 | 46 | -------------------------------------------------------------------------------- /docs/source/quickstart.rst: -------------------------------------------------------------------------------- 1 | Getting started 2 | =============== 3 | 4 | 1. Add the **sitetree** application to INSTALLED_APPS in your settings file (usually 'settings.py'). 5 | 2. Check that *django.core.context_processors.request* is added to TEMPLATE_CONTEXT_PROCESSORS in your settings file. 6 | For Django 1.8+: *django.template.context_processors.request* should be defined in ``TEMPLATES/OPTIONS/context_processors``. 7 | 3. Check that *django.contrib.auth.context_processors.auth* is enabled in TEMPLATE_CONTEXT_PROCESSORS too. 8 | 4. Run ``./manage.py migrate`` to install sitetree tables into database. 9 | 5. Go to Django Admin site and add some trees and tree items (see :ref:`Making tree ` section). 10 | 6. Add *{% load sitetree %}* tag to the top of a template. 11 | 12 | Now you can use the following template tags: 13 | + :ref:`sitetree_menu ` - to render menu based on sitetree; 14 | + :ref:`sitetree_breadcrumbs ` - to render breadcrumbs path based on sitetree; 15 | + :ref:`sitetree_tree ` - to render site tree; 16 | + :ref:`sitetree_page_title ` - to render current page title resolved against definite sitetree. 17 | + :ref:`sitetree_page_description ` - to render current page description resolved against definite sitetree. 18 | 19 | 20 | .. _making-tree: 21 | 22 | Making a tree 23 | ------------- 24 | 25 | Taken from `StackOverflow `_. 26 | 27 | In this tutorial we create a sitetree that could handle URI like */categoryname/entryname*. 28 | 29 | ------------ 30 | 31 | To create a tree: 32 | 33 | 1. Go to site administration panel; 34 | 2. Click +Add near 'Site Trees'; 35 | 3. Enter alias for your sitetree, e.g. 'maintree'. You'll address your tree by this alias in template tags; 36 | 4. Push 'Add Site Tree Item'; 37 | 5. Create the first item:: 38 | 39 | Parent - As it is root item that would have no parent. 40 | Title - Let it be 'My site'. 41 | URL - This URL is static, so put here '/'. 42 | 43 | 6. Create a second item (that one would handle 'categoryname' from your 'categoryname/entryname'):: 44 | 45 | Parent - Choose 'My site' item from step 5. 46 | Title - Put here 'Category #{{ category.id }}'. 47 | URL - Put named URL 'category-detailed category.name'. 48 | 49 | In 'Additional settings': check 'URL as Pattern' checkbox. 50 | 51 | 7. Create a third item (that one would handle 'entryname' from your 'categoryname/entryname'):: 52 | 53 | Parent - Choose 'Category #{{ category.id }}' item from step 6. 54 | Title - Put here 'Entry #{{ entry.id }}'. 55 | URL - Put named URL 'entry-detailed category.name entry.name'. 56 | 57 | In 'Additional settings': check 'URL as Pattern' checkbox. 58 | 59 | 8. Put '{% load sitetree %}' into your template to have access to sitetree tags. 60 | 9. Put '{% sitetree_menu from "maintree" include "trunk" %}' into your template to render menu from tree trunk. 61 | 10. Put '{% sitetree_breadcrumbs from "maintree" %}' into your template to render breadcrumbs. 62 | 63 | ------------ 64 | 65 | Steps 6 and 7 clarifications: 66 | 67 | * In titles we use Django template variables, which would be resolved just like they do in your templates. 68 | 69 | E.g.: You made your view for 'categoryname' (let's call it 'detailed_category') to pass category object into template as 'category' variable. Suppose that category object has 'id' property. 70 | In your template you use '{{ category.id }}' to render id. And we do just the same for site tree item in step 6. 71 | 72 | * In URLs we use Django's named URL patterns (`documentation `_). That is almost idential to usage of Django '`url `_' tag in templates. 73 | 74 | Your urls configuration for steps 6, 7 supposed to include:: 75 | 76 | url(r'^(?P\S+)/(?P\S+)/$', 'detailed_entry', name='entry-detailed'), 77 | url(r'^(?P\S+)/$', 'detailed_category', name='category-detailed'), 78 | 79 | Consider 'name' argument values of 'url' function. 80 | 81 | So, putting 'entry-detailed category.name entry.name' in step 7 into URL field we tell sitetree to associate that sitetree item with URL named 'entry-detailed', passing to it category_name and entry_name parameters. 82 | -------------------------------------------------------------------------------- /docs/source/tags.rst: -------------------------------------------------------------------------------- 1 | SiteTree template tags 2 | ====================== 3 | 4 | To use template tags available in SiteTree you should add **{% load sitetree %}** tag to the top of chosen template. 5 | 6 | Tree tag argument (part in double quotes, following '**from**' word) of SiteTree tags should contain the tree alias. 7 | 8 | **Hints:** 9 | 10 | + Tree tag argument could be a template variable (do not use quotes for those). 11 | + Optional **template** argument could be supplied to all SitetTree tags except *sitetree_page_title* to render using different templates. 12 | It should contain path to template file. 13 | 14 | Examples:: 15 | 16 | {% sitetree_menu from "mytree" include "trunk,topmenu" template "mytrees/mymenu.html" %} 17 | {% sitetree_breadcrumbs from "mytree" template "mytrees/mybreadcrumbs.html" %} 18 | 19 | 20 | 21 | .. _tag-menu: 22 | 23 | sitetree_menu 24 | ------------- 25 | 26 | This tag renders menu based on sitetree. 27 | 28 | Usage example:: 29 | 30 | {% sitetree_menu from "mytree" include "trunk,topmenu" %} 31 | 32 | This command renders as a menu sitetree items from tree named 'mytree', including items **under** 'trunk' and 'topmenu' aliased items. 33 | That means that 'trunk' and 'topmenu' themselves won't appear in a menu, but rather all their ancestors. If you need item filtering behaviour 34 | please use :ref:`tree handler customizations `. 35 | 36 | Aliases are given to items through Django's admin site. 37 | 38 | `Note that there are some reserved aliases`. To illustrate how do they work, take a look at the sample tree:: 39 | 40 | Home 41 | |-- Users 42 | | |-- Moderators 43 | | |-- Ordinary 44 | | 45 | |-- Articles 46 | | |-- About cats 47 | | | |-- Good 48 | | | |-- Bad 49 | | | |-- Ugly 50 | | | 51 | | |-- About dogs 52 | | |-- About mice 53 | | 54 | |-- Contacts 55 | | |-- Russia 56 | | | |-- Web 57 | | | | |-- Public 58 | | | | |-- Private 59 | | | | 60 | | | |-- Postal 61 | | | 62 | | |-- Australia 63 | | |-- China 64 | Exit 65 | 66 | 67 | .. note:: 68 | 69 | As it mentioned above, basic built-in templates won't limit the depth of rendered tree, if you need to render 70 | the limited number of levels, you ought to :ref:`override the built-in templates `. 71 | For brevity rendering examples below will show only top level rendered for each alias. 72 | 73 | + **trunk** - get hierarchy under trunk, i.e. root item(s) - items without parents: 74 | 75 | Renders:: 76 | 77 | Home 78 | Exit 79 | 80 | + **this-children** - get items under item resolved as current for the current page; 81 | 82 | Considering that we are now at `Articles` renders:: 83 | 84 | About cats 85 | About dogs 86 | About mice 87 | 88 | + **this-siblings** - get items under parent of item resolved as current for the current page (current item included); 89 | 90 | Considering that we are now at `Bad` renders:: 91 | 92 | Good 93 | Bad 94 | Ugly 95 | 96 | + **this-parent-siblings** - items under parent item for the item resolved as current for the current page. 97 | 98 | Considering that we are now at `Public` renders:: 99 | 100 | Web 101 | Postal 102 | 103 | + **this-ancestor-children** - items under grandparent item (closest to root) for the item resolved as current for the current page. 104 | 105 | Considering that we are now at `Public` renders all items under `Home` (which is closest to the root). 106 | 107 | Thus in the template tag example above `trunk` is reserved alias, and `topmenu` alias is given to an item through 108 | admin site. 109 | 110 | Sitetree items could be addressed not only by aliases but also by IDs:: 111 | 112 | {% sitetree_menu from "mytree" include "10" %} 113 | 114 | 115 | 116 | .. _tag-breadcrumbs: 117 | 118 | sitetree_breadcrumbs 119 | -------------------- 120 | 121 | This tag renders breadcrumbs path (from tree root to current page) based on sitetree. 122 | 123 | Usage example:: 124 | 125 | {% sitetree_breadcrumbs from "mytree" %} 126 | 127 | This command renders breadcrumbs from tree named 'mytree'. 128 | 129 | 130 | 131 | .. _tag-tree: 132 | 133 | sitetree_tree 134 | ------------- 135 | 136 | This tag renders entire site tree. 137 | 138 | Usage example:: 139 | 140 | {% sitetree_tree from "mytree" %} 141 | 142 | This command renders sitetree from tree named 'mytree'. 143 | 144 | 145 | 146 | .. _tag-page-title: 147 | 148 | sitetree_page_title 149 | ------------------- 150 | 151 | This tag renders current page title resolved against definite sitetree. Title is taken from a sitetree item title resolved as current for the current page. 152 | 153 | Usage example:: 154 | 155 | {% sitetree_page_title from "mytree" %} 156 | 157 | This command renders current page title from tree named 'mytree'. 158 | 159 | 160 | 161 | .. _tag-page-description: 162 | 163 | sitetree_page_description 164 | ------------------------- 165 | 166 | This tag renders current page description resolved against definite sitetree. Description is taken from a sitetree item description resolved as current for the current page. 167 | 168 | That can be useful for meta description for an HTML page. 169 | 170 | Usage example:: 171 | 172 | {% sitetree_page_description from "mytree" %} 173 | 174 | This command renders current page description from tree named 'mytree'. 175 | 176 | 177 | .. _tag-page-hint: 178 | 179 | sitetree_page_hint 180 | ------------------ 181 | 182 | This tag is similar to `sitetree_page_description`, but it uses data from tree item `hint` field instead of a `description` fields. 183 | 184 | Usage example:: 185 | 186 | {% sitetree_page_hint from "mytree" %} 187 | 188 | 189 | 190 | .. _tag-ignore-errors: 191 | 192 | SITETREE_RAISE_ITEMS_ERRORS_ON_DEBUG 193 | ------------------------------------ 194 | 195 | DEFAULT: True 196 | 197 | There are some rare occasions when you want to turn off errors that are thrown by sitetree even during debug. 198 | 199 | Setting SITETREE_RAISE_ITEMS_ERRORS_ON_DEBUG = False will turn them off. 200 | -------------------------------------------------------------------------------- /docs/source/tagsadv.rst: -------------------------------------------------------------------------------- 1 | Advanced SiteTree tags 2 | ====================== 3 | 4 | .. _tags-advanced: 5 | 6 | SiteTree introduces two advanced template tags which you have to deal with in case you override the built-in sitetree templates. 7 | 8 | 9 | sitetree_children 10 | ----------------- 11 | 12 | Implements down the tree traversal with rendering. 13 | 14 | Usage example:: 15 | 16 | {% sitetree_children of someitem for menu template "sitetree/mychildren.html" %} 17 | 18 | Used to render child items of specific sitetree item 'someitem' for 'menu' navigation type, using template "sitetree/mychildren.html". 19 | 20 | Allowed navigation types: 1) *menu*; 2) *sitetree*. 21 | 22 | Basically template argument should contain path to current template itself. 23 | 24 | 25 | .. _tag-url: 26 | 27 | sitetree_url 28 | ------------ 29 | 30 | Resolves site tree item's url or url pattern. 31 | 32 | Usage example:: 33 | 34 | {% sitetree_url for someitem params %} 35 | 36 | This tag is much the same as Django built-in 'url' tag. The difference is that after 'for' it should get site tree item object. 37 | 38 | It can cast the resolved URL into a context variable when using `as` clause just like `url` tag. 39 | -------------------------------------------------------------------------------- /docs/source/thirdparty.rst: -------------------------------------------------------------------------------- 1 | Thirdparty applications support 2 | =============================== 3 | 4 | Here belongs some notes on thirdparty Django applications support in SiteTree. 5 | 6 | 7 | 8 | django-smuggler 9 | --------------- 10 | 11 | https://pypi.python.org/pypi/django-smuggler/ 12 | 13 | `Smuggler` dump and load buttons will be available on trees listing page if this app is installed 14 | allowing to dump and load site trees and items right from your browser. 15 | 16 | 17 | 18 | django-modeltranslation 19 | ----------------------- 20 | 21 | https://pypi.python.org/pypi/django-modeltranslation/ 22 | 23 | If you do not want to use the built-in `sitetree` Internationalization machinery, with `modeltranslation` you can 24 | localize your tree items into different languages. This requires some work though. 25 | 26 | 1. Create a custom sitetree item model: 27 | 28 | 29 | .. code-block:: python 30 | 31 | # models.py of some of your apps (e.g. myapp). 32 | from sitetree.models import TreeItemBase 33 | 34 | 35 | class MyTranslatableTreeItem(TreeItemBase): 36 | """This model will be used by modeltranslation.""" 37 | 38 | 39 | 2. Instruct Django to use your custom model: 40 | 41 | .. code-block:: python 42 | 43 | # setting.py of your project. 44 | SITETREE_MODEL_TREE_ITEM = 'myapp.MyTreeItem' 45 | 46 | 47 | 3. Tune up Admin contrib to handle translatable tree items: 48 | 49 | .. code-block:: python 50 | 51 | # admin.py of your application with translatable tree item model. 52 | from modeltranslation.admin import TranslationAdmin 53 | from sitetree.admin import TreeItemAdmin, override_item_admin 54 | 55 | 56 | class CustomTreeItemAdmin(TreeItemAdmin, TranslationAdmin): 57 | """This allows admin contrib to support translations for tree items.""" 58 | 59 | override_item_admin(CustomTreeItemAdmin) 60 | 61 | 62 | 4. Instruct `modeltranslation` how to handle your tree item model: 63 | 64 | .. code-block:: python 65 | 66 | # translation.py of your application. 67 | from modeltranslation.translator import translator, TranslationOptions 68 | 69 | from .models import MyTranslatableTreeItem 70 | 71 | 72 | class TreeItemTranslationOptions(TranslationOptions): 73 | 74 | # These fields are for translation. 75 | fields = ('title', 'hint', 'description') 76 | 77 | 78 | translator.register(MyTreeItem, TreeItemTranslationOptions) 79 | 80 | 81 | That's how you made `sitetree` work with `modeltranslation`. 82 | 83 | Read `django-modeltranslation` documentation for more information on tuning. 84 | 85 | 86 | django-tenants 87 | --------------- 88 | 89 | https://pypi.python.org/pypi/django-tenants/ 90 | 91 | You should use a custom cache config to make it work, configure something like this on the django cache. 92 | 93 | .. code-block:: python 94 | 95 | CACHES = { 96 | ... 97 | "sitetree_cache": { 98 | "BACKEND": "django.core.cache.backends.dummy.DummyCache", 99 | "KEY_FUNCTION": "django_tenants.cache.make_key", 100 | "REVERSE_KEY_FUNCTION": "django_tenants.cache.reverse_key", 101 | }, 102 | } 103 | 104 | SITETREE_CACHE_NAME = "sitetree_cache" 105 | 106 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --pyargs 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | release = clean --all sdist bdist_wheel upload 3 | test = pytest 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from setuptools import setup, find_packages 5 | 6 | from sitetree import VERSION 7 | 8 | f = open(os.path.join(os.path.dirname(__file__), 'README.rst')) 9 | readme = f.read() 10 | f.close() 11 | 12 | PYTEST_RUNNER = ['pytest-runner'] if 'test' in sys.argv else [] 13 | 14 | setup( 15 | name='django-sitetree', 16 | version='.'.join(map(str, VERSION)), 17 | url='http://github.com/idlesign/django-sitetree', 18 | 19 | description='This reusable Django app introduces site tree, menu and breadcrumbs navigation elements', 20 | long_description=readme, 21 | license='BSD 3-Clause License', 22 | 23 | author='Igor `idle sign` Starikov', 24 | author_email='idlesign@yandex.ru', 25 | 26 | packages=find_packages(), 27 | include_package_data=True, 28 | zip_safe=False, 29 | 30 | setup_requires=[] + PYTEST_RUNNER, 31 | tests_require=[ 32 | 'pytest', 33 | 'pytest-djangoapp>=0.15.1', 34 | ], 35 | 36 | classifiers=[ 37 | 'Development Status :: 5 - Production/Stable', 38 | 'Environment :: Web Environment', 39 | 'Framework :: Django', 40 | 'Intended Audience :: Developers', 41 | 'License :: OSI Approved :: BSD License', 42 | 'Operating System :: OS Independent', 43 | 'Programming Language :: Python', 44 | 'Programming Language :: Python :: 3', 45 | 'Programming Language :: Python :: 3.7', 46 | 'Programming Language :: Python :: 3.8', 47 | 'Programming Language :: Python :: 3.9', 48 | 'Programming Language :: Python :: 3.10', 49 | 'Programming Language :: Python :: 3.11', 50 | ], 51 | ) 52 | -------------------------------------------------------------------------------- /sitetree/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = (1, 18, 0) 2 | 3 | 4 | default_app_config = 'sitetree.apps.SitetreeConfig' -------------------------------------------------------------------------------- /sitetree/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class SitetreeConfig(AppConfig): 6 | """Sitetree configuration.""" 7 | 8 | name: str = 'sitetree' 9 | verbose_name: str = _('Site Trees') 10 | default_auto_field = 'django.db.models.AutoField' 11 | -------------------------------------------------------------------------------- /sitetree/compat.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | try: 4 | from django.template.base import TokenType 5 | TOKEN_BLOCK = TokenType.BLOCK 6 | TOKEN_TEXT = TokenType.TEXT 7 | TOKEN_VAR = TokenType.VAR 8 | except ImportError: 9 | from django.template.base import TOKEN_BLOCK, TOKEN_TEXT, TOKEN_VAR 10 | 11 | 12 | class CommandOption: 13 | """Command line option wrapper.""" 14 | 15 | def __init__(self, *args, **kwargs): 16 | self.args = args 17 | self.kwargs = kwargs 18 | 19 | 20 | def options_getter(command_options): 21 | """Compatibility function to get rid of optparse in management commands after Django 1.10. 22 | 23 | :param tuple command_options: tuple with `CommandOption` objects. 24 | 25 | """ 26 | def get_options(option_func: Callable = None): 27 | from optparse import make_option 28 | 29 | func = option_func or make_option 30 | options = tuple([func(*option.args, **option.kwargs) for option in command_options]) 31 | 32 | return [] if option_func is None else options 33 | 34 | return get_options 35 | -------------------------------------------------------------------------------- /sitetree/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class SiteTreeError(Exception): 4 | """Exception class for sitetree application.""" 5 | -------------------------------------------------------------------------------- /sitetree/fields.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from django import template 4 | from django.template.base import Parser, Token 5 | from django.forms import ChoiceField 6 | from django.utils.safestring import mark_safe 7 | 8 | from .compat import TOKEN_BLOCK 9 | from .templatetags.sitetree import sitetree_tree 10 | from .utils import get_tree_model, get_tree_item_model 11 | from .settings import ITEMS_FIELD_ROOT_ID 12 | 13 | if False: # pragma: nocover 14 | from .models import TreeItemBase, TreeBase # noqa 15 | 16 | 17 | MODEL_TREE_CLASS = get_tree_model() 18 | MODEL_TREE_ITEM_CLASS = get_tree_item_model() 19 | 20 | 21 | class TreeItemChoiceField(ChoiceField): 22 | """Generic sitetree item field. 23 | Customized ChoiceField with TreeItems of a certain tree. 24 | 25 | Accepts the `tree` kwarg - tree model or alias. 26 | Use `initial` kwarg to set initial sitetree item by its ID. 27 | 28 | """ 29 | template: str = 'admin/sitetree/tree/tree_combo.html' 30 | root_title: str = '---------' 31 | 32 | def __init__( 33 | self, 34 | tree: 'TreeBase' = None, 35 | required: bool = True, 36 | widget=None, 37 | label=None, 38 | initial=None, 39 | help_text=None, 40 | *args, **kwargs 41 | ): 42 | super().__init__( 43 | required=required, widget=widget, label=label, initial=initial, 44 | help_text=help_text, *args, **kwargs) 45 | 46 | self.tree = None 47 | self.choices_init(tree) 48 | 49 | def choices_init(self, tree: Optional['TreeBase']): 50 | """Initialize choices for the given tree. 51 | 52 | :param tree: 53 | 54 | """ 55 | if not tree: 56 | return 57 | 58 | if isinstance(tree, MODEL_TREE_CLASS): 59 | tree = tree.alias 60 | 61 | self.tree = tree 62 | self.choices = self._build_choices() 63 | 64 | def _build_choices(self): 65 | """Build choices list runtime using 'sitetree_tree' tag""" 66 | tree_token = f'sitetree_tree from "{self.tree}" template "{self.template}"' 67 | 68 | context_kwargs = {'current_app': 'admin'} 69 | context = template.Context(context_kwargs) 70 | context.update({'request': object()}) 71 | 72 | choices_str = sitetree_tree( 73 | Parser([]), Token(token_type=TOKEN_BLOCK, contents=tree_token) 74 | ).render(context) 75 | 76 | tree_choices = [(ITEMS_FIELD_ROOT_ID, self.root_title)] 77 | 78 | for line in choices_str.splitlines(): 79 | if line.strip(): 80 | splitted = line.split(':::') 81 | tree_choices.append((splitted[0], mark_safe(splitted[1]))) 82 | 83 | return tree_choices 84 | 85 | def clean(self, value): 86 | if not value: 87 | return None 88 | 89 | try: 90 | return MODEL_TREE_ITEM_CLASS.objects.get(pk=value) 91 | 92 | except MODEL_TREE_ITEM_CLASS.DoesNotExist: 93 | return None 94 | -------------------------------------------------------------------------------- /sitetree/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from .fields import TreeItemChoiceField 4 | 5 | 6 | class TreeItemForm(forms.Form): 7 | """Generic sitetree form. 8 | 9 | Accepts the following kwargs: 10 | 11 | - `tree`: tree model or alias 12 | - `tree_item`: ID of an initial tree item 13 | 14 | """ 15 | choice_field_class = TreeItemChoiceField 16 | 17 | def __init__(self, *args, **kwargs): 18 | tree = kwargs.pop('tree', None) 19 | tree_item = kwargs.pop('tree_item', None) 20 | super().__init__(*args, **kwargs) 21 | 22 | # autocomplete off - deals with Firefox form caching 23 | # https://bugzilla.mozilla.org/show_bug.cgi?id=46845 24 | self.fields['tree_item'] = self.choice_field_class( 25 | tree, initial=tree_item, widget=forms.Select(attrs={'autocomplete': 'off'})) 26 | -------------------------------------------------------------------------------- /sitetree/locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idlesign/django-sitetree/37a0c347eaac91f570aa5bf73c55c75035414660/sitetree/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /sitetree/locale/en/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 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2013-09-25 22:11+0700\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: admin.py:81 21 | msgid "Basic settings" 22 | msgstr "" 23 | 24 | #: admin.py:84 25 | msgid "Access settings" 26 | msgstr "" 27 | 28 | #: admin.py:88 29 | msgid "Display settings" 30 | msgstr "" 31 | 32 | #: admin.py:92 33 | msgid "Additional settings" 34 | msgstr "" 35 | 36 | #: admin.py:153 37 | msgid "" 38 | "You are seeing this warning because \"URL as Pattern\" option is active and " 39 | "pattern entered above seems to be invalid. Currently registered URL pattern " 40 | "names and parameters: " 41 | msgstr "" 42 | 43 | #: admin.py:230 44 | msgid "Item's parent left unchanged. Item couldn't be parent to itself." 45 | msgstr "" 46 | 47 | #: models.py:31 models.py:56 templates/admin/sitetree/tree/change_form.html:41 48 | msgid "Title" 49 | msgstr "" 50 | 51 | #: models.py:31 52 | msgid "Site tree title for presentational purposes." 53 | msgstr "" 54 | 55 | #: models.py:32 models.py:62 56 | msgid "Alias" 57 | msgstr "" 58 | 59 | #: models.py:32 60 | msgid "" 61 | "Short name to address site tree from templates.
Note: change " 62 | "with care." 63 | msgstr "" 64 | 65 | #: models.py:36 models.py:60 66 | msgid "Site Tree" 67 | msgstr "" 68 | 69 | #: models.py:37 70 | msgid "Site Trees" 71 | msgstr "" 72 | 73 | #: models.py:52 74 | msgid "Any" 75 | msgstr "" 76 | 77 | #: models.py:53 78 | msgid "All" 79 | msgstr "" 80 | 81 | #: models.py:56 82 | msgid "" 83 | "Site tree item title. Can contain template variables E.g.: {{ mytitle }}." 84 | msgstr "" 85 | 86 | #: models.py:57 87 | msgid "Hint" 88 | msgstr "" 89 | 90 | #: models.py:57 91 | msgid "Some additional information about this item that is used as a hint." 92 | msgstr "" 93 | 94 | #: models.py:58 templates/admin/sitetree/tree/change_form.html:42 95 | msgid "URL" 96 | msgstr "" 97 | 98 | #: models.py:58 99 | msgid "Exact URL or URL pattern (see \"Additional settings\") for this item." 100 | msgstr "" 101 | 102 | #: models.py:59 103 | msgid "URL as Pattern" 104 | msgstr "" 105 | 106 | #: models.py:59 107 | msgid "" 108 | "Whether the given URL should be treated as a pattern.
Note: " 109 | "Refer to Django \"URL dispatcher\" documentation (e.g. \"Naming URL patterns" 110 | "\" part)." 111 | msgstr "" 112 | 113 | #: models.py:60 114 | msgid "Site tree this item belongs to." 115 | msgstr "" 116 | 117 | #: models.py:61 templates/admin/sitetree/tree/change_form.html:34 118 | msgid "Hidden" 119 | msgstr "" 120 | 121 | #: models.py:61 122 | msgid "Whether to show this item in navigation." 123 | msgstr "" 124 | 125 | #: models.py:62 126 | #, python-format 127 | msgid "" 128 | "Short name to address site tree item from a template.
Reserved " 129 | "aliases: \"%s\"." 130 | msgstr "" 131 | 132 | #: models.py:63 133 | msgid "Description" 134 | msgstr "" 135 | 136 | #: models.py:63 137 | msgid "Additional comments on this item." 138 | msgstr "" 139 | 140 | #: models.py:64 141 | msgid "Show in menu" 142 | msgstr "" 143 | 144 | #: models.py:64 145 | msgid "Whether to show this item in a menu." 146 | msgstr "" 147 | 148 | #: models.py:65 149 | msgid "Show in breadcrumb path" 150 | msgstr "" 151 | 152 | #: models.py:65 153 | msgid "Whether to show this item in a breadcrumb path." 154 | msgstr "" 155 | 156 | #: models.py:66 157 | msgid "Show in site tree" 158 | msgstr "" 159 | 160 | #: models.py:66 161 | msgid "Whether to show this item in a site tree." 162 | msgstr "" 163 | 164 | #: models.py:67 165 | msgid "Logged in only" 166 | msgstr "" 167 | 168 | #: models.py:67 169 | msgid "Check it to grant access to this item to authenticated users only." 170 | msgstr "" 171 | 172 | #: models.py:68 templates/admin/sitetree/tree/change_form.html:40 173 | msgid "Guests only" 174 | msgstr "" 175 | 176 | #: models.py:68 177 | msgid "Check it to grant access to this item to guests only." 178 | msgstr "" 179 | 180 | #: models.py:69 181 | msgid "Restrict access to permissions" 182 | msgstr "" 183 | 184 | #: models.py:69 185 | msgid "" 186 | "Check it to restrict user access to this item, using Django permissions " 187 | "system." 188 | msgstr "" 189 | 190 | #: models.py:70 191 | msgid "Permissions granting access" 192 | msgstr "" 193 | 194 | #: models.py:71 195 | msgid "Permissions interpretation" 196 | msgstr "" 197 | 198 | #: models.py:71 199 | msgid "" 200 | "Any — user should have any of chosen permissions. All " 201 | "— user should have all chosen permissions." 202 | msgstr "" 203 | 204 | #: models.py:74 205 | msgid "Parent" 206 | msgstr "" 207 | 208 | #: models.py:74 209 | msgid "Parent site tree item." 210 | msgstr "" 211 | 212 | #: models.py:75 templates/admin/sitetree/tree/change_form.html:43 213 | msgid "Sort order" 214 | msgstr "" 215 | 216 | #: models.py:75 217 | msgid "Item position among other site tree items under the same parent." 218 | msgstr "" 219 | 220 | #: models.py:89 221 | msgid "Site Tree Item" 222 | msgstr "" 223 | 224 | #: models.py:90 templates/admin/sitetree/tree/change_form.html:17 225 | msgid "Site Tree Items" 226 | msgstr "" 227 | 228 | #: templates/admin/sitetree/tree/change_form.html:24 229 | msgid "Add Site Tree item" 230 | msgstr "" 231 | 232 | #: templates/admin/sitetree/tree/change_form.html:35 233 | msgid "Menu" 234 | msgstr "" 235 | 236 | #: templates/admin/sitetree/tree/change_form.html:36 237 | msgid "Breadcrumbs" 238 | msgstr "" 239 | 240 | #: templates/admin/sitetree/tree/change_form.html:37 241 | msgid "Tree" 242 | msgstr "" 243 | 244 | #: templates/admin/sitetree/tree/change_form.html:38 245 | msgid "Restricted" 246 | msgstr "" 247 | 248 | #: templates/admin/sitetree/tree/change_form.html:39 249 | msgid "Users only" 250 | msgstr "" 251 | 252 | #: templates/admin/sitetree/tree/tree.html:23 253 | msgid "Move up" 254 | msgstr "" 255 | 256 | #: templates/admin/sitetree/tree/tree.html:25 257 | msgid "Move down" 258 | msgstr "" 259 | 260 | #: templates/admin/sitetree/treeitem/breadcrumbs.html:5 261 | msgid "Home" 262 | msgstr "" 263 | 264 | #: templates/admin/sitetree/treeitem/breadcrumbs.html:11 265 | msgid "Delete" 266 | msgstr "" 267 | 268 | #: templates/admin/sitetree/treeitem/breadcrumbs.html:15 269 | msgid "History" 270 | msgstr "" 271 | 272 | #: templates/admin/sitetree/treeitem/breadcrumbs.html:17 273 | msgid "Add" 274 | msgstr "" 275 | -------------------------------------------------------------------------------- /sitetree/locale/es/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idlesign/django-sitetree/37a0c347eaac91f570aa5bf73c55c75035414660/sitetree/locale/es/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /sitetree/locale/es/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 | # 5 | # Translators: 6 | # AdrianLC , 2013 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: django-sitetree\n" 10 | "Report-Msgid-Bugs-To: https://github.com/idlesign/django-sitetree/issues\n" 11 | "POT-Creation-Date: 2012-09-11 22:07+0700\n" 12 | "PO-Revision-Date: 2013-07-22 16:52+0000\n" 13 | "Last-Translator: AdrianLC \n" 14 | "Language-Team: Spanish (Spain) (http://www.transifex.com/projects/p/django-sitetree/language/es_ES/)\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Language: es_ES\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: admin.py:54 22 | msgid "Basic settings" 23 | msgstr "Ajustes básicos" 24 | 25 | #: admin.py:57 26 | msgid "Access settings" 27 | msgstr "Ajustes de acceso" 28 | 29 | #: admin.py:61 30 | msgid "Display settings" 31 | msgstr "Ajustes de visualización" 32 | 33 | #: admin.py:65 34 | msgid "Additional settings" 35 | msgstr "Ajustes adicionales" 36 | 37 | #: admin.py:148 38 | msgid "" 39 | "You are seeing this warning because \"URL as Pattern\" option is active and " 40 | "pattern entered above seems to be invalid. Currently registered URL pattern " 41 | "names and parameters: " 42 | msgstr "Estás viendo esta advertencia porque la opción \"URL como Patrón\" está activada y el patrón introducido no es válido. Nombres de patrones registrados actualmente y sus parámetros :" 43 | 44 | #: admin.py:211 45 | msgid "Item's parent left unchanged. Item couldn't be parent to itself." 46 | msgstr "El padre de la sección no se modificó. La sección no puede ser padre de sí misma." 47 | 48 | #: models.py:26 models.py:46 templates/admin/sitetree/tree/change_form.html:40 49 | msgid "Title" 50 | msgstr "Título" 51 | 52 | #: models.py:26 53 | msgid "Site tree title for presentational purposes." 54 | msgstr "Título representativo del mapa web." 55 | 56 | #: models.py:27 models.py:52 57 | msgid "Alias" 58 | msgstr "Alias" 59 | 60 | #: models.py:27 61 | msgid "" 62 | "Short name to address site tree from templates.
Note: change " 63 | "with care." 64 | msgstr "Nombre corto para referirse al mapa web desde las plantillas.
Nota:Modifíquese con precaución." 65 | 66 | #: models.py:30 models.py:50 67 | msgid "Site Tree" 68 | msgstr "Mapa Web" 69 | 70 | #: models.py:31 71 | msgid "Site Trees" 72 | msgstr "Mapas Web" 73 | 74 | #: models.py:42 75 | msgid "Any" 76 | msgstr "Alguno" 77 | 78 | #: models.py:43 79 | msgid "All" 80 | msgstr "Todos" 81 | 82 | #: models.py:46 83 | msgid "" 84 | "Site tree item title. Can contain template variables E.g.: {{ mytitle }}." 85 | msgstr "Título para la sección del mapa web. Puede contener variables de plantilla Ej.: {{ mititulo }}." 86 | 87 | #: models.py:47 88 | msgid "Hint" 89 | msgstr "Indicación" 90 | 91 | #: models.py:47 92 | msgid "Some additional information about this item that is used as a hint." 93 | msgstr "Información adicional sobre esta sección que se usará como indicación." 94 | 95 | #: models.py:48 templates/admin/sitetree/tree/change_form.html:41 96 | msgid "URL" 97 | msgstr "URL" 98 | 99 | #: models.py:48 100 | msgid "Exact URL or URL pattern (see \"Additional settings\") for this item." 101 | msgstr "URL exacta o patrón de URL (diríjase a \"Ajustes adicionales\") de esta sección." 102 | 103 | #: models.py:49 104 | msgid "URL as Pattern" 105 | msgstr "URL como Patrón" 106 | 107 | #: models.py:49 108 | msgid "" 109 | "Whether the given URL should be treated as a pattern.
Note: " 110 | "Refer to Django \"URL dispatcher\" documentation (e.g. \"Naming URL " 111 | "patterns\" part)." 112 | msgstr "Si la URL proporcionada debe de tratarse como un patrón.
Véase la documentación de Django sobre \"URL dispatcher\" (en inglés) (Por ej. la sección \"Naming URL patterns\")." 113 | 114 | #: models.py:50 115 | msgid "Site tree this item belongs to." 116 | msgstr "Mapa web al que pertenece esta sección." 117 | 118 | #: models.py:51 templates/admin/sitetree/tree/change_form.html:34 119 | msgid "Hidden" 120 | msgstr "Oculto" 121 | 122 | #: models.py:51 123 | msgid "Whether to show this item in navigation." 124 | msgstr "Si se debe mostrar esta sección en la navegación." 125 | 126 | #: models.py:52 127 | msgid "" 128 | "Short name to address site tree item from a template.
Reserved " 129 | "aliases: \"trunk\", \"this-children\", \"this-siblings\" and \"this-" 130 | "ancestor-children\"." 131 | msgstr "Nombre corto para referirse al mapa web desde una plantilla.
Alias reservados: \"trunk\", \"this-childen\", \"this-siblings\" y \"this-ancestor-childen\"." 132 | 133 | #: models.py:53 134 | msgid "Description" 135 | msgstr "Descripción" 136 | 137 | #: models.py:53 138 | msgid "Additional comments on this item." 139 | msgstr "Comentarios adicionales sobre esta sección" 140 | 141 | #: models.py:54 142 | msgid "Show in menu" 143 | msgstr "Mostrar en el menú" 144 | 145 | #: models.py:54 146 | msgid "Whether to show this item in a menu." 147 | msgstr "Si se debe mostrar esta sección en el menú." 148 | 149 | #: models.py:55 150 | msgid "Show in breadcrumb path" 151 | msgstr "Mostrar en las migas de pan (breadcumbs)" 152 | 153 | #: models.py:55 154 | msgid "Whether to show this item in a breadcrumb path." 155 | msgstr "Mostrar o no esta sección en las migas de pan." 156 | 157 | #: models.py:56 158 | msgid "Show in site tree" 159 | msgstr "Mostrar en el mapa web" 160 | 161 | #: models.py:56 162 | msgid "Whether to show this item in a site tree." 163 | msgstr "Si se debe mostrar esta sección en el mapa web." 164 | 165 | #: models.py:57 166 | msgid "Logged in only" 167 | msgstr "Solo con sesión iniciada" 168 | 169 | #: models.py:57 170 | msgid "Check it to grant access to this item to authenticated users only." 171 | msgstr "Marcar para permitir el acceso a esta sección exclusivamente a usuarios identificados." 172 | 173 | #: models.py:58 174 | msgid "Restrict access to permissions" 175 | msgstr "Restringir acceso según permisos" 176 | 177 | #: models.py:58 178 | msgid "" 179 | "Check it to restrict user access to this item, using Django permissions " 180 | "system." 181 | msgstr "Marcar para restringir el acceso a esta sección mediante el sistema de permisos de Django." 182 | 183 | #: models.py:59 184 | msgid "Permissions granting access" 185 | msgstr "Permisos que conceden acceso" 186 | 187 | #: models.py:60 188 | msgid "Permissions interpretation" 189 | msgstr "Interpretación de los permisos" 190 | 191 | #: models.py:63 192 | msgid "Parent" 193 | msgstr "Padre" 194 | 195 | #: models.py:63 196 | msgid "Parent site tree item." 197 | msgstr "Sección padre en el mapa web." 198 | 199 | #: models.py:64 templates/admin/sitetree/tree/change_form.html:42 200 | msgid "Sort order" 201 | msgstr "Orden de aparición" 202 | 203 | #: models.py:64 204 | msgid "Item position among other site tree items under the same parent." 205 | msgstr "Posición de la sección entre las demás secciones del mapa web con el mismo padre." 206 | 207 | #: models.py:77 208 | msgid "Site Tree Item" 209 | msgstr "Sección de Mapa Web" 210 | 211 | #: models.py:78 templates/admin/sitetree/tree/change_form.html:17 212 | msgid "Site Tree Items" 213 | msgstr "Secciones de Mapa Web" 214 | 215 | #: templates/admin/sitetree/tree/change_form.html:24 216 | msgid "Add Site Tree item" 217 | msgstr "Añadir Sección de Mapa Web" 218 | 219 | #: templates/admin/sitetree/tree/change_form.html:35 220 | msgid "Menu" 221 | msgstr "Menú" 222 | 223 | #: templates/admin/sitetree/tree/change_form.html:36 224 | msgid "Breadcrumbs" 225 | msgstr "Migas de Pan" 226 | 227 | #: templates/admin/sitetree/tree/change_form.html:37 228 | msgid "Tree" 229 | msgstr "Mapa" 230 | 231 | #: templates/admin/sitetree/tree/change_form.html:38 232 | msgid "Rights Restriction" 233 | msgstr "Restricción por permisos" 234 | 235 | #: templates/admin/sitetree/tree/change_form.html:39 236 | msgid "For logged in" 237 | msgstr "Solo sesión iniciada" 238 | 239 | #: templates/admin/sitetree/tree/tree.html:22 240 | msgid "Move up" 241 | msgstr "Desplazar hacia arriba" 242 | 243 | #: templates/admin/sitetree/tree/tree.html:24 244 | msgid "Move down" 245 | msgstr "Desplazar hacia abajo" 246 | -------------------------------------------------------------------------------- /sitetree/locale/fa/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idlesign/django-sitetree/37a0c347eaac91f570aa5bf73c55c75035414660/sitetree/locale/fa/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /sitetree/locale/fa/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 | # 5 | # Translators: 6 | # Ali Javadi , 2013. 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: django-sitetree\n" 10 | "Report-Msgid-Bugs-To: https://github.com/idlesign/django-sitetree/issues\n" 11 | "POT-Creation-Date: 2012-09-11 22:07+0700\n" 12 | "PO-Revision-Date: 2013-01-19 17:49+0000\n" 13 | "Last-Translator: rohamn \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Language: fa\n" 19 | "Plural-Forms: nplurals=1; plural=0;\n" 20 | 21 | #: admin.py:54 22 | msgid "Basic settings" 23 | msgstr "تنظیمات پایه" 24 | 25 | #: admin.py:57 26 | msgid "Access settings" 27 | msgstr "تنظیمات دسترسی" 28 | 29 | #: admin.py:61 30 | msgid "Display settings" 31 | msgstr "تنظیمات نمایش" 32 | 33 | #: admin.py:65 34 | msgid "Additional settings" 35 | msgstr "تنظیمات اضافی" 36 | 37 | #: admin.py:148 38 | msgid "" 39 | "You are seeing this warning because \"URL as Pattern\" option is active and " 40 | "pattern entered above seems to be invalid. Currently registered URL pattern " 41 | "names and parameters: " 42 | msgstr "شما این پیغام را می‌بینید چون گزینه‌ی «آدرس به عنوان قالب» فعال است و قالب ارائه شده غلط به نظر می‌رسد. قالب و متغیر‌های قالب‌های ثبت شده در حال حاظر: " 43 | 44 | #: admin.py:211 45 | msgid "Item's parent left unchanged. Item couldn't be parent to itself." 46 | msgstr "ریشه‌‌ی آیتم بدون تغییر باقی مانده است. آیتم نمی‌تواند ریشه‌ی خود باشد." 47 | 48 | #: models.py:26 models.py:46 templates/admin/sitetree/tree/change_form.html:40 49 | msgid "Title" 50 | msgstr "عنوان" 51 | 52 | #: models.py:26 53 | msgid "Site tree title for presentational purposes." 54 | msgstr "عنوان نمایشی منو." 55 | 56 | #: models.py:27 models.py:52 57 | msgid "Alias" 58 | msgstr "لقب" 59 | 60 | #: models.py:27 61 | msgid "" 62 | "Short name to address site tree from templates.
Note: change " 63 | "with care." 64 | msgstr "نامی کوتاه برای آدرس دهی درقالب‌های کد. نکته: با دقت تغییر دهید." 65 | 66 | #: models.py:30 models.py:50 67 | msgid "Site Tree" 68 | msgstr "شاخه‌های سایت" 69 | 70 | #: models.py:31 71 | msgid "Site Trees" 72 | msgstr "شاخه‌های سایت" 73 | 74 | #: models.py:42 75 | msgid "Any" 76 | msgstr "هر" 77 | 78 | #: models.py:43 79 | msgid "All" 80 | msgstr "همه" 81 | 82 | #: models.py:46 83 | msgid "" 84 | "Site tree item title. Can contain template variables E.g.: {{ mytitle }}." 85 | msgstr "عنوان شاخه‌ی سایت. می‌تواند حاوی متغیر‌های قالب‌های کد باشد. مانند: {{ mytitle }}" 86 | 87 | #: models.py:47 88 | msgid "Hint" 89 | msgstr "راهنمایی" 90 | 91 | #: models.py:47 92 | msgid "Some additional information about this item that is used as a hint." 93 | msgstr "برخی اطلاعات اضافی در مورد این آیتم که می‌تواند به عنوان راهنمایی استفاده شود." 94 | 95 | #: models.py:48 templates/admin/sitetree/tree/change_form.html:41 96 | msgid "URL" 97 | msgstr "آدرس" 98 | 99 | #: models.py:48 100 | msgid "Exact URL or URL pattern (see \"Additional settings\") for this item." 101 | msgstr "آدرس دقبق یا قالب آدرس ( برای تنظیمات اضافی را ببینید)" 102 | 103 | #: models.py:49 104 | msgid "URL as Pattern" 105 | msgstr "آدرس به صورت قالب" 106 | 107 | #: models.py:49 108 | msgid "" 109 | "Whether the given URL should be treated as a pattern.
Note: " 110 | "Refer to Django \"URL dispatcher\" documentation (e.g. \"Naming URL " 111 | "patterns\" part)." 112 | msgstr "اینکه با این آدرس باید به صورت قالب برخورد شود یا نه. " 113 | 114 | #: models.py:50 115 | msgid "Site tree this item belongs to." 116 | msgstr "شاخه‌ای که این آیتم به آن متعلق است." 117 | 118 | #: models.py:51 templates/admin/sitetree/tree/change_form.html:34 119 | msgid "Hidden" 120 | msgstr "پنهان" 121 | 122 | #: models.py:51 123 | msgid "Whether to show this item in navigation." 124 | msgstr "اینکهاین آیتم باید درمسیریابی استفاده شود یا نه." 125 | 126 | #: models.py:52 127 | msgid "" 128 | "Short name to address site tree item from a template.
Reserved " 129 | "aliases: \"trunk\", \"this-children\", \"this-siblings\" and \"this-" 130 | "ancestor-children\"." 131 | msgstr "نامی کوتاه که برای صدا زدن شاخه‌های سایت در یک قالب استفاده می‌شود. لقب‌های رزرو شده: \"trunk\" و \"this-children\" و \"this-siblings\" و \"this-ancestor-children\"" 132 | 133 | #: models.py:53 134 | msgid "Description" 135 | msgstr "توضیحات" 136 | 137 | #: models.py:53 138 | msgid "Additional comments on this item." 139 | msgstr "توضیحات اضافی روی این آیتم." 140 | 141 | #: models.py:54 142 | msgid "Show in menu" 143 | msgstr "نمایش در منو" 144 | 145 | #: models.py:54 146 | msgid "Whether to show this item in a menu." 147 | msgstr "اینکه این آیتم در یک منو به نمایش در آید یا نه." 148 | 149 | #: models.py:55 150 | msgid "Show in breadcrumb path" 151 | msgstr "نمایش در نقشه‌ی سایت." 152 | 153 | #: models.py:55 154 | msgid "Whether to show this item in a breadcrumb path." 155 | msgstr "اینکه این آیتم درنقشه‌ی سایت به نمایش درآید یا نه." 156 | 157 | #: models.py:56 158 | msgid "Show in site tree" 159 | msgstr "نمایش در شاخه‌های سایت" 160 | 161 | #: models.py:56 162 | msgid "Whether to show this item in a site tree." 163 | msgstr "اینکه این آیتم در شاخه‌های سایت به نمایش در آید یا نه." 164 | 165 | #: models.py:57 166 | msgid "Logged in only" 167 | msgstr "کاربران وارد شده" 168 | 169 | #: models.py:57 170 | msgid "Check it to grant access to this item to authenticated users only." 171 | msgstr "تیک بزنید تا دسترسی این آیتم تنها به کاربرای تصدیق هویت شده محدود شود." 172 | 173 | #: models.py:58 174 | msgid "Restrict access to permissions" 175 | msgstr "محدود کردن دسترسی‌ها به اجازه‌ها" 176 | 177 | #: models.py:58 178 | msgid "" 179 | "Check it to restrict user access to this item, using Django permissions " 180 | "system." 181 | msgstr "تیک بزنید تا دسترسی کاربر را به اجازه‌ی دسترسی‌های خاص محدود کنید." 182 | 183 | #: models.py:59 184 | msgid "Permissions granting access" 185 | msgstr "اجازه‌های دسترسی مجاز" 186 | 187 | #: models.py:60 188 | msgid "Permissions interpretation" 189 | msgstr "تفسیر اجازه‌های دسترسی" 190 | 191 | #: models.py:63 192 | msgid "Parent" 193 | msgstr "ریشه" 194 | 195 | #: models.py:63 196 | msgid "Parent site tree item." 197 | msgstr "شاخه‌ی ریشه." 198 | 199 | #: models.py:64 templates/admin/sitetree/tree/change_form.html:42 200 | msgid "Sort order" 201 | msgstr "ترتیب" 202 | 203 | #: models.py:64 204 | msgid "Item position among other site tree items under the same parent." 205 | msgstr "محل جاگیری آیتم در شاخه." 206 | 207 | #: models.py:77 208 | msgid "Site Tree Item" 209 | msgstr "آیتم شاخه‌های سایت" 210 | 211 | #: models.py:78 templates/admin/sitetree/tree/change_form.html:17 212 | msgid "Site Tree Items" 213 | msgstr "آیتم‌های شاخه‌های سایت" 214 | 215 | #: templates/admin/sitetree/tree/change_form.html:24 216 | msgid "Add Site Tree item" 217 | msgstr "اضافه کردن شاخه" 218 | 219 | #: templates/admin/sitetree/tree/change_form.html:35 220 | msgid "Menu" 221 | msgstr "منو" 222 | 223 | #: templates/admin/sitetree/tree/change_form.html:36 224 | msgid "Breadcrumbs" 225 | msgstr "نقشه‌ی سایت" 226 | 227 | #: templates/admin/sitetree/tree/change_form.html:37 228 | msgid "Tree" 229 | msgstr "شاخه" 230 | 231 | #: templates/admin/sitetree/tree/change_form.html:38 232 | msgid "Rights Restriction" 233 | msgstr "محدود کردن حقوق" 234 | 235 | #: templates/admin/sitetree/tree/change_form.html:39 236 | msgid "For logged in" 237 | msgstr "برای کاربران وارد شده" 238 | 239 | #: templates/admin/sitetree/tree/tree.html:22 240 | msgid "Move up" 241 | msgstr "بالا بردن" 242 | 243 | #: templates/admin/sitetree/tree/tree.html:24 244 | msgid "Move down" 245 | msgstr "ایین آوردن" 246 | -------------------------------------------------------------------------------- /sitetree/locale/fr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idlesign/django-sitetree/37a0c347eaac91f570aa5bf73c55c75035414660/sitetree/locale/fr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /sitetree/locale/ja/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idlesign/django-sitetree/37a0c347eaac91f570aa5bf73c55c75035414660/sitetree/locale/ja/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /sitetree/locale/nb/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idlesign/django-sitetree/37a0c347eaac91f570aa5bf73c55c75035414660/sitetree/locale/nb/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /sitetree/locale/ru/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idlesign/django-sitetree/37a0c347eaac91f570aa5bf73c55c75035414660/sitetree/locale/ru/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /sitetree/locale/uk/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idlesign/django-sitetree/37a0c347eaac91f570aa5bf73c55c75035414660/sitetree/locale/uk/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /sitetree/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: django-sitetree\n" 9 | "Report-Msgid-Bugs-To: https://github.com/idlesign/django-sitetree/issues\n" 10 | "POT-Creation-Date: 2011-04-24 11:22+0700\n" 11 | "PO-Revision-Date: 2011-04-24 12:15+0000\n" 12 | "Last-Translator: Sergiy_Gavrylov \n" 13 | "Language-Team: LANGUAGE \n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Language: uk_UA\n" 18 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n" 19 | 20 | #: admin.py:16 21 | msgid "Basic settings" 22 | msgstr "Основні налаштування" 23 | 24 | #: admin.py:19 25 | msgid "Display settings" 26 | msgstr "Налаштування показу" 27 | 28 | #: admin.py:23 29 | msgid "Additional settings" 30 | msgstr "Додаткові налаштування" 31 | 32 | #: admin.py:142 33 | msgid "Item's parent left unchanged. Item couldn't be parent to itself." 34 | msgstr "" 35 | "Батько елемента залишений без змін. Елемент не може бути батьком для самого " 36 | "себе." 37 | 38 | #: models.py:23 models.py:39 39 | msgid "Alias" 40 | msgstr "Псевдонім" 41 | 42 | #: models.py:23 43 | msgid "Short name to address site tree from a template." 44 | msgstr "Коротка назва для звертання до дерева сайту з шаблону." 45 | 46 | #: models.py:26 models.py:37 47 | msgid "Site Tree" 48 | msgstr "Дерево сайту" 49 | 50 | #: models.py:27 51 | msgid "Site Trees" 52 | msgstr "Дерева сайту" 53 | 54 | #: models.py:33 templates/admin/sitetree/tree/change_form.html:38 55 | msgid "Title" 56 | msgstr "Заголовок" 57 | 58 | #: models.py:33 59 | msgid "" 60 | "Site tree item title. Can contain template variables E.g.: {{ mytitle }}." 61 | msgstr "" 62 | "Заголовок елемента дерева сайту. Може містити змінні шаблону, наприклад: {{ " 63 | "mytitle }}." 64 | 65 | #: models.py:34 66 | msgid "Hint" 67 | msgstr "Підказка" 68 | 69 | #: models.py:34 70 | msgid "Some additional information about this item that is used as a hint." 71 | msgstr "Додаткові дані про елемент, що будуть використовуватись як підказка." 72 | 73 | #: models.py:35 templates/admin/sitetree/tree/change_form.html:39 74 | msgid "URL" 75 | msgstr "URL" 76 | 77 | #: models.py:35 78 | msgid "Exact URL or URL pattern (see \"Additional settings\") for this item." 79 | msgstr "" 80 | "Точний URL чи URL-шаблон (див. «Додаткові налаштування») для цього елемента." 81 | 82 | #: models.py:36 83 | msgid "URL as Pattern" 84 | msgstr "URL як шаблон" 85 | 86 | #: models.py:36 87 | msgid "" 88 | "Whether the given URL should be treated as a pattern.
Note: Refer" 89 | " to Django \"URL dispatcher\" documentation (e.g. \"Naming URL patterns\" " 90 | "part)." 91 | msgstr "" 92 | "Чи заданий URL потрібно обробляти як шаблон.
Увага: Зверніться до" 93 | " документації Django «Диспетчер URL» (розділ «Присвоювання назв URL-" 94 | "шаблонам»)." 95 | 96 | #: models.py:37 97 | msgid "Site tree this item belongs to." 98 | msgstr "Дерево сайту, до якого належить елемент." 99 | 100 | #: models.py:38 templates/admin/sitetree/tree/change_form.html:34 101 | msgid "Hidden" 102 | msgstr "Прихований" 103 | 104 | #: models.py:38 105 | msgid "Whether to show this item in navigation." 106 | msgstr "Чи приховувати цей елемент в навігації." 107 | 108 | #: models.py:39 109 | msgid "" 110 | "Short name to address site tree item from a template.
Reserved " 111 | "aliases: \"trunk\", \"this-children\" and \"this-siblings\"." 112 | msgstr "" 113 | "Коротка назва для звертання до елементу з шаблона.
Зарезервовані " 114 | "псевдоніми: «trunk», «this-children» та «this-siblings»." 115 | 116 | #: models.py:40 117 | msgid "Description" 118 | msgstr "Опис" 119 | 120 | #: models.py:40 121 | msgid "Additional comments on this item." 122 | msgstr "Додаткові коментарі для цього елементу." 123 | 124 | #: models.py:41 125 | msgid "Show in menu" 126 | msgstr "Показувати в меню" 127 | 128 | #: models.py:41 129 | msgid "Whether to show this item in a menu." 130 | msgstr "Чи показувати цей елемент в меню." 131 | 132 | #: models.py:42 133 | msgid "Show in breadcrumb path" 134 | msgstr "Показувати в навігаційному ланцюжку" 135 | 136 | #: models.py:42 137 | msgid "Whether to show this item in a breadcrumb path." 138 | msgstr "Чи показувати цей елемент в навігаційному ланцюжку." 139 | 140 | #: models.py:43 141 | msgid "Show in site tree" 142 | msgstr "Показувати в дереві сайту" 143 | 144 | #: models.py:43 145 | msgid "Whether to show this item in a site tree." 146 | msgstr "Чи показувати цей елемент в дереві сайту." 147 | 148 | #: models.py:46 149 | msgid "Parent" 150 | msgstr "Батьківський сайт" 151 | 152 | #: models.py:46 153 | msgid "Parent site tree item." 154 | msgstr "Елемент дерева батьківського сайту." 155 | 156 | #: models.py:47 templates/admin/sitetree/tree/change_form.html:40 157 | msgid "Sort order" 158 | msgstr "Порядок сортування" 159 | 160 | #: models.py:47 161 | msgid "Item position among other site tree items under the same parent." 162 | msgstr "Позиція елемента між іншими елементами того ж батьківського сайту." 163 | 164 | #: models.py:60 165 | msgid "Site Tree Item" 166 | msgstr "Елемент дерева сайту" 167 | 168 | #: models.py:61 templates/admin/sitetree/tree/change_form.html:17 169 | msgid "Site Tree Items" 170 | msgstr "Елементи дерева сайту" 171 | 172 | #: templates/admin/sitetree/tree/change_form.html:24 173 | msgid "Add Site Tree item" 174 | msgstr "Додати елемент дерева сайту" 175 | 176 | #: templates/admin/sitetree/tree/change_form.html:35 177 | msgid "Menu" 178 | msgstr "Меню" 179 | 180 | #: templates/admin/sitetree/tree/change_form.html:36 181 | msgid "Breadcrumbs" 182 | msgstr "Навігаційний ланцюжок" 183 | 184 | #: templates/admin/sitetree/tree/change_form.html:37 185 | msgid "Tree" 186 | msgstr "Дерево" 187 | 188 | #: templates/admin/sitetree/tree/tree.html:16 189 | msgid "Move up" 190 | msgstr "Перемістити вгору" 191 | 192 | #: templates/admin/sitetree/tree/tree.html:18 193 | msgid "Move down" 194 | msgstr "Перемістити вниз" 195 | -------------------------------------------------------------------------------- /sitetree/management/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sitetree/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sitetree/management/commands/sitetree_resync_apps.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from django.db import DEFAULT_DB_ALIAS 3 | 4 | from sitetree.utils import get_tree_model, import_project_sitetree_modules 5 | from sitetree.settings import APP_MODULE_NAME 6 | from sitetree.sitetreeapp import Cache 7 | from sitetree.compat import CommandOption, options_getter 8 | 9 | 10 | MODEL_TREE_CLASS = get_tree_model() 11 | 12 | 13 | get_options = options_getter(( 14 | CommandOption( 15 | '--database', action='store', dest='database', default=DEFAULT_DB_ALIAS, 16 | help='Nominates a specific database to place trees and items into. Defaults to the "default" database.' 17 | ), 18 | )) 19 | 20 | 21 | class Command(BaseCommand): 22 | 23 | help = 'Places sitetrees of the project applications (defined in `app_name.sitetree.py`) into DB, ' \ 24 | 'replacing old ones if any.' 25 | 26 | args = '[app_name app_name ...]' 27 | 28 | option_list = get_options() 29 | 30 | def add_arguments(self, parser): 31 | parser.add_argument('args', metavar='app', nargs='*', help='Application names.') 32 | get_options(parser.add_argument) 33 | 34 | def handle(self, *apps, **options): 35 | using = options.get('database', DEFAULT_DB_ALIAS) 36 | 37 | tree_modules = import_project_sitetree_modules() 38 | 39 | if not tree_modules: 40 | self.stdout.write(f'No sitetrees found in project apps (searched in %app%/{APP_MODULE_NAME}.py).\n') 41 | 42 | for module in tree_modules: 43 | sitetrees = getattr(module, 'sitetrees', None) 44 | app = module.__dict__['__package__'] 45 | if not apps or app in apps: 46 | if sitetrees is not None: 47 | self.stdout.write(f'Sitetrees found in `{app}` app ...\n') 48 | for tree in sitetrees: 49 | self.stdout.write(f' Processing `{tree.alias}` tree ...\n') 50 | # Delete trees with the same name beforehand. 51 | MODEL_TREE_CLASS.objects.filter(alias=tree.alias).using(using).delete() 52 | # Drop id to let the DB handle it. 53 | tree.id = None 54 | tree.save(using=using) 55 | for item in tree.dynamic_items: 56 | self.stdout.write(f' Adding `{item.title}` tree item ...\n') 57 | # Drop id to let the DB handle it. 58 | item.id = None 59 | if item.parent is not None: 60 | # Suppose parent tree object is already saved to DB. 61 | item.parent_id = item.parent.id 62 | item.tree = tree 63 | item.save(using=using) 64 | # Copy permissions to M2M field once `item` 65 | # has been saved 66 | if hasattr(item.access_permissions, 'set'): 67 | item.access_permissions.set(item.permissions) 68 | 69 | else: 70 | item.access_permissions = item.permissions 71 | 72 | Cache.reset() 73 | -------------------------------------------------------------------------------- /sitetree/management/commands/sitetreedump.py: -------------------------------------------------------------------------------- 1 | from django.core import serializers 2 | from django.core.management.base import BaseCommand, CommandError 3 | from django.db import DEFAULT_DB_ALIAS 4 | 5 | from sitetree.utils import get_tree_model, get_tree_item_model 6 | from sitetree.compat import CommandOption, options_getter 7 | 8 | 9 | MODEL_TREE_CLASS = get_tree_model() 10 | MODEL_TREE_ITEM_CLASS = get_tree_item_model() 11 | 12 | 13 | get_options = options_getter(( 14 | CommandOption( 15 | '--indent', default=None, dest='indent', type=int, 16 | help='Specifies the indent level to use when pretty-printing output.'), 17 | 18 | CommandOption('--items_only', action='store_true', dest='items_only', default=False, 19 | help='Export tree items only.'), 20 | 21 | CommandOption('--database', action='store', dest='database', default=DEFAULT_DB_ALIAS, 22 | help='Nominates a specific database to export fixtures from. Defaults to the "default" database.'), 23 | )) 24 | 25 | 26 | class Command(BaseCommand): 27 | 28 | option_list = get_options() 29 | help = 'Output sitetrees from database as a fixture in JSON format.' 30 | args = '[tree_alias tree_alias ...]' 31 | 32 | def add_arguments(self, parser): 33 | parser.add_argument('args', metavar='tree', nargs='*', help='Tree aliases.', default=[]) 34 | get_options(parser.add_argument) 35 | 36 | def handle(self, *aliases, **options): 37 | 38 | indent = options.get('indent', None) 39 | using = options.get('database', DEFAULT_DB_ALIAS) 40 | items_only = options.get('items_only', False) 41 | 42 | objects = [] 43 | 44 | if aliases: 45 | trees = MODEL_TREE_CLASS._default_manager.using(using).filter(alias__in=aliases) 46 | else: 47 | trees = MODEL_TREE_CLASS._default_manager.using(using).all() 48 | 49 | if not items_only: 50 | objects.extend(trees) 51 | 52 | for tree in trees: 53 | objects.extend(MODEL_TREE_ITEM_CLASS._default_manager.using(using).filter(tree=tree).order_by('parent')) 54 | 55 | try: 56 | return serializers.serialize('json', objects, indent=indent) 57 | 58 | except Exception as e: 59 | raise CommandError(f'Unable to serialize sitetree(s): {e}') 60 | -------------------------------------------------------------------------------- /sitetree/management/commands/sitetreeload.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | import sys 4 | from django.core import serializers 5 | from django.core.exceptions import ObjectDoesNotExist 6 | from django.core.management.base import BaseCommand, CommandError 7 | from django.core.management.color import no_style 8 | from django.db import connections, router, transaction, DEFAULT_DB_ALIAS 9 | 10 | from sitetree.compat import CommandOption, options_getter 11 | from sitetree.utils import get_tree_model, get_tree_item_model 12 | 13 | MODEL_TREE_CLASS = get_tree_model() 14 | MODEL_TREE_ITEM_CLASS = get_tree_item_model() 15 | 16 | 17 | get_options = options_getter(( 18 | CommandOption( 19 | '--database', action='store', dest='database', 20 | default=DEFAULT_DB_ALIAS, help='Nominates a specific database to load fixtures into. ' 21 | 'Defaults to the "default" database.'), 22 | 23 | CommandOption( 24 | '--mode', action='store', dest='mode', default='append', 25 | help='Mode to put data into DB. Variants: `replace`, `append`.'), 26 | 27 | CommandOption( 28 | '--items_into_tree', action='store', dest='items_into_tree', default=None, 29 | help='Import only tree items data into tree with given alias.'), 30 | )) 31 | 32 | 33 | class Command(BaseCommand): 34 | 35 | option_list = get_options() 36 | 37 | help = 'Loads sitetrees from fixture in JSON format into database.' 38 | args = '[fixture_file fixture_file ...]' 39 | 40 | def add_arguments(self, parser): 41 | parser.add_argument('args', metavar='fixture', nargs='+', help='Fixture files.') 42 | get_options(parser.add_argument) 43 | 44 | def handle(self, *fixture_files, **options): 45 | 46 | using = options.get('database', DEFAULT_DB_ALIAS) 47 | mode = options.get('mode', 'append') 48 | items_into_tree = options.get('items_into_tree', None) 49 | 50 | if items_into_tree is not None: 51 | try: 52 | items_into_tree = MODEL_TREE_CLASS.objects.get(alias=items_into_tree) 53 | except ObjectDoesNotExist: 54 | raise CommandError( 55 | f'Target tree aliased `{items_into_tree}` does not exist. Please create it before import.') 56 | else: 57 | mode = 'append' 58 | 59 | connection = connections[using] 60 | cursor = connection.cursor() 61 | 62 | self.style = no_style() 63 | 64 | loaded_object_count = 0 65 | 66 | if mode == 'replace': 67 | MODEL_TREE_CLASS.objects.all().delete() 68 | MODEL_TREE_ITEM_CLASS.objects.all().delete() 69 | 70 | for fixture_file in fixture_files: 71 | 72 | self.stdout.write(f'Loading fixture from `{fixture_file}` ...\n') 73 | 74 | fixture = open(fixture_file, 'r') 75 | 76 | try: 77 | objects = serializers.deserialize('json', fixture, using=using) 78 | except (SystemExit, KeyboardInterrupt): 79 | raise 80 | 81 | trees = [] 82 | tree_items = defaultdict(list) 83 | tree_item_parents = defaultdict(list) 84 | tree_items_new_indexes = {} 85 | 86 | try: 87 | allow_migrate = router.allow_migrate 88 | except AttributeError: 89 | # Django < 1.7 90 | allow_migrate = router.allow_syncdb 91 | 92 | for obj in objects: 93 | if allow_migrate(using, obj.object.__class__): 94 | if isinstance(obj.object, (MODEL_TREE_CLASS, MODEL_TREE_ITEM_CLASS)): 95 | if isinstance(obj.object, MODEL_TREE_CLASS): 96 | trees.append(obj.object) 97 | else: 98 | if items_into_tree is not None: 99 | obj.object.tree_id = items_into_tree.id 100 | tree_items[obj.object.tree_id].append(obj.object) 101 | tree_item_parents[obj.object.parent_id].append(obj.object.id) 102 | 103 | if items_into_tree is not None: 104 | trees = [items_into_tree,] 105 | 106 | try: 107 | 108 | for tree in trees: 109 | 110 | self.stdout.write(f'\nImporting tree `{tree.alias}` ...\n') 111 | orig_tree_id = tree.id 112 | 113 | if items_into_tree is None: 114 | if mode == 'append': 115 | tree.pk = None 116 | tree.id = None 117 | 118 | tree.save(using=using) 119 | loaded_object_count += 1 120 | 121 | parents_ahead = [] 122 | 123 | # Parents go first: enough for simple cases. 124 | tree_items[orig_tree_id].sort(key=lambda item: item.id not in tree_item_parents.keys()) 125 | 126 | for tree_item in tree_items[orig_tree_id]: 127 | parent_ahead = False 128 | self.stdout.write(f'Importing item `{tree_item.title}` ...\n') 129 | tree_item.tree_id = tree.id 130 | orig_item_id = tree_item.id 131 | 132 | if mode == 'append': 133 | tree_item.pk = None 134 | tree_item.id = None 135 | 136 | if tree_item.id in tree_items_new_indexes: 137 | tree_item.pk = tree_item.id = tree_items_new_indexes[tree_item.id] 138 | 139 | if tree_item.parent_id is not None: 140 | if tree_item.parent_id in tree_items_new_indexes: 141 | tree_item.parent_id = tree_items_new_indexes[tree_item.parent_id] 142 | else: 143 | parent_ahead = True 144 | 145 | tree_item.save(using=using) 146 | loaded_object_count += 1 147 | 148 | if mode == 'append': 149 | tree_items_new_indexes[orig_item_id] = tree_item.id 150 | if parent_ahead: 151 | parents_ahead.append(tree_item) 152 | 153 | # Second pass is necessary for tree items being imported before their parents. 154 | for tree_item in parents_ahead: 155 | tree_item.parent_id = tree_items_new_indexes[tree_item.parent_id] 156 | tree_item.save(using=using) 157 | 158 | except (SystemExit, KeyboardInterrupt): 159 | raise 160 | 161 | except Exception: 162 | import traceback 163 | fixture.close() 164 | 165 | self.stderr.write( 166 | self.style.ERROR( 167 | f"Fixture `{fixture_file}` import error: " 168 | f"{''.join(traceback.format_exception(*sys.exc_info()))}\n") 169 | ) 170 | 171 | fixture.close() 172 | 173 | # Reset DB sequences, for DBMS with sequences support. 174 | if loaded_object_count > 0: 175 | sequence_sql = connection.ops.sequence_reset_sql(self.style, [MODEL_TREE_CLASS, MODEL_TREE_ITEM_CLASS]) 176 | if sequence_sql: 177 | self.stdout.write('Resetting DB sequences ...\n') 178 | for line in sequence_sql: 179 | cursor.execute(line) 180 | 181 | connection.close() 182 | -------------------------------------------------------------------------------- /sitetree/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import sitetree.models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('auth', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Tree', 17 | fields=[ 18 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 19 | ('title', models.CharField(help_text='Site tree title for presentational purposes.', max_length=100, verbose_name='Title', blank=True)), 20 | ('alias', models.CharField(help_text='Short name to address site tree from templates.
Note: change with care.', unique=True, max_length=80, verbose_name='Alias', db_index=True)), 21 | ], 22 | options={ 23 | 'abstract': False, 24 | 'verbose_name': 'Site Tree', 25 | 'verbose_name_plural': 'Site Trees', 26 | }, 27 | bases=(models.Model,), 28 | ), 29 | migrations.CreateModel( 30 | name='TreeItem', 31 | fields=[ 32 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 33 | ('title', models.CharField(help_text='Site tree item title. Can contain template variables E.g.: {{ mytitle }}.', max_length=100, verbose_name='Title')), 34 | ('hint', models.CharField(default='', help_text='Some additional information about this item that is used as a hint.', max_length=200, verbose_name='Hint', blank=True)), 35 | ('url', models.CharField(help_text='Exact URL or URL pattern (see "Additional settings") for this item.', max_length=200, verbose_name='URL', db_index=True)), 36 | ('urlaspattern', models.BooleanField(default=False, help_text='Whether the given URL should be treated as a pattern.
Note: Refer to Django "URL dispatcher" documentation (e.g. "Naming URL patterns" part).', db_index=True, verbose_name='URL as Pattern')), 37 | ('hidden', models.BooleanField(default=False, help_text='Whether to show this item in navigation.', db_index=True, verbose_name='Hidden')), 38 | ('alias', sitetree.models.CharFieldNullable(max_length=80, blank=True, help_text='Short name to address site tree item from a template.
Reserved aliases: "trunk", "this-children", "this-siblings", "this-ancestor-children", "this-parent-siblings".', null=True, verbose_name='Alias', db_index=True)), 39 | ('description', models.TextField(default='', help_text='Additional comments on this item.', verbose_name='Description', blank=True)), 40 | ('inmenu', models.BooleanField(default=True, help_text='Whether to show this item in a menu.', db_index=True, verbose_name='Show in menu')), 41 | ('inbreadcrumbs', models.BooleanField(default=True, help_text='Whether to show this item in a breadcrumb path.', db_index=True, verbose_name='Show in breadcrumb path')), 42 | ('insitetree', models.BooleanField(default=True, help_text='Whether to show this item in a site tree.', db_index=True, verbose_name='Show in site tree')), 43 | ('access_loggedin', models.BooleanField(default=False, help_text='Check it to grant access to this item to authenticated users only.', db_index=True, verbose_name='Logged in only')), 44 | ('access_guest', models.BooleanField(default=False, help_text='Check it to grant access to this item to guests only.', db_index=True, verbose_name='Guests only')), 45 | ('access_restricted', models.BooleanField(default=False, help_text='Check it to restrict user access to this item, using Django permissions system.', db_index=True, verbose_name='Restrict access to permissions')), 46 | ('access_perm_type', models.IntegerField(default=1, help_text='Any — user should have any of chosen permissions. All — user should have all chosen permissions.', verbose_name='Permissions interpretation', choices=[(1, 'Any'), (2, 'All')])), 47 | ('sort_order', models.IntegerField(default=0, help_text='Item position among other site tree items under the same parent.', verbose_name='Sort order', db_index=True)), 48 | ('access_permissions', models.ManyToManyField(to='auth.Permission', verbose_name='Permissions granting access', blank=True)), 49 | ('parent', models.ForeignKey(related_name='treeitem_parent', on_delete=models.CASCADE, blank=True, to='sitetree.TreeItem', help_text='Parent site tree item.', null=True, verbose_name='Parent')), 50 | ('tree', models.ForeignKey(related_name='treeitem_tree', on_delete=models.CASCADE, verbose_name='Site Tree', to='sitetree.Tree', help_text='Site tree this item belongs to.')), 51 | ], 52 | options={ 53 | 'abstract': False, 54 | 'verbose_name': 'Site Tree Item', 55 | 'verbose_name_plural': 'Site Tree Items', 56 | }, 57 | bases=(models.Model,), 58 | ), 59 | migrations.AlterUniqueTogether( 60 | name='treeitem', 61 | unique_together=set([('tree', 'alias')]), 62 | ), 63 | ] 64 | -------------------------------------------------------------------------------- /sitetree/migrations/0002_alter_treeitem_parent_alter_treeitem_tree.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.8 on 2022-10-31 10:11 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('sitetree', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='treeitem', 16 | name='parent', 17 | field=models.ForeignKey(blank=True, help_text='Parent site tree item.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_parent', to='sitetree.treeitem', verbose_name='Parent'), 18 | ), 19 | migrations.AlterField( 20 | model_name='treeitem', 21 | name='tree', 22 | field=models.ForeignKey(help_text='Site tree this item belongs to.', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_tree', to='sitetree.tree', verbose_name='Site Tree'), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /sitetree/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idlesign/django-sitetree/37a0c347eaac91f570aa5bf73c55c75035414660/sitetree/migrations/__init__.py -------------------------------------------------------------------------------- /sitetree/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import Permission 2 | from django.db import models 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | from .settings import MODEL_TREE, TREE_ITEMS_ALIASES 6 | 7 | 8 | class CharFieldNullable(models.CharField): 9 | """We use custom char field to put nulls in SiteTreeItem 'alias' field. 10 | That allows 'unique_together' directive in Meta to work properly, so 11 | we don't have two site tree items with the same alias in the same site tree. 12 | 13 | """ 14 | def get_prep_value(self, value): 15 | if value is not None: 16 | if value.strip() == '': 17 | return None 18 | return self.to_python(value) 19 | 20 | 21 | class TreeBase(models.Model): 22 | 23 | title = models.CharField( 24 | _('Title'), max_length=100, help_text=_('Site tree title for presentational purposes.'), blank=True) 25 | 26 | alias = models.CharField( 27 | _('Alias'), max_length=80, 28 | help_text=_('Short name to address site tree from templates.
Note: change with care.'), 29 | unique=True, db_index=True) 30 | 31 | class Meta: 32 | abstract = True 33 | verbose_name = _('Site Tree') 34 | verbose_name_plural = _('Site Trees') 35 | 36 | def get_title(self) -> str: 37 | return self.title or self.alias 38 | 39 | def __str__(self) -> str: 40 | return self.alias 41 | 42 | 43 | class TreeItemBase(models.Model): 44 | 45 | PERM_TYPE_ANY = 1 46 | PERM_TYPE_ALL = 2 47 | 48 | PERM_TYPE_CHOICES = { 49 | PERM_TYPE_ANY: _('Any'), 50 | PERM_TYPE_ALL: _('All'), 51 | } 52 | 53 | title = models.CharField( 54 | _('Title'), max_length=100, 55 | help_text=_('Site tree item title. Can contain template variables E.g.: {{ mytitle }}.')) 56 | 57 | hint = models.CharField( 58 | _('Hint'), max_length=200, 59 | help_text=_('Some additional information about this item that is used as a hint.'), blank=True, default='') 60 | 61 | url = models.CharField( 62 | _('URL'), max_length=200, 63 | help_text=_('Exact URL or URL pattern (see "Additional settings") for this item.'), db_index=True) 64 | 65 | urlaspattern = models.BooleanField( 66 | _('URL as Pattern'), 67 | help_text=_('Whether the given URL should be treated as a pattern.
' 68 | 'Note: Refer to Django "URL dispatcher" documentation (e.g. "Naming URL patterns" part).'), 69 | db_index=True, default=False) 70 | 71 | tree = models.ForeignKey( 72 | MODEL_TREE, related_name='%(class)s_tree', on_delete=models.CASCADE, verbose_name=_('Site Tree'), 73 | help_text=_('Site tree this item belongs to.'), db_index=True) 74 | 75 | hidden = models.BooleanField( 76 | _('Hidden'), help_text=_('Whether to show this item in navigation.'), db_index=True, default=False) 77 | 78 | alias = CharFieldNullable( 79 | _('Alias'), max_length=80, 80 | help_text=_( 81 | 'Short name to address site tree item from a template.
' 82 | 'Reserved aliases: "%s".' % '", "'.join(TREE_ITEMS_ALIASES) 83 | ), 84 | db_index=True, blank=True, null=True) 85 | 86 | description = models.TextField( 87 | _('Description'), 88 | help_text=_('Additional comments on this item.'), blank=True, default='') 89 | 90 | inmenu = models.BooleanField( 91 | _('Show in menu'), 92 | help_text=_('Whether to show this item in a menu.'), db_index=True, default=True) 93 | 94 | inbreadcrumbs = models.BooleanField( 95 | _('Show in breadcrumb path'), 96 | help_text=_('Whether to show this item in a breadcrumb path.'), db_index=True, default=True) 97 | 98 | insitetree = models.BooleanField( 99 | _('Show in site tree'), 100 | help_text=_('Whether to show this item in a site tree.'), db_index=True, default=True) 101 | 102 | access_loggedin = models.BooleanField( 103 | _('Logged in only'), 104 | help_text=_('Check it to grant access to this item to authenticated users only.'), 105 | db_index=True, default=False) 106 | 107 | access_guest = models.BooleanField( 108 | _('Guests only'), 109 | help_text=_('Check it to grant access to this item to guests only.'), db_index=True, default=False) 110 | 111 | access_restricted = models.BooleanField( 112 | _('Restrict access to permissions'), 113 | help_text=_('Check it to restrict user access to this item, using Django permissions system.'), 114 | db_index=True, default=False) 115 | 116 | access_permissions = models.ManyToManyField( 117 | Permission, verbose_name=_('Permissions granting access'), blank=True) 118 | 119 | access_perm_type = models.IntegerField( 120 | _('Permissions interpretation'), 121 | help_text=_('Any — user should have any of chosen permissions. ' 122 | 'All — user should have all chosen permissions.'), 123 | choices=PERM_TYPE_CHOICES.items(), default=PERM_TYPE_ANY) 124 | 125 | # These two are for 'adjacency list' model. 126 | # This is the current approach of tree representation for sitetree. 127 | parent = models.ForeignKey( 128 | 'self', related_name='%(class)s_parent', on_delete=models.CASCADE, verbose_name=_('Parent'), 129 | help_text=_('Parent site tree item.'), db_index=True, null=True, blank=True) 130 | 131 | sort_order = models.IntegerField( 132 | _('Sort order'), 133 | help_text=_('Item position among other site tree items under the same parent.'), db_index=True, default=0) 134 | 135 | def save(self, force_insert=False, force_update=False, **kwargs): 136 | # Ensure that item is not its own parent, since this breaks 137 | # the sitetree (and possibly the entire site). 138 | if self.parent == self: 139 | self.parent = None 140 | 141 | # Set item's sort order to its primary key. 142 | id_ = self.id 143 | if id_ and self.sort_order == 0: 144 | self.sort_order = id_ 145 | 146 | super().save(force_insert, force_update, **kwargs) 147 | 148 | # Set item's sort order to its primary key if not already set. 149 | if self.sort_order == 0: 150 | self.sort_order = self.id 151 | self.save() 152 | 153 | class Meta: 154 | abstract = True 155 | verbose_name = _('Site Tree Item') 156 | verbose_name_plural = _('Site Tree Items') 157 | unique_together = ('tree', 'alias') 158 | 159 | def __str__(self) -> str: 160 | return self.title 161 | 162 | 163 | class Tree(TreeBase): 164 | """Built-in tree class. Default functionality.""" 165 | 166 | 167 | class TreeItem(TreeItemBase): 168 | """Built-in tree item class. Default functionality.""" 169 | -------------------------------------------------------------------------------- /sitetree/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | 4 | SITETREE_CLS: str = getattr(settings, 'SITETREE_CLS', None) 5 | """Allows deep tree handling customization. Accepts sitetreeap.SiteTree subclass.""" 6 | 7 | MODEL_TREE: str = getattr(settings, 'SITETREE_MODEL_TREE', 'sitetree.Tree') 8 | """Path to a tree model (app.class).""" 9 | 10 | MODEL_TREE_ITEM: str = getattr(settings, 'SITETREE_MODEL_TREE_ITEM', 'sitetree.TreeItem') 11 | """Path to a tree item model (app.class).""" 12 | 13 | APP_MODULE_NAME: str = getattr(settings, 'SITETREE_APP_MODULE_NAME', 'sitetrees') 14 | """Module name where applications store trees shipped with them.""" 15 | 16 | UNRESOLVED_ITEM_MARKER: str = getattr(settings, 'SITETREE_UNRESOLVED_ITEM_MARKER', u'#unresolved') 17 | """This string is place instead of item URL if actual URL cannot be resolved.""" 18 | 19 | RAISE_ITEMS_ERRORS_ON_DEBUG: bool = getattr(settings, 'SITETREE_RAISE_ITEMS_ERRORS_ON_DEBUG', True) 20 | """Whether to raise exceptions in DEBUG mode if current page item is unresolved.""" 21 | 22 | DYNAMIC_ONLY: bool = getattr(settings, 'SITETREE_DYNAMIC_ONLY', False) 23 | """Whether to query DB for static trees items or use dynamic only.""" 24 | 25 | ITEMS_FIELD_ROOT_ID: str = getattr(settings, 'SITETREE_ITEMS_FIELD_ROOT_ID', '') 26 | """Item ID to be used for root item in TreeItemChoiceField. 27 | This is adjustable to be able to workaround client-side field validation issues in thirdparties. 28 | 29 | """ 30 | 31 | CACHE_TIMEOUT: int = getattr(settings, 'SITETREE_CACHE_TIMEOUT', 31536000) 32 | """Sitetree objects are stored in Django cache for a year (60 * 60 * 24 * 365 = 31536000 sec). 33 | Cache is only invalidated on sitetree or sitetree item change. 34 | 35 | """ 36 | 37 | CACHE_NAME: str = getattr(settings, 'SITETREE_CACHE_NAME', 'default') 38 | """Sitetree cache name to use (Defined in django CACHES hash).""" 39 | 40 | ADMIN_APP_NAME: str = getattr(settings, 'SITETREE_ADMIN_APP_NAME', 'admin') 41 | """Admin application name. In cases custom admin application is used.""" 42 | 43 | 44 | # Reserved tree items aliases. 45 | ALIAS_TRUNK = 'trunk' 46 | ALIAS_THIS_CHILDREN = 'this-children' 47 | ALIAS_THIS_SIBLINGS = 'this-siblings' 48 | ALIAS_THIS_ANCESTOR_CHILDREN = 'this-ancestor-children' 49 | ALIAS_THIS_PARENT_SIBLINGS = 'this-parent-siblings' 50 | 51 | TREE_ITEMS_ALIASES = [ 52 | ALIAS_TRUNK, 53 | ALIAS_THIS_CHILDREN, 54 | ALIAS_THIS_SIBLINGS, 55 | ALIAS_THIS_ANCESTOR_CHILDREN, 56 | ALIAS_THIS_PARENT_SIBLINGS 57 | ] 58 | -------------------------------------------------------------------------------- /sitetree/static/css/sitetree_bootstrap_submenu.css: -------------------------------------------------------------------------------- 1 | // Taken from https://codepen.io/ajaypatelaj/pen/prHjD 2 | 3 | .dropdown-submenu { 4 | position: relative; 5 | } 6 | 7 | .dropdown-submenu>.dropdown-menu { 8 | top: 0; 9 | left: 100%; 10 | margin-top: -6px; 11 | margin-left: -1px; 12 | -webkit-border-radius: 0 6px 6px 6px; 13 | -moz-border-radius: 0 6px 6px; 14 | border-radius: 0 6px 6px 6px; 15 | } 16 | 17 | .dropdown-submenu:hover>.dropdown-menu { 18 | display: block; 19 | } 20 | 21 | .dropdown-submenu>a:after { 22 | display: block; 23 | content: " "; 24 | float: right; 25 | width: 0; 26 | height: 0; 27 | border-color: transparent; 28 | border-style: solid; 29 | border-width: 5px 0 5px 5px; 30 | border-left-color: #ccc; 31 | margin-top: 5px; 32 | margin-right: -10px; 33 | } 34 | 35 | .dropdown-submenu:hover>a:after { 36 | border-left-color: #fff; 37 | } 38 | 39 | .dropdown-submenu.pull-left { 40 | float: none; 41 | } 42 | 43 | .dropdown-submenu.pull-left>.dropdown-menu { 44 | left: -100%; 45 | margin-left: 10px; 46 | -webkit-border-radius: 6px 0 6px 6px; 47 | -moz-border-radius: 6px 0 6px 6px; 48 | border-radius: 6px 0 6px 6px; 49 | } 50 | -------------------------------------------------------------------------------- /sitetree/templates/admin/sitetree/tree/change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %} 2 | {% load i18n admin_list sitetree %} 3 | 4 | {% block after_related_objects %} 5 | {% if change %} 6 | 14 | 15 |

 

16 |
17 |

{% trans "Site Tree Items" %} {{ original.alias }}

18 |
19 |

 

20 | {% if has_add_permission %} 21 | 28 | {% endif %} 29 | 30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | {% sitetree_tree from original.alias template "admin/sitetree/tree/tree.html" %} 48 | 49 |
{% trans "Hidden" %}{% trans "Menu" %}{% trans "Breadcrumbs" %}{% trans "Tree" %}{% trans "Restricted" %}{% trans "Users only" %}{% trans "Guests only" %}{% trans "Title" %}{% trans "URL" %}{% trans "Sort order" %}
50 |
51 |

 

52 | {% endif %} 53 | {% endblock %} 54 | -------------------------------------------------------------------------------- /sitetree/templates/admin/sitetree/tree/change_list_.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_list.html" %} 2 | {% load i18n %} 3 | 4 | {% block object-tools-items %} 5 | {% if user.is_superuser %} 6 |
  • {% trans "Dump data" %}
  • 7 |
  • {% trans "Load data" %}
  • 8 | {% endif %} 9 | {{ block.super }} 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /sitetree/templates/admin/sitetree/tree/tree.html: -------------------------------------------------------------------------------- 1 | {% load i18n sitetree %} 2 | {% load static %} 3 | 4 | {% get_static_prefix as STATIC_URL %} 5 | 6 | 11 | {% for item in sitetree_items %} 12 | 13 | {{ item.hidden }} 14 | {{ item.inmenu }} 15 | {{ item.inbreadcrumbs }} 16 | {{ item.insitetree }} 17 | {{ item.access_restricted }} 18 | {{ item.access_loggedin }} 19 | {{ item.access_guest }} 20 | 21 | {% for d in item.depth_range %}     {% endfor %} 22 | {% if item.parent %}|—{% endif %} 23 | {% if item.is_dynamic %}{{ item.title }}{% else %}{{ item.title }}{% endif %} 24 | 25 | {{ item.url }} 26 | 27 |    28 | 29 |     30 | 31 | 32 | 33 | {% if item.has_children %} 34 | {% sitetree_children of item for sitetree template "admin/sitetree/tree/tree.html" %} 35 | {% endif %} 36 | {% endfor %} 37 | -------------------------------------------------------------------------------- /sitetree/templates/admin/sitetree/tree/tree_combo.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %}{% for item in sitetree_items %} 2 | {{ item.id }}:::{% for d in item.depth_range %}    {% endfor %}{% if item.parent %}|- {% endif %}{{ item.title }} 3 | {% if item.has_children %}{% sitetree_children of item for sitetree template "admin/sitetree/tree/tree_combo.html" %}{% endif %}{% endfor %} 4 | -------------------------------------------------------------------------------- /sitetree/templates/admin/sitetree/treeitem/breadcrumbs.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 20 | -------------------------------------------------------------------------------- /sitetree/templates/admin/sitetree/treeitem/change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %} 2 | {% load i18n admin_modify %} 3 | {% block content_title %}

    {{ title }} {% if not add %}"{{ original.title }}"{% endif %}

    {% endblock %} 4 | {% block extrahead %}{{ block.super }} 5 | 21 | 43 | {% endblock %} 44 | {% block content %}{{ block.super }} 45 | 53 | {% endblock %} 54 | {% block breadcrumbs %}{% include "admin/sitetree/treeitem/breadcrumbs.html" %}{% endblock %} -------------------------------------------------------------------------------- /sitetree/templates/admin/sitetree/treeitem/delete_confirmation.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/delete_confirmation.html" %} 2 | {% block breadcrumbs %}{% include "admin/sitetree/treeitem/breadcrumbs.html" %}{% endblock %} -------------------------------------------------------------------------------- /sitetree/templates/admin/sitetree/treeitem/object_history.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/object_history.html" %} 2 | {% block breadcrumbs %}{% include "admin/sitetree/treeitem/breadcrumbs.html" %}{% endblock %} -------------------------------------------------------------------------------- /sitetree/templates/sitetree/breadcrumbs-title.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | {% if sitetree_items|length == 1 %} 3 | {% else %}{% for item in sitetree_items %}{{ item.title_resolved }}{% if not forloop.last %} - {% endif %}{% endfor %}{% endif %} -------------------------------------------------------------------------------- /sitetree/templates/sitetree/breadcrumbs.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | {% if sitetree_items|length == 1 %} 3 | {% else %} 4 |
      5 | {% for item in sitetree_items %} 6 | {% if not forloop.last %} 7 |
    • {{ item.title_resolved }}
    • 8 |
    • >
    • 9 | {% else %} 10 |
    • {{ item.title_resolved }}
    • 11 | {% endif %} 12 | {% endfor %} 13 |
    14 | {% endif %} -------------------------------------------------------------------------------- /sitetree/templates/sitetree/breadcrumbs_bootstrap.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | {% if sitetree_items|length == 1 %} 3 | {% else %} 4 | 13 | {% endif %} -------------------------------------------------------------------------------- /sitetree/templates/sitetree/breadcrumbs_bootstrap3.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | {% if sitetree_items|length == 1 %} 3 | {% else %} 4 | 13 | {% endif %} -------------------------------------------------------------------------------- /sitetree/templates/sitetree/breadcrumbs_bootstrap4.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | {% if sitetree_items|length == 1 %} 3 | {% else %} 4 | 15 | {% endif %} -------------------------------------------------------------------------------- /sitetree/templates/sitetree/breadcrumbs_foundation.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | {% if sitetree_items|length == 1 %} 3 | {% else %} 4 | 13 | {% endif %} -------------------------------------------------------------------------------- /sitetree/templates/sitetree/breadcrumbs_semantic.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | {% if sitetree_items|length == 1 %} 3 | {% else %} 4 | 13 | {% endif %} -------------------------------------------------------------------------------- /sitetree/templates/sitetree/menu.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 |
      3 | {% for item in sitetree_items %} 4 |
    • 5 | {{ item.title_resolved }} 6 | {% if item.has_children %} 7 | {% sitetree_children of item for menu template "sitetree/menu.html" %} 8 | {% endif %} 9 |
    • 10 | {% endfor %} 11 |
    -------------------------------------------------------------------------------- /sitetree/templates/sitetree/menu_bootstrap.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /sitetree/templates/sitetree/menu_bootstrap3.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /sitetree/templates/sitetree/menu_bootstrap3_deep.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | 15 | -------------------------------------------------------------------------------- /sitetree/templates/sitetree/menu_bootstrap3_deep_dropdown.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | 12 | -------------------------------------------------------------------------------- /sitetree/templates/sitetree/menu_bootstrap3_dropdown.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /sitetree/templates/sitetree/menu_bootstrap3_navpills-stacked.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /sitetree/templates/sitetree/menu_bootstrap3_navpills.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /sitetree/templates/sitetree/menu_bootstrap4.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /sitetree/templates/sitetree/menu_bootstrap4_dropdown.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /sitetree/templates/sitetree/menu_bootstrap4_navpills-stacked.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /sitetree/templates/sitetree/menu_bootstrap4_navpills.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /sitetree/templates/sitetree/menu_bootstrap5.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | 14 | -------------------------------------------------------------------------------- /sitetree/templates/sitetree/menu_bootstrap5_dropdown.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | 7 | -------------------------------------------------------------------------------- /sitetree/templates/sitetree/menu_bootstrap_dropdown.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /sitetree/templates/sitetree/menu_bootstrap_navlist.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /sitetree/templates/sitetree/menu_foundation-vertical.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /sitetree/templates/sitetree/menu_foundation.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /sitetree/templates/sitetree/menu_foundation_flyout.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /sitetree/templates/sitetree/menu_foundation_sidenav.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /sitetree/templates/sitetree/menu_semantic-vertical.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /sitetree/templates/sitetree/menu_semantic.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | {% for item in sitetree_items %} 3 | {% if item.has_children %} 4 | 7 | {% else %} 8 | {{ item.title_resolved }} 9 | {% endif %} 10 | {% endfor %} -------------------------------------------------------------------------------- /sitetree/templates/sitetree/menu_semantic_dropdown.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /sitetree/templates/sitetree/tree.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | {% if sitetree_items %} 3 |
      4 | {% for item in sitetree_items %} 5 | {% if item.insitetree %} 6 |
    • 7 | {{ item.title_resolved }} 8 | {% if item.has_children %} 9 | {% sitetree_children of item for sitetree template "sitetree/tree.html" %} 10 | {% endif %} 11 |
    • 12 | {% endif %} 13 | {% endfor %} 14 |
    15 | {% endif %} -------------------------------------------------------------------------------- /sitetree/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idlesign/django-sitetree/37a0c347eaac91f570aa5bf73c55c75035414660/sitetree/templatetags/__init__.py -------------------------------------------------------------------------------- /sitetree/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # This package is considered both as a django app, and a test package. 2 | -------------------------------------------------------------------------------- /sitetree/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pytest_djangoapp import configure_djangoapp_plugin 4 | 5 | 6 | def hook(settings): 7 | apps = settings['INSTALLED_APPS'] 8 | # apps.remove('sitetree.tests.testapp') 9 | # apps.append('sitetree.tests.testapp.conf.MyAppConfig') 10 | 11 | settings['TEMPLATES'][0]['OPTIONS']['context_processors'].append('django.template.context_processors.request') 12 | 13 | return settings 14 | 15 | 16 | pytest_plugins = configure_djangoapp_plugin( 17 | settings=dict( 18 | SITETREE_CLS='sitetree.tests.testapp.mysitetree.MySiteTree', 19 | ), 20 | admin_contrib=True, 21 | settings_hook=hook 22 | ) 23 | 24 | 25 | @pytest.fixture 26 | def build_tree(): 27 | """Builds a sitetree from dict definition. 28 | Returns items indexed by urls. 29 | 30 | Example: 31 | items_map = build_tree( 32 | {'alias': 'mytree'}, 33 | [{ 34 | 'title': 'one', 'url': '/one/', 'children': [ 35 | {'title': 'subone', 'url': '/subone/'} 36 | ] 37 | }] 38 | ) 39 | 40 | """ 41 | from sitetree.models import Tree, TreeItem 42 | from django.contrib.auth.models import Permission 43 | 44 | def build(tree_dict, items): 45 | 46 | def attach_items(tree, items, parent=None): 47 | for item_dict in items: 48 | children = item_dict.pop('children', []) 49 | 50 | access_permissions = item_dict.pop('access_permissions', []) 51 | 52 | item = TreeItem(**item_dict) 53 | item.tree = tree 54 | item.parent = parent 55 | item.save() 56 | 57 | for permission in access_permissions: 58 | item.access_permissions.add(Permission.objects.get(codename=permission)) 59 | 60 | items_map[f'{item.url}'] = item 61 | 62 | children and attach_items(tree, children, parent=item) 63 | 64 | items_map = {} 65 | 66 | tree = Tree(**tree_dict) 67 | tree.save() 68 | attach_items(tree, items) 69 | 70 | return items_map 71 | 72 | return build 73 | 74 | 75 | @pytest.fixture 76 | def common_tree(build_tree): 77 | items = build_tree( 78 | {'alias': 'mytree'}, 79 | [{ 80 | 'title': 'Home', 'url': '/home/', 'children': [ 81 | {'title': 'Users', 'url': '/users/', 'children': [ 82 | {'title': 'Moderators', 'url': '/users/moderators/'}, 83 | {'title': 'Ordinary', 'url': '/users/ordinary/'}, 84 | {'title': 'Hidden', 'hidden': True, 'url': '/users/hidden/'}, 85 | ]}, 86 | {'title': 'Articles', 'url': '/articles/', 'children': [ 87 | {'title': 'About cats', 'url': '/articles/cats/', 'children': [ 88 | {'title': 'Good', 'url': '/articles/cats/good/'}, 89 | {'title': 'Bad', 'url': '/articles/cats/bad/'}, 90 | {'title': 'Ugly', 'url': '/articles/cats/ugly/'}, 91 | ]}, 92 | {'title': 'About dogs', 'url': '/articles/dogs/'}, 93 | {'title': 'About mice', 'inmenu': False, 'url': '/articles/mice/'}, 94 | ]}, 95 | {'title': 'Contacts', 'inbreadcrumbs': False, 'url': '/contacts/', 'children': [ 96 | {'title': 'Russia', 'url': '/contacts/russia/', 97 | 'hint': 'The place', 'description': 'Russian Federation', 'children': [ 98 | {'title': 'Web', 'alias': 'ruweb', 'url': '/contacts/russia/web/', 'children': [ 99 | {'title': 'Public {{ subtitle }}', 'url': '/contacts/russia/web/public/'}, 100 | {'title': 'my model {{ model }}', 'url': '/mymodel/'}, 101 | {'title': 'Private', 102 | 'url': '/contacts/russia/web/private/', 103 | 'hint': 'Private Area Hint', 104 | 'description': 'Private Area Description', 105 | }, 106 | ]}, 107 | {'title': 'Postal', 'insitetree': False, 'url': '/contacts/russia/postal/'}, 108 | ]}, 109 | {'title': 'Australia', 'urlaspattern': True, 'url': 'contacts_australia australia_var', 110 | 'children': [ 111 | {'title': 'Alice Springs', 'access_loggedin': True, 'url': '/contacts/australia/alice/'}, 112 | {'title': 'Darwin', 'access_guest': True, 'url': '/contacts/australia/darwin/'}, 113 | ]}, 114 | {'title': 'China', 'urlaspattern': True, 'url': 'contacts_china china_var'}, 115 | ]}, 116 | ] 117 | }] 118 | ) 119 | items[''] = items['/home/'] 120 | return items 121 | -------------------------------------------------------------------------------- /sitetree/tests/test_admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib.admin.sites import site 2 | 3 | 4 | def get_item_admin(): 5 | from sitetree.models import TreeItem 6 | from sitetree.admin import TreeItemAdmin 7 | admin = TreeItemAdmin(TreeItem, site) 8 | return admin 9 | 10 | 11 | def test_parent_choices(request_client, build_tree, user_create, template_strip_tags): 12 | 13 | out = build_tree( 14 | {'alias': 'tree1'}, 15 | [{ 16 | 'title': 'one', 'url': '/one/', 'children': [ 17 | {'title': 'subone', 'url': '/subone/'} 18 | ] 19 | }] 20 | ) 21 | 22 | build_tree( 23 | {'alias': 'tree2'}, 24 | [{ 25 | 'title': 'some', 'url': '/some/', 'children': [ 26 | {'title': 'other', 'url': '/other/'} 27 | ] 28 | }] 29 | ) 30 | subone = out['/subone/'] 31 | client = request_client(user=user_create(superuser=True)) 32 | result = client.get(('admin:sitetree_treeitem_change', dict(item_id=subone.id, tree_id=subone.tree_id))) 33 | stripped = template_strip_tags(result.content.decode()) 34 | print(result.content.decode()) 35 | assert '|---------|one|    |- subone' in stripped 36 | assert '|---------|some|    |- other' not in stripped 37 | 38 | 39 | def test_admin_tree_item_basic(request_get, common_tree): 40 | 41 | admin = get_item_admin() 42 | admin.tree = common_tree[''] 43 | form = admin.get_form(request_get()) 44 | 45 | known_url_names = form.known_url_names 46 | assert set(known_url_names) == {'contacts_china', 'devices_grp', 'contacts_australia', 'raiser'} 47 | 48 | 49 | def test_admin_tree_item_move(common_tree): 50 | from sitetree.models import TreeItem, Tree 51 | 52 | main_tree = Tree(alias='main') 53 | main_tree.save() 54 | 55 | new_item_1 = TreeItem(title='title_1', sort_order=1, tree_id=main_tree.pk) 56 | new_item_1.save() 57 | 58 | new_item_2 = TreeItem(title='title_2', sort_order=2, tree_id=main_tree.pk) 59 | new_item_2.save() 60 | 61 | new_item_3 = TreeItem(title='title_3', sort_order=3, tree_id=main_tree.pk) 62 | new_item_3.save() 63 | 64 | admin = get_item_admin() 65 | 66 | admin.item_move(None, None, new_item_2.id, 'up') 67 | 68 | assert TreeItem.objects.get(pk=new_item_1.id).sort_order == 2 69 | assert TreeItem.objects.get(pk=new_item_2.id).sort_order == 1 70 | assert TreeItem.objects.get(pk=new_item_3.id).sort_order == 3 71 | 72 | admin.item_move(None, None, new_item_1.id, 'down') 73 | 74 | assert TreeItem.objects.get(pk=new_item_1.id).sort_order == 3 75 | assert TreeItem.objects.get(pk=new_item_2.id).sort_order == 1 76 | assert TreeItem.objects.get(pk=new_item_3.id).sort_order == 2 77 | 78 | 79 | def test_admin_tree_item_get_tree(request_get, common_tree): 80 | home = common_tree[''] 81 | tree = home.tree 82 | 83 | admin = get_item_admin() 84 | 85 | assert admin.get_tree(request_get(), tree.pk) == tree 86 | assert admin.get_tree(request_get(), None, home.pk) == tree 87 | 88 | 89 | def test_admin_tree_item_save_model(request_get, common_tree): 90 | users = common_tree['/users/'] 91 | tree = users.tree 92 | 93 | admin = get_item_admin() 94 | 95 | # Simulate bogus 96 | admin.previous_parent = users.parent 97 | users.parent = users 98 | 99 | admin.tree = tree 100 | admin.save_model(request_get(), users, None, change=True) 101 | 102 | assert users.tree == admin.tree 103 | assert users.parent == admin.previous_parent 104 | 105 | 106 | def test_admin_tree(): 107 | from sitetree.admin import TreeAdmin 108 | from sitetree.models import Tree 109 | 110 | admin = TreeAdmin(Tree, site) 111 | urls = admin.get_urls() 112 | 113 | assert len(urls) > 0 114 | 115 | 116 | def test_redirects_handler(request_get): 117 | from sitetree.admin import redirects_handler 118 | 119 | def get_location(referer, item_id=None): 120 | 121 | req = request_get(referer) 122 | req.META['HTTP_REFERER'] = referer 123 | 124 | args = [req] 125 | kwargs = {} 126 | if item_id is not None: 127 | kwargs['item_id'] = item_id 128 | 129 | handler = redirects_handler(*args, **kwargs) 130 | 131 | headers = getattr(handler, 'headers', None) 132 | 133 | if headers is None: 134 | # pre 3.2 135 | result = handler._headers['location'][1] 136 | else: 137 | result = headers['location'] 138 | 139 | return result 140 | 141 | assert get_location('/') == '/../' 142 | assert get_location('/delete/') == '/delete/../../' 143 | assert get_location('/history/') == '/history/../../' 144 | assert get_location('/history/', 42) == '/history/../' 145 | -------------------------------------------------------------------------------- /sitetree/tests/test_dynamic.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_dynamic_only(template_render_tag, template_context, template_strip_tags, monkeypatch): 5 | from sitetree.toolbox import compose_dynamic_tree, register_dynamic_trees, tree, item 6 | 7 | # If DYNAMIC_ONLY is not set, pytest-django will tell: "Database access not allowed" on any DB access attempt. 8 | monkeypatch.setattr('sitetree.sitetreeapp.DYNAMIC_ONLY', 'UNKNOWN') 9 | 10 | register_dynamic_trees(compose_dynamic_tree([tree('dynamic1', items=[ 11 | item('dynamic1_1', '/dynamic1_1_url', url_as_pattern=False, sort_order=2), 12 | ])]), reset_cache=True) 13 | 14 | result = template_strip_tags(template_render_tag('sitetree', 'sitetree_tree from "dynamic1"', template_context())) 15 | 16 | assert 'dynamic1_1' in result 17 | 18 | 19 | dynamic_access_checked = [] 20 | 21 | 22 | def dynamic_access_check_it(tree): 23 | dynamic_access_checked.append('yes') 24 | return True 25 | 26 | 27 | def test_dynamic_basic(template_render_tag, template_context, template_strip_tags): 28 | 29 | from sitetree.toolbox import compose_dynamic_tree, register_dynamic_trees, tree, item, get_dynamic_trees 30 | from sitetree.sitetreeapp import _IDX_ORPHAN_TREES 31 | 32 | item_dyn_attrs = item('dynamic2_1', '/dynamic2_1_url', url_as_pattern=False, dynamic_attrs={'a': 'b'}) 33 | assert item_dyn_attrs.a == 'b' 34 | 35 | item_dyn_access_check = item( 36 | 'dynamic1_1', '/dynamic1_1_url', url_as_pattern=False, sort_order=2, 37 | access_check=dynamic_access_check_it 38 | ) 39 | assert item_dyn_access_check.access_check is dynamic_access_check_it 40 | 41 | trees = [ 42 | compose_dynamic_tree([tree('dynamic1', items=[ 43 | item_dyn_access_check, 44 | item('dynamic1_2', '/dynamic1_2_url', url_as_pattern=False, sort_order=1), 45 | ])]), 46 | compose_dynamic_tree([tree('dynamic2', items=[ 47 | item_dyn_attrs, 48 | item('dynamic2_2', '/dynamic2_2_url', url_as_pattern=False), 49 | ])]), 50 | ] 51 | 52 | register_dynamic_trees(*trees, reset_cache=True) # new less-brackets style 53 | result = template_strip_tags(template_render_tag('sitetree', 'sitetree_tree from "dynamic1"', template_context())) 54 | 55 | assert 'dynamic1_1|dynamic1_2' in result 56 | assert 'dynamic2_1' not in result 57 | assert dynamic_access_checked == ['yes'] 58 | 59 | register_dynamic_trees(trees) 60 | 61 | result = template_strip_tags(template_render_tag('sitetree', 'sitetree_tree from "dynamic1"', template_context())) 62 | assert 'dynamic1_1|dynamic1_2' in result 63 | assert 'dynamic2_1' not in result 64 | 65 | trees = get_dynamic_trees() 66 | assert len(trees[_IDX_ORPHAN_TREES]) == 2 67 | 68 | from sitetree.sitetreeapp import _DYNAMIC_TREES 69 | _DYNAMIC_TREES.clear() 70 | 71 | 72 | def test_dynamic_attach(template_render_tag, template_context, template_strip_tags, common_tree): 73 | 74 | from sitetree.toolbox import compose_dynamic_tree, register_dynamic_trees, tree, item 75 | 76 | children = [ 77 | item('dynamic2_2_child', '/dynamic2_2_url_child', url_as_pattern=False), 78 | ] 79 | 80 | register_dynamic_trees([ 81 | compose_dynamic_tree([tree('dynamic1', items=[ 82 | item('dynamic1_1', '/dynamic1_1_url', url_as_pattern=False), 83 | item('dynamic1_2', '/dynamic1_2_url', url_as_pattern=False), 84 | ])], target_tree_alias='mytree'), 85 | 86 | compose_dynamic_tree([tree('dynamic2', items=[ 87 | item('dynamic2_1', '/dynamic2_1_url', url_as_pattern=False), 88 | item('dynamic2_2', '/dynamic2_2_url', url_as_pattern=False, children=children), 89 | ], title='some_title')], target_tree_alias='mytree', parent_tree_item_alias='ruweb'), 90 | 91 | ]) 92 | result = template_strip_tags(template_render_tag('sitetree', 'sitetree_tree from "mytree"', template_context())) 93 | 94 | assert 'Web|dynamic2_1|dynamic2_2' in result 95 | assert 'China|dynamic1_1|dynamic1_2' in result 96 | 97 | from sitetree.sitetreeapp import _DYNAMIC_TREES 98 | _DYNAMIC_TREES.clear() 99 | 100 | 101 | def test_dynamic_attach_from_module(template_render_tag, template_context, template_strip_tags, settings): 102 | 103 | from sitetree.toolbox import compose_dynamic_tree, register_dynamic_trees 104 | 105 | register_dynamic_trees(compose_dynamic_tree('sitetree.tests.testapp', include_trees=['dynamic4'])) 106 | 107 | result = template_strip_tags(template_render_tag('sitetree', 'sitetree_tree from "dynamic4"', template_context())) 108 | 109 | assert 'dynamic4_1' in result 110 | 111 | settings.DEBUG = True 112 | with pytest.warns(UserWarning): 113 | compose_dynamic_tree('nonexistent') 114 | 115 | from sitetree.sitetreeapp import _DYNAMIC_TREES 116 | _DYNAMIC_TREES.clear() 117 | -------------------------------------------------------------------------------- /sitetree/tests/test_forms.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def test_form(common_tree): 4 | from sitetree.toolbox import TreeItemForm 5 | 6 | form = TreeItemForm(tree='mytree', tree_item='root') 7 | 8 | items_field = form.fields['tree_item'] 9 | 10 | assert items_field.tree == 'mytree' 11 | assert items_field.initial == 'root' 12 | assert len(items_field.choices) == len(common_tree) 13 | 14 | 15 | def test_field(common_tree): 16 | from sitetree.toolbox import TreeItemChoiceField 17 | 18 | len_common_tree = len(common_tree) 19 | items_field = TreeItemChoiceField('mytree', initial='root') 20 | 21 | assert items_field.tree == 'mytree' 22 | assert items_field.initial == 'root' 23 | assert len(items_field.choices) == len_common_tree 24 | 25 | home_item = common_tree[''] 26 | item = items_field.clean(home_item.id) 27 | 28 | assert item == home_item 29 | 30 | assert items_field.clean('') is None 31 | assert items_field.clean(len_common_tree + 100) is None # Unknown id 32 | 33 | items_field = TreeItemChoiceField(home_item.tree, initial='root') 34 | assert items_field.tree == 'mytree' 35 | -------------------------------------------------------------------------------- /sitetree/tests/test_management.py: -------------------------------------------------------------------------------- 1 | from json import loads 2 | 3 | import pytest 4 | from django.core.management.base import CommandError 5 | from django.core.serializers.base import DeserializationError 6 | 7 | 8 | def test_sitetreeload(tmpdir, capsys, command_run): 9 | from sitetree.models import Tree, TreeItem 10 | 11 | def load(treedump, command_kwargs=None): 12 | f = tmpdir.join('somefile.json') 13 | f.write(treedump) 14 | command_kwargs = command_kwargs or {} 15 | command_run('sitetreeload', [f'{f}'], command_kwargs) 16 | 17 | treedump = ( 18 | '[' 19 | '{"pk": 2, "fields": {"alias": "tree1", "title": "tree one"}, "model": "sitetree.tree"}, ' 20 | '{"pk": 3, "fields": {"alias": "tree2", "title": "tree two"}, "model": "sitetree.tree"}, ' 21 | '{"pk": 7, "fields": {"access_restricted": false, "inmenu": true, "title": "tree item one",' 22 | ' "hidden": false, "description": "", "alias": null, "url": "/tree1/item1/", "access_loggedin": false,' 23 | ' "urlaspattern": false, "access_perm_type": 1, "tree": 2, "hint": "", "inbreadcrumbs": true,' 24 | ' "access_permissions": [], "sort_order": 7, "access_guest": false, "parent": null, "insitetree": true},' 25 | ' "model": "sitetree.treeitem"},' 26 | '{"pk": 8, "model": "sitetree.treeitem", ' 27 | '"fields": {"title": "tree item two", "alias": null, "url": "/", "tree": 2, "sort_order": 8, "parent": 7}}' 28 | ']' 29 | ) 30 | 31 | with pytest.raises(CommandError): 32 | load(treedump, dict(items_into_tree='nonexisting')) 33 | 34 | load(treedump) 35 | 36 | assert Tree.objects.filter(title='tree one').exists() 37 | assert Tree.objects.filter(title='tree two').exists() 38 | assert TreeItem.objects.get(title='tree item one', tree__alias='tree1') 39 | assert TreeItem.objects.get(title='tree item two', tree__alias='tree1', parent__title='tree item one') 40 | 41 | load(treedump, dict(items_into_tree='tree2')) 42 | assert TreeItem.objects.filter(title='tree item one', tree__alias='tree2').exists() 43 | 44 | load(treedump, dict(mode='replace')) 45 | assert TreeItem.objects.filter(title='tree item one').count() == 1 46 | 47 | with pytest.raises(DeserializationError): 48 | load(treedump.replace('7}}', '7}},'), dict(mode='replace')) 49 | 50 | load(treedump.replace('7}}', '27}}'), dict(mode='replace')) 51 | out, err = capsys.readouterr() 52 | assert 'does not exist.' in err 53 | 54 | 55 | def test_sitetreedump(capsys, common_tree, command_run): 56 | 57 | command_run('sitetreedump') 58 | 59 | out, _ = capsys.readouterr() 60 | out = loads(out) 61 | 62 | assert len(out) == len(common_tree) 63 | 64 | command_run('sitetreedump', ['notree']) 65 | 66 | out, _ = capsys.readouterr() 67 | out = loads(out) 68 | 69 | assert out == [] 70 | 71 | 72 | def test_sitetree_resync_apps(capsys, command_run): 73 | from sitetree.models import TreeItem 74 | 75 | command_run('sitetree_resync_apps', ['sitetree.tests.testapp']) 76 | out, _ = capsys.readouterr() 77 | 78 | assert 'Sitetrees found in' in out 79 | assert len(TreeItem.objects.all()) == 2 80 | -------------------------------------------------------------------------------- /sitetree/tests/test_migrations.py: -------------------------------------------------------------------------------- 1 | def test_migrations(check_migrations): 2 | result = check_migrations() 3 | assert result is True, "ERROR: Migrations check failed! Models' changes not migrated, please run './manage.py makemigrations' to solve the issue!" 4 | -------------------------------------------------------------------------------- /sitetree/tests/test_models.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_model_tree(): 5 | from sitetree.models import Tree 6 | 7 | tree = Tree(alias='test') 8 | tree.save() 9 | 10 | assert str(tree) == tree.alias 11 | assert tree.get_title() == tree.alias 12 | 13 | with pytest.raises(Exception): 14 | Tree(alias='test').save() # Unique alias 15 | 16 | 17 | def test_model_tree_item(): 18 | from sitetree.models import Tree, TreeItem 19 | 20 | tree1 = Tree(alias='test') 21 | tree1.save() 22 | 23 | item1 = TreeItem(tree=tree1, alias='only', title='only title') 24 | item1.save() 25 | 26 | assert str(item1) == item1.title 27 | 28 | item2 = TreeItem(tree=tree1, alias='other', parent=item1) 29 | item2.save() 30 | 31 | item3 = TreeItem(tree=tree1, parent=item1) 32 | item3.save() 33 | 34 | item3.sort_order = 100 35 | item3.parent = item3 36 | item3.save() 37 | 38 | assert item3.parent is None # Can't be itself 39 | assert item3.sort_order == 100 40 | 41 | item3.sort_order = 0 42 | item3.save() 43 | 44 | assert item3.sort_order == item3.id # Automatic ordering 45 | 46 | with pytest.raises(Exception): 47 | TreeItem(tree=tree1, alias='only').save() # Unique alias within tree 48 | -------------------------------------------------------------------------------- /sitetree/tests/test_other.py: -------------------------------------------------------------------------------- 1 | from sitetree.settings import ALIAS_TRUNK 2 | 3 | 4 | def test_stress(template_render_tag, template_context, template_strip_tags, build_tree, common_tree): 5 | 6 | build_tree( 7 | {'alias': 'othertree'}, 8 | [{'title': 'Root', 'url': '/', 'children': [ 9 | {'title': 'Other title', 'url': '/contacts/russia/web/private/'}, 10 | {'title': 'Title_{{ myvar }}', 'url': '/some/'} 11 | ]}], 12 | ) 13 | 14 | context = template_context(context_dict={'myvar': 'myval'}, request='/contacts/russia/web/private/') 15 | 16 | title = template_render_tag('sitetree', 'sitetree_page_title from "mytree"', context) 17 | title_other = template_render_tag('sitetree', 'sitetree_page_title from "othertree"', context) 18 | 19 | hint = template_render_tag('sitetree', 'sitetree_page_hint from "mytree"', context) 20 | description = template_render_tag('sitetree', 'sitetree_page_description from "mytree"', context) 21 | tree = template_strip_tags(template_render_tag('sitetree', 'sitetree_tree from "mytree"', context)) 22 | breadcrumbs = template_strip_tags(template_render_tag('sitetree', 'sitetree_breadcrumbs from "mytree"', context)) 23 | 24 | menu = template_render_tag('sitetree', f'sitetree_menu from "mytree" include "{ALIAS_TRUNK}"', context) 25 | menu_other = template_render_tag('sitetree', f'sitetree_menu from "othertree" include "{ALIAS_TRUNK}"', context) 26 | 27 | assert title == 'Private' 28 | assert title_other == 'Other title' 29 | assert hint == 'Private Area Hint' 30 | assert description == 'Private Area Description' 31 | assert breadcrumbs == 'Home|>|Russia|>|Web|>|Private' 32 | 33 | assert template_strip_tags(menu) == ( 34 | 'Home|Users|Moderators|Ordinary|Articles|About cats|Good|Bad|Ugly|About dogs|' 35 | 'Contacts|Russia|Web|Public|my model|Private|Postal|Australia|Darwin|China' 36 | ) 37 | assert 'current_item current_branch">Private' in menu 38 | 39 | assert template_strip_tags(menu_other) == 'Root|Other title|Title_myval' 40 | assert 'current_item current_branch">Other title' in menu_other 41 | 42 | assert tree == ( 43 | 'Home|Users|Moderators|Ordinary|Articles|About cats|Good|Bad|Ugly|About dogs|About mice|Contacts|' 44 | 'Russia|Web|Public|my model|Private|Australia|Darwin|China' 45 | ) 46 | 47 | 48 | def test_lazy_title(template_context): 49 | 50 | from sitetree.sitetreeapp import LazyTitle, get_sitetree 51 | 52 | assert LazyTitle('one') == 'one' 53 | 54 | title = LazyTitle('here{% no_way %}there') 55 | 56 | get_sitetree().current_page_context = template_context() 57 | 58 | assert title == 'herethere' 59 | 60 | 61 | def test_customized_tree_handler(template_context): 62 | 63 | from sitetree.sitetreeapp import get_sitetree 64 | 65 | assert get_sitetree().customized # see MySiteTree 66 | 67 | 68 | def test_techincal_view_exception_unmasked(request_client, settings): 69 | # We expect that customized 500 template using sitetree is handled as expected. 70 | client = request_client(raise_exceptions=False) 71 | response = client.get('/raiser/') 72 | assert response.content == b'\n\n
      \n\t\n
    ' 73 | 74 | 75 | def test_urlquote(request_client, build_tree, template_render_tag, template_strip_tags, template_context, request_get): 76 | 77 | build_tree( 78 | {'alias': 'bogustree'}, 79 | [{'title': 'HOME', 'url': '/', 'children': [ 80 | {'title': 'Reports', 'url': '/reports', 'children': [ 81 | {'title': 'Devices {{ grp }}', 'urlaspattern': True, 'url': 'devices_grp grp'}, 82 | ]}, 83 | 84 | ]}], 85 | ) 86 | 87 | name = 'Устройство10x 45.9:(2)=S+5' # handle both non-ascii and special chars as )( 88 | context = template_context(context_dict={'grp': name}, request=f'/devices/{name}') 89 | breadcrumbs = template_strip_tags( 90 | template_render_tag('sitetree', 'sitetree_breadcrumbs from "bogustree"', context)) 91 | 92 | assert name in breadcrumbs 93 | -------------------------------------------------------------------------------- /sitetree/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.core.exceptions import ImproperlyConfigured 3 | 4 | 5 | def test_import(): 6 | 7 | from sitetree.utils import import_project_sitetree_modules 8 | 9 | modules = import_project_sitetree_modules() 10 | 11 | assert len(modules) == 1 12 | assert modules[0].sitetrees 13 | 14 | 15 | def test_get_app_n_model(): 16 | 17 | from sitetree.utils import get_app_n_model 18 | 19 | app, model = get_app_n_model('MODEL_TREE') 20 | assert app == 'sitetree' 21 | assert model == 'Tree' 22 | 23 | with pytest.raises(ImproperlyConfigured): 24 | get_app_n_model('ALIAS_TRUNK') 25 | 26 | 27 | def test_import_app_sitetree_module(): 28 | 29 | from sitetree.utils import import_app_sitetree_module 30 | 31 | with pytest.raises(ImportError): 32 | import_app_sitetree_module('sitetre') 33 | 34 | 35 | def test_import_project_sitetree_modules(): 36 | 37 | from sitetree import settings 38 | from sitetree.models import Tree 39 | from sitetree.utils import get_model_class 40 | 41 | cls = get_model_class('MODEL_TREE') 42 | 43 | assert cls is Tree 44 | 45 | model_old = settings.MODEL_TREE 46 | settings.MODEL_TREE = 'nowhere.Model' 47 | 48 | try: 49 | with pytest.raises(ImproperlyConfigured): 50 | get_model_class('MODEL_TREE') 51 | 52 | finally: 53 | settings.MODEL_TREE = model_old 54 | 55 | 56 | def get_permission_and_name(): 57 | from django.contrib.auth.models import Permission 58 | perm = Permission.objects.all()[0] 59 | perm_name = f'{perm.content_type.app_label}.{perm.codename}' 60 | return perm, perm_name 61 | 62 | 63 | class TestPermissions(): 64 | 65 | def test_permission_any(self): 66 | from sitetree.toolbox import item 67 | 68 | i1 = item('root', 'url') 69 | assert i1.access_perm_type == i1.PERM_TYPE_ALL 70 | assert i1.permissions == [] 71 | 72 | i2 = item('root', 'url', perms_mode_all=True) 73 | assert i2.access_perm_type == i1.PERM_TYPE_ALL 74 | 75 | i3 = item('root', 'url', perms_mode_all=False) 76 | assert i3.access_perm_type == i1.PERM_TYPE_ANY 77 | 78 | def test_int_permissions(self): 79 | from sitetree.toolbox import item 80 | 81 | i1 = item('root', 'url', access_by_perms=[1, 2, 3]) 82 | assert i1.permissions == [1, 2, 3] 83 | 84 | def test_valid_string_permissions(self): 85 | from sitetree.toolbox import item 86 | 87 | perm, perm_name = get_permission_and_name() 88 | 89 | i1 = item('root', 'url', access_by_perms=perm_name) 90 | assert i1.permissions == [perm] 91 | 92 | def test_perm_obj_permissions(self): 93 | from sitetree.toolbox import item 94 | 95 | perm, __ = get_permission_and_name() 96 | 97 | i1 = item('root', 'url', access_by_perms=perm) 98 | assert i1.permissions == [perm] 99 | 100 | def test_bad_string_permissions(self, template_context, template_render_tag): 101 | from sitetree.toolbox import register_dynamic_trees, tree, item, compose_dynamic_tree 102 | 103 | register_dynamic_trees(compose_dynamic_tree([tree('bad', items=[ 104 | item('root', 'url', access_by_perms='bad name'), 105 | ])]), reset_cache=True) 106 | 107 | with pytest.raises(ValueError): 108 | template_render_tag( 109 | 'sitetree', f'sitetree_page_title from "bad"', 110 | template_context(request='/')) 111 | 112 | def test_unknown_name_permissions(self, template_context, template_render_tag): 113 | from sitetree.toolbox import register_dynamic_trees, tree, item, compose_dynamic_tree 114 | 115 | register_dynamic_trees(compose_dynamic_tree([tree('unknown', items=[ 116 | item('root', 'url', access_by_perms='unknown.name'), 117 | ])]), reset_cache=True) 118 | 119 | with pytest.raises(ValueError): 120 | template_render_tag( 121 | 'sitetree', f'sitetree_page_title from "unknown"', 122 | template_context(request='/')) 123 | 124 | def test_float_permissions(self, template_context, template_render_tag): 125 | from sitetree.toolbox import register_dynamic_trees, tree, item, compose_dynamic_tree 126 | 127 | register_dynamic_trees(compose_dynamic_tree([tree('fortytwodottwo', items=[ 128 | item('root', 'url', access_by_perms=42.2), 129 | ])]), reset_cache=True) 130 | 131 | with pytest.raises(ValueError) as e: 132 | template_render_tag( 133 | 'sitetree', f'sitetree_page_title from "fortytwodottwo"', 134 | template_context(request='/')) 135 | 136 | def test_access_restricted(self): 137 | from sitetree.toolbox import item 138 | 139 | # Test that default is False 140 | i0 = item('root', 'url', access_by_perms=1) 141 | assert i0.access_restricted 142 | 143 | # True is respected 144 | i1 = item('root', 'url') 145 | assert not i1.access_restricted 146 | -------------------------------------------------------------------------------- /sitetree/tests/testapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idlesign/django-sitetree/37a0c347eaac91f570aa5bf73c55c75035414660/sitetree/tests/testapp/__init__.py -------------------------------------------------------------------------------- /sitetree/tests/testapp/admin.py: -------------------------------------------------------------------------------- 1 | from sitetree.admin import TreeItemAdmin, TreeAdmin, override_tree_admin, override_item_admin 2 | 3 | 4 | class CustomTreeAdmin(TreeAdmin): 5 | pass 6 | 7 | 8 | class CustomTreeItemAdmin(TreeItemAdmin): 9 | pass 10 | 11 | 12 | override_tree_admin(CustomTreeAdmin) 13 | override_item_admin(CustomTreeItemAdmin) 14 | -------------------------------------------------------------------------------- /sitetree/tests/testapp/conf.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MyAppConfig(AppConfig): 5 | 6 | name: str = 'sitetree.tests.testapp' 7 | -------------------------------------------------------------------------------- /sitetree/tests/testapp/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class MyModel(models.Model): 5 | 6 | afield = models.CharField('my', max_length=20) 7 | 8 | def __str__(self): 9 | return self.afield 10 | -------------------------------------------------------------------------------- /sitetree/tests/testapp/mysitetree.py: -------------------------------------------------------------------------------- 1 | from sitetree.sitetreeapp import SiteTree 2 | 3 | 4 | class MySiteTree(SiteTree): 5 | """Custom tree handler to test deep customization abilities.""" 6 | 7 | customized = True 8 | -------------------------------------------------------------------------------- /sitetree/tests/testapp/sitetrees.py: -------------------------------------------------------------------------------- 1 | from sitetree.toolbox import tree, item 2 | 3 | 4 | sitetrees = [ 5 | tree('dynamic3', items=[ 6 | item('dynamic3_1', '/dynamic3_1_url', url_as_pattern=False), 7 | ]), 8 | tree('dynamic4', items=[ 9 | item('dynamic4_1', '/dynamic4_1_url', url_as_pattern=False), 10 | ]), 11 | ] 12 | -------------------------------------------------------------------------------- /sitetree/tests/testapp/templates/my500.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | {% sitetree_menu from "mymenu" include "trunk" %} -------------------------------------------------------------------------------- /sitetree/tests/testapp/templates/mymodel.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -{% sitetree_page_title from "mytree" %}- 3 | -------------------------------------------------------------------------------- /sitetree/tests/testapp/urls.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.urls import re_path, path 3 | from django.contrib import admin 4 | from django.views.defaults import server_error 5 | from .models import MyModel 6 | 7 | 8 | def raise_exception(request): 9 | raise Exception('This one should be handled by 500 technical view') 10 | 11 | 12 | def show_mymodel(request): 13 | model = MyModel(afield='thisismine') 14 | model.save() 15 | return render(request, 'mymodel.html', {'model': model}) 16 | 17 | 18 | urlpatterns = [ 19 | path('mymodel/', show_mymodel), 20 | re_path(r'^admin/', admin.site.urls), 21 | re_path(r'contacts/australia/(?P[^/]+)/', lambda r, value: None, name='contacts_australia'), 22 | re_path(r'contacts/australia/(?P\d+)/', lambda r, value: None, name='contacts_china'), 23 | re_path(r'raiser/', raise_exception, name='raiser'), 24 | re_path(r'^devices/(?P([\w() 0-9a-zA-Z!*:.?+=_-])+)$', lambda r, value: None, name='devices_grp'), 25 | ] 26 | 27 | handler500 = lambda request: server_error(request, template_name='my500.html') 28 | -------------------------------------------------------------------------------- /sitetree/toolbox.py: -------------------------------------------------------------------------------- 1 | # Unused imports below are exposed as API. 2 | from .fields import TreeItemChoiceField # noqa 3 | from .forms import TreeItemForm # noqa 4 | from .sitetreeapp import register_i18n_trees, register_items_hook, compose_dynamic_tree, register_dynamic_trees, get_dynamic_trees # noqa 5 | from .utils import get_tree_item_model, get_tree_model, tree, item, import_app_sitetree_module, import_project_sitetree_modules # noqa 6 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{37}-django{20,21,22,30,31,32} 4 | py{38,39}-django{20,21,22,30,31,32,40} 5 | py{310,311}-django{20,22,30,31,32,40,50} 6 | 7 | install_command = pip install {opts} {packages} 8 | skip_missing_interpreters = True 9 | 10 | [testenv] 11 | commands = python setup.py test 12 | 13 | deps = 14 | django20: Django>=2.0,<2.1 15 | django21: Django>=2.1,<2.2 16 | django22: Django>=2.2,<2.3 17 | django30: Django>=3.0,<3.1 18 | django31: Django>=3.1,<3.2 19 | django32: Django>=3.2,<3.3 20 | django40: Django>=4.0,<4.1 21 | django50: Django>=5.0,<5.1 22 | --------------------------------------------------------------------------------