├── demo ├── demo │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── apps.py │ ├── templates │ │ ├── index.html │ │ ├── demo_nav.html │ │ ├── listing.html │ │ ├── _head_bootstrap.html │ │ ├── _head_bootstrap3.html │ │ ├── _head_bootstrap4.html │ │ ├── _head_none.html │ │ ├── _head_foundation.html │ │ ├── _head_semantic.html │ │ ├── listing_none.html │ │ ├── _listing_contents.html │ │ ├── listing_semantic.html │ │ ├── listing_foundation.html │ │ ├── listing_bootstrap.html │ │ ├── listing_bootstrap4.html │ │ ├── _base.html │ │ └── listing_bootstrap3.html │ ├── urls.py │ ├── utils.py │ ├── sitetrees.py │ ├── views.py │ ├── admin.py │ ├── middleware.py │ ├── models.py │ └── static │ │ └── foundation │ │ └── app.js ├── settings │ ├── __init__.py │ ├── urls.py │ ├── wsgi.py │ └── settings.py ├── requirements.txt ├── db.sqlite3 ├── Dockerfile ├── docker-compose.yml ├── README.rst └── manage.py ├── tests ├── testapp │ ├── __init__.py │ ├── templates │ │ ├── my500.html │ │ └── mymodel.html │ ├── conf.py │ ├── mysitetree.py │ ├── models.py │ ├── sitetrees.py │ ├── admin.py │ └── urls.py ├── __init__.py ├── test_migrations.py ├── test_forms.py ├── test_models.py ├── test_management.py ├── test_other.py ├── test_admin.py ├── conftest.py ├── test_dynamic.py └── test_utils.py ├── src └── sitetree │ ├── migrations │ ├── __init__.py │ ├── 0002_alter_treeitem_parent_alter_treeitem_tree.py │ └── 0001_initial.py │ ├── templatetags │ └── __init__.py │ ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── sitetreedump.py │ │ ├── sitetree_resync_apps.py │ │ └── sitetreeload.py │ ├── __init__.py │ ├── exceptions.py │ ├── locale │ ├── de │ │ └── LC_MESSAGES │ │ │ └── django.mo │ ├── es │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fa │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fr │ │ └── LC_MESSAGES │ │ │ └── django.mo │ ├── ja │ │ └── LC_MESSAGES │ │ │ └── django.mo │ ├── nb │ │ └── LC_MESSAGES │ │ │ └── django.mo │ ├── ru │ │ └── LC_MESSAGES │ │ │ └── django.mo │ ├── uk │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── en │ │ └── LC_MESSAGES │ │ └── django.po │ ├── templates │ ├── admin │ │ └── sitetree │ │ │ ├── treeitem │ │ │ ├── object_history.html │ │ │ ├── delete_confirmation.html │ │ │ ├── breadcrumbs.html │ │ │ └── change_form.html │ │ │ └── tree │ │ │ ├── tree_combo.html │ │ │ ├── change_list_.html │ │ │ ├── change_form.html │ │ │ └── tree.html │ └── sitetree │ │ ├── breadcrumbs-title.html │ │ ├── menu_foundation_sidenav.html │ │ ├── menu_bootstrap3_navpills.html │ │ ├── menu_bootstrap_navlist.html │ │ ├── menu_semantic_dropdown.html │ │ ├── menu_bootstrap3_navpills-stacked.html │ │ ├── menu_foundation_flyout.html │ │ ├── menu_bootstrap4_navpills.html │ │ ├── menu_bootstrap4_navpills-stacked.html │ │ ├── menu_bootstrap_dropdown.html │ │ ├── menu_bootstrap4_dropdown.html │ │ ├── menu_bootstrap5_dropdown.html │ │ ├── menu_bootstrap3_dropdown.html │ │ ├── breadcrumbs.html │ │ ├── breadcrumbs_bootstrap3.html │ │ ├── breadcrumbs_foundation.html │ │ ├── breadcrumbs_bootstrap.html │ │ ├── tree.html │ │ ├── breadcrumbs_semantic.html │ │ ├── menu.html │ │ ├── breadcrumbs_bootstrap4.html │ │ ├── menu_semantic.html │ │ ├── menu_foundation-vertical.html │ │ ├── menu_foundation.html │ │ ├── menu_bootstrap3_deep_dropdown.html │ │ ├── menu_semantic-vertical.html │ │ ├── menu_bootstrap.html │ │ ├── menu_bootstrap4.html │ │ ├── menu_bootstrap3_deep.html │ │ ├── menu_bootstrap3.html │ │ └── menu_bootstrap5.html │ ├── apps.py │ ├── toolbox.py │ ├── forms.py │ ├── compat.py │ ├── static │ └── css │ │ └── sitetree_bootstrap_submenu.css │ ├── settings.py │ ├── fields.py │ └── models.py ├── .gitignore ├── .readthedocs.yaml ├── INSTALL.md ├── CONTRIBUTING.md ├── docs ├── 035_tagsadv.md ├── 060_performance.md ├── 015_i18n.md ├── 050_forms.md ├── 040_customization.md ├── index.md ├── 045_admin.md ├── 065_thirdparty.md ├── 025_management.md ├── 055_models.md ├── 005_quickstart.md ├── 020_apps.md ├── 010_tags.md └── 030_templatesmod.md ├── ruff.toml ├── mkdocs.yml ├── LICENSE ├── README.md ├── .github └── workflows │ └── python-package.yml ├── pyproject.toml └── AUTHORS.md /demo/demo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/testapp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/demo/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/sitetree/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/sitetree/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/sitetree/management/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/sitetree/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /demo/requirements.txt: -------------------------------------------------------------------------------- 1 | Django==4.2.25 2 | django-sitetree==1.14 3 | -------------------------------------------------------------------------------- /demo/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idlesign/django-sitetree/HEAD/demo/db.sqlite3 -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # This package is considered both as a django app, and a test package. 2 | -------------------------------------------------------------------------------- /src/sitetree/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = "1.18.0" 2 | 3 | default_app_config = 'sitetree.apps.SitetreeConfig' 4 | -------------------------------------------------------------------------------- /tests/testapp/templates/my500.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | {% sitetree_menu from "mymenu" include "trunk" %} -------------------------------------------------------------------------------- /tests/testapp/templates/mymodel.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -{% sitetree_page_title from "mytree" %}- 3 | -------------------------------------------------------------------------------- /src/sitetree/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class SiteTreeError(Exception): 4 | """Exception class for sitetree application.""" 5 | -------------------------------------------------------------------------------- /demo/demo/apps.py: -------------------------------------------------------------------------------- 1 | 2 | from django.apps import AppConfig 3 | 4 | 5 | class DemoConfig(AppConfig): 6 | 7 | name = 'demo' 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | .pydevproject 3 | .idea 4 | .tox 5 | __pycache__ 6 | *.pyc 7 | *.pyo 8 | *.egg-info 9 | docs/_build/ 10 | -------------------------------------------------------------------------------- /src/sitetree/locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idlesign/django-sitetree/HEAD/src/sitetree/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /src/sitetree/locale/es/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idlesign/django-sitetree/HEAD/src/sitetree/locale/es/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /src/sitetree/locale/fa/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idlesign/django-sitetree/HEAD/src/sitetree/locale/fa/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /src/sitetree/locale/fr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idlesign/django-sitetree/HEAD/src/sitetree/locale/fr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /src/sitetree/locale/ja/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idlesign/django-sitetree/HEAD/src/sitetree/locale/ja/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /src/sitetree/locale/nb/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idlesign/django-sitetree/HEAD/src/sitetree/locale/nb/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /src/sitetree/locale/ru/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idlesign/django-sitetree/HEAD/src/sitetree/locale/ru/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /src/sitetree/locale/uk/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idlesign/django-sitetree/HEAD/src/sitetree/locale/uk/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /tests/testapp/conf.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MyAppConfig(AppConfig): 5 | 6 | name: str = 'tests.testapp' 7 | -------------------------------------------------------------------------------- /demo/demo/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends '_base.html' %} 2 | {% load sitetree %} 3 | 4 | {% block body %} 5 | {% include 'demo_nav.html' %} 6 | {% endblock %} -------------------------------------------------------------------------------- /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/demo/templates/demo_nav.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | 3 |
4 |

Demo navigation

5 | {% sitetree_tree from "demo" %} 6 |
-------------------------------------------------------------------------------- /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 %} -------------------------------------------------------------------------------- /src/sitetree/templates/admin/sitetree/treeitem/object_history.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/object_history.html" %} 2 | {% block breadcrumbs %}{% include "admin/sitetree/treeitem/breadcrumbs.html" %}{% endblock %} -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/sitetree/templates/admin/sitetree/treeitem/delete_confirmation.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/delete_confirmation.html" %} 2 | {% block breadcrumbs %}{% include "admin/sitetree/treeitem/breadcrumbs.html" %}{% endblock %} -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /demo/demo/templates/_head_bootstrap4.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /demo/demo/templates/_head_none.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/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 %} -------------------------------------------------------------------------------- /demo/demo/templates/_head_foundation.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /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" -------------------------------------------------------------------------------- /tests/test_migrations.py: -------------------------------------------------------------------------------- 1 | def test_migrations(check_migrations): 2 | result = check_migrations() 3 | assert result is True, ( 4 | "ERROR: Migrations check failed! Models' changes not migrated, " 5 | "please run './manage.py makemigrations' to solve the issue!") 6 | -------------------------------------------------------------------------------- /demo/demo/templates/_head_semantic.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: "ubuntu-24.04" 5 | tools: 6 | python: "3" 7 | jobs: 8 | pre_install: 9 | - pip install mkdocs-material mkdocs-navsorted-plugin mkdocs-apidescribed-plugin git+https://github.com/idlesign/django-sitetree 10 | 11 | mkdocs: 12 | configuration: mkdocs.yml 13 | 14 | -------------------------------------------------------------------------------- /demo/demo/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | 3 | from .views import detailed, index, listing 4 | 5 | urlpatterns = [ 6 | re_path(r'^$', index, name='index'), 7 | re_path(r'^articles/$', listing, name='articles-listing'), 8 | re_path(r'^articles/(?P\d+)/$', detailed, name='articles-detailed'), 9 | ] 10 | -------------------------------------------------------------------------------- /src/sitetree/templates/sitetree/menu_foundation_sidenav.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /tests/testapp/sitetrees.py: -------------------------------------------------------------------------------- 1 | from sitetree.toolbox import item, tree 2 | 3 | sitetrees = [ 4 | tree('dynamic3', items=[ 5 | item('dynamic3_1', '/dynamic3_1_url', url_as_pattern=False), 6 | ]), 7 | tree('dynamic4', items=[ 8 | item('dynamic4_1', '/dynamic4_1_url', url_as_pattern=False), 9 | ]), 10 | ] 11 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /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': f'_head{theme}.html', 8 | 'tpl_realm': f'{view_type}{theme}.html' 9 | }) 10 | return render(request, f'{view_type}.html', context) 11 | -------------------------------------------------------------------------------- /tests/testapp/admin.py: -------------------------------------------------------------------------------- 1 | from sitetree.admin import TreeAdmin, TreeItemAdmin, override_item_admin, override_tree_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 | -------------------------------------------------------------------------------- /src/sitetree/templates/sitetree/menu_bootstrap3_navpills.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /src/sitetree/templates/sitetree/menu_bootstrap_navlist.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /src/sitetree/templates/sitetree/menu_semantic_dropdown.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/sitetree/templates/sitetree/menu_bootstrap3_navpills-stacked.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /src/sitetree/templates/sitetree/menu_foundation_flyout.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /src/sitetree/templates/sitetree/menu_bootstrap4_navpills.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /src/sitetree/templates/sitetree/menu_bootstrap4_navpills-stacked.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /src/sitetree/templates/sitetree/menu_bootstrap_dropdown.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /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 |
-------------------------------------------------------------------------------- /src/sitetree/templates/sitetree/menu_bootstrap4_dropdown.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /src/sitetree/templates/sitetree/menu_bootstrap5_dropdown.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | 7 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/sitetree/templates/sitetree/menu_bootstrap3_dropdown.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /demo/demo/sitetrees.py: -------------------------------------------------------------------------------- 1 | from sitetree.utils import item, tree 2 | 3 | sitetrees = ( 4 | tree('books', items=[ 5 | item('Books', '/books/', url_as_pattern=False, children=[ 6 | item('{{ book.title }}', 'books-details', in_menu=False, in_sitetree=False), 7 | item('Add a book', 'books-add'), 8 | ]) 9 | ]), 10 | tree('other', items=[ 11 | item('Item', '/item/', url_as_pattern=False, access_guest=False) 12 | ]), 13 | ) 14 | -------------------------------------------------------------------------------- /demo/settings/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib import admin 3 | from django.urls import include, path 4 | 5 | urlpatterns = [ 6 | path('admin/', admin.site.urls), 7 | path('', include(('demo.urls', 'demo'), namespace='demo')), 8 | ] 9 | 10 | 11 | if settings.DEBUG and settings.USE_DEBUG_TOOLBAR: 12 | import debug_toolbar 13 | urlpatterns = [ 14 | path('__debug__/', include(debug_toolbar.urls)), 15 | ] + urlpatterns 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/sitetree/templates/sitetree/breadcrumbs.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | {% if sitetree_items|length == 1 %} 3 | {% else %} 4 | 14 | {% endif %} -------------------------------------------------------------------------------- /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' %} -------------------------------------------------------------------------------- /src/sitetree/templates/sitetree/breadcrumbs_bootstrap3.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | {% if sitetree_items|length == 1 %} 3 | {% else %} 4 | 13 | {% endif %} -------------------------------------------------------------------------------- /src/sitetree/templates/sitetree/breadcrumbs_foundation.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | {% if sitetree_items|length == 1 %} 3 | {% else %} 4 | 13 | {% endif %} -------------------------------------------------------------------------------- /src/sitetree/templates/sitetree/breadcrumbs_bootstrap.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | {% if sitetree_items|length == 1 %} 3 | {% else %} 4 | 13 | {% endif %} -------------------------------------------------------------------------------- /src/sitetree/templates/sitetree/tree.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | {% if sitetree_items %} 3 | 15 | {% endif %} -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | # django-sitetree installation 2 | 3 | Python `pip` package is required to install `django-sitetree`. 4 | 5 | 6 | ## From sources 7 | 8 | Use the following command line to install `django-sitetree` from sources directory (containing `pyproject.toml`): 9 | 10 | pip install . 11 | 12 | ## From PyPI 13 | 14 | Alternatively you can install `django-sitetree` from PyPI: 15 | 16 | pip install django-sitetree 17 | 18 | 19 | Use `-U` flag for upgrade: 20 | 21 | pip install -U django-sitetree 22 | 23 | -------------------------------------------------------------------------------- /src/sitetree/templates/sitetree/breadcrumbs_semantic.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | {% if sitetree_items|length == 1 %} 3 | {% else %} 4 | 13 | {% endif %} -------------------------------------------------------------------------------- /demo/demo/views.py: -------------------------------------------------------------------------------- 1 | 2 | from django.shortcuts import get_list_or_404, redirect 3 | 4 | from sitetree.toolbox import register_i18n_trees 5 | 6 | from .models import Article 7 | from .utils import render_themed 8 | 9 | register_i18n_trees(['main']) 10 | 11 | 12 | def index(request): 13 | return render_themed(request, 'index', {}) 14 | 15 | 16 | def listing(request): 17 | return render_themed(request, 'listing', {'articles': get_list_or_404(Article)}) 18 | 19 | 20 | def detailed(request, article_id): 21 | return redirect('demo:articles-listing') 22 | -------------------------------------------------------------------------------- /src/sitetree/templates/sitetree/menu.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /demo/demo/admin.py: -------------------------------------------------------------------------------- 1 | 2 | from django.contrib import admin 3 | 4 | from .models import Article 5 | 6 | admin.site.register(Article) 7 | 8 | 9 | customized_sitetree_admin = False 10 | 11 | if customized_sitetree_admin: 12 | 13 | from sitetree.admin import TreeAdmin, TreeItemAdmin, override_item_admin, override_tree_admin 14 | 15 | class CustomTreeItemAdmin(TreeItemAdmin): 16 | 17 | fieldsets = None 18 | 19 | 20 | class CustomTreeAdmin(TreeAdmin): 21 | 22 | exclude = ('title',) 23 | 24 | override_item_admin(CustomTreeItemAdmin) 25 | override_tree_admin(CustomTreeAdmin) 26 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/sitetree/templates/sitetree/breadcrumbs_bootstrap4.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | {% if sitetree_items|length == 1 %} 3 | {% else %} 4 | 15 | {% endif %} -------------------------------------------------------------------------------- /src/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 %} -------------------------------------------------------------------------------- /src/sitetree/templates/sitetree/menu_foundation-vertical.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /src/sitetree/templates/sitetree/menu_foundation.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /src/sitetree/templates/sitetree/menu_bootstrap3_deep_dropdown.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | 12 | -------------------------------------------------------------------------------- /src/sitetree/templates/sitetree/menu_semantic-vertical.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /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/models.py: -------------------------------------------------------------------------------- 1 | 2 | from django.db import models 3 | 4 | 5 | class Article(models.Model): 6 | 7 | title = models.CharField('Title', max_length=200, blank=False) 8 | date_created = models.DateTimeField('Created', auto_created=True) 9 | contents = models.TextField('Contents') 10 | 11 | def __str__(self): 12 | return self.title 13 | 14 | 15 | customized_sitetree_models = False 16 | 17 | if customized_sitetree_models: 18 | 19 | from sitetree.models import TreeBase, TreeItemBase 20 | 21 | 22 | class MyTree(TreeBase): 23 | 24 | custom_field = models.CharField('Custom tree field', max_length=50, null=True, blank=True) 25 | 26 | 27 | class MyTreeItem(TreeItemBase): 28 | 29 | custom_field = models.IntegerField('Custom item field', default=42) 30 | -------------------------------------------------------------------------------- /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 |
    -------------------------------------------------------------------------------- /src/sitetree/templates/sitetree/menu_bootstrap.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /src/sitetree/templates/sitetree/menu_bootstrap4.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /src/sitetree/templates/sitetree/menu_bootstrap3_deep.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | 15 | -------------------------------------------------------------------------------- /src/sitetree/templates/sitetree/menu_bootstrap3.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | -------------------------------------------------------------------------------- /src/sitetree/templates/sitetree/menu_bootstrap5.html: -------------------------------------------------------------------------------- 1 | {% load sitetree %} 2 | 14 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # django-sitetree contributing 2 | 3 | ## Submit issues 4 | 5 | If you spotted something weird in application behavior or want to propose a feature 6 | you can do that at 7 | 8 | ## Write code 9 | 10 | If you are eager to participate in application development, fork it at , 11 | write your code, whether it should be a bugfix or a feature implementation, and make a pull request right 12 | from the forked project page. 13 | 14 | ## Translate 15 | 16 | If want to translate the application into your native language use Transifex: 17 | and submit the issue. 18 | 19 | 20 | ## Spread the word 21 | 22 | If you have some tips and tricks or any other words that you think might be of interest for the others — publish it 23 | wherever you find convenient. 24 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /demo/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | from pathlib import Path 5 | 6 | PATH_DEMO = Path(__file__).parent 7 | PATH_SITETREE = PATH_DEMO.parent 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 | -------------------------------------------------------------------------------- /src/sitetree/templates/admin/sitetree/treeitem/breadcrumbs.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 20 | -------------------------------------------------------------------------------- /tests/testapp/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.shortcuts import render 3 | from django.urls import path, re_path 4 | from django.views.defaults import server_error 5 | 6 | from .models import MyModel 7 | 8 | 9 | def raise_exception(request): 10 | raise Exception('This one should be handled by 500 technical view') 11 | 12 | 13 | def show_mymodel(request): 14 | model = MyModel(afield='thisismine') 15 | model.save() 16 | return render(request, 'mymodel.html', {'model': model}) 17 | 18 | 19 | urlpatterns = [ 20 | path('mymodel/', show_mymodel), 21 | re_path(r'^admin/', admin.site.urls), 22 | re_path(r'contacts/australia/(?P[^/]+)/', lambda r, value: None, name='contacts_australia'), 23 | re_path(r'contacts/australia/(?P\d+)/', lambda r, value: None, name='contacts_china'), 24 | re_path(r'raiser/', raise_exception, name='raiser'), 25 | re_path(r'^devices/(?P([\w() 0-9a-zA-Z!*:.?+=_-])+)$', lambda r, value: None, name='devices_grp'), 26 | ] 27 | 28 | handler500 = lambda request: server_error(request, template_name='my500.html') 29 | -------------------------------------------------------------------------------- /src/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 # noqa 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 # noqa: PLC0415 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/035_tagsadv.md: -------------------------------------------------------------------------------- 1 | # Advanced template tags 2 | 3 | SiteTree introduces two advanced template tags which you have to deal with in case you override the built-in sitetree templates. 4 | 5 | 6 | ## sitetree_children 7 | 8 | Implements down the tree traversal with rendering. 9 | 10 | !!! example 11 | ```html 12 | {% sitetree_children of someitem for menu template "sitetree/mychildren.html" %} 13 | ``` 14 | 15 | Used to render child items of specific sitetree item `someitem` for `menu` navigation type, using template **sitetree/mychildren.html**. 16 | 17 | Allowed navigation types: 18 | 19 | 1. **menu** 20 | 2. **sitetree** 21 | 22 | Basically template argument should contain path to current template itself. 23 | 24 | 25 | ## sitetree_url 26 | 27 | Resolves site tree item's url or url pattern. 28 | 29 | !!! example 30 | ```html 31 | {% sitetree_url for someitem params %} 32 | ``` 33 | 34 | 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. 35 | 36 | It can cast the resolved URL into a context variable when using `as` clause just like `url` tag. 37 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /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, match='constraint failed'): 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, match='constraint failed'): 47 | TreeItem(tree=tree1, alias='only').save() # Unique alias within tree 48 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | target-version = "py38" 2 | line-length = 120 3 | extend-exclude = [ 4 | "migrations", 5 | ] 6 | 7 | [format] 8 | quote-style = "single" 9 | exclude = [] 10 | 11 | [lint] 12 | select = [ 13 | "B", # possible bugs 14 | "BLE", # broad exception 15 | "C4", # comprehensions 16 | "DTZ", # work with datetimes 17 | "E", # code style 18 | "ERA", # commented code 19 | "EXE", # check executables 20 | "F", # misc 21 | "FA", # future annotations 22 | "FBT", # booleans 23 | "FURB", # modernizing 24 | "G", # logging format 25 | "I", # imports 26 | "ICN", # import conventions 27 | "INT", # i18n 28 | "ISC", # stringc concat 29 | "PERF", # perfomance 30 | "PIE", # misc 31 | "PLC", # misc 32 | "PLE", # misc err 33 | "PT", # pytest 34 | "PTH", # pathlib 35 | "PYI", # typing 36 | "RSE", # exc raise 37 | "SLOT", # slots related 38 | "TC", # typing 39 | "UP", # py upgrade 40 | ] 41 | 42 | ignore = [] 43 | 44 | 45 | [lint.extend-per-file-ignores] 46 | 47 | "demo/*" = [ 48 | "ERA001", 49 | "B904", 50 | "F401", 51 | ] 52 | 53 | "src/*" = [ 54 | "FA100", 55 | ] 56 | 57 | "src/sitetree/sitetreeapp.py" = [ 58 | "TC001", 59 | ] 60 | 61 | "tests/*" = [ 62 | "E731", 63 | "PLC0415", 64 | "C408", 65 | ] 66 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: django-sitetree 2 | site_url: !ENV READTHEDOCS_CANONICAL_URL 3 | 4 | copyright: Copyright © 2010-2025, Igor Starikov 5 | 6 | repo_url: https://github.com/idlesign/django-sitetree 7 | edit_uri: edit/master/docs/ 8 | 9 | extra: 10 | social: 11 | - icon: fontawesome/brands/github 12 | link: https://github.com/idlesign 13 | 14 | plugins: 15 | - search 16 | - navsorted 17 | - apidescribed 18 | 19 | theme: 20 | name: material 21 | palette: 22 | primary: green 23 | features: 24 | - content.action.edit 25 | - content.action.view 26 | - content.code.copy 27 | - navigation.indexes 28 | - navigation.tab 29 | - navigation.tabs.sticky 30 | - navigation.top 31 | - toc.follow 32 | 33 | markdown_extensions: 34 | - abbr 35 | - footnotes 36 | - pymdownx.betterem 37 | - pymdownx.caret 38 | - pymdownx.mark 39 | - pymdownx.highlight: 40 | anchor_linenums: true 41 | linenums: true 42 | line_spans: __span 43 | - pymdownx.superfences: 44 | custom_fences: 45 | - name: mermaid 46 | class: mermaid 47 | format: !!python/name:pymdownx.superfences.fence_code_format 48 | - pymdownx.smartsymbols 49 | - pymdownx.tabbed: 50 | alternate_style: true 51 | - pymdownx.tilde 52 | - tables 53 | - toc: 54 | permalink: true 55 | -------------------------------------------------------------------------------- /docs/060_performance.md: -------------------------------------------------------------------------------- 1 | # Performance notes 2 | 3 | To avoid performance hits on large sitetrees try to simplify them, and/or reduce number of sitetree items: 4 | 5 | * Restructure (unify) sitetree items where appropriate. E.g.: 6 | ``` 7 | Home 8 | |-- Category "Photo" 9 | | |-- Item "{{ item.title }}" 10 | | 11 | |-- Category "Audio" 12 | | |-- Item "{{ item.title }}" 13 | | 14 | |-- etc. 15 | ``` 16 | 17 | could be restructured into: 18 | ``` 19 | Home 20 | |-- Category "{{ category.title }}" 21 | | |-- Item "{{ item.title }}" 22 | | 23 | |-- etc. 24 | ``` 25 | 26 | * Do not use `URL as Pattern` sitetree item option. Instead, you may use hardcoded URLs. 27 | 28 | * Do not use access permissions restrictions (access rights) where not required. 29 | 30 | * Use Django templates caching machinery. 31 | 32 | * Use fast Django cache backend. 33 | 34 | !!! warning 35 | Sitetree uses Django cache framework to store trees data, but keep in mind that 36 | Django's default is [Local-memory caching](https://docs.djangoproject.com/en/dev/topics/cache/#local-memory-caching) 37 | that is known not playing well with multiple processes (which will eventually cause sitetree to render navigation 38 | in different states for different processes), so you're advised to use the other choices. 39 | 40 | You can specify the cache backend to use, setting the `SITETREE_CACHE_NAME` on the django settings to specify the name 41 | of the cache to use. 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2025, 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 | -------------------------------------------------------------------------------- /docs/015_i18n.md: -------------------------------------------------------------------------------- 1 | # Internationalization 2 | 3 | With `django-sitetree` it is possible to render different trees for different active 4 | locales still addressing them by the same alias from a template. 5 | 6 | **register_i18n_trees()** function registers aliases of internationalized sitetrees. 7 | Internationalized sitetrees are those, which are dubbed by other trees having 8 | locale identifying suffixes in their aliases. 9 | 10 | Let's suppose `my_tree` is the alias of a generic tree. This tree is the one 11 | that we call by its alias in templates, and it is the one which is used 12 | if no i18n version of that tree is found. 13 | 14 | Given that `my_tree_en`, `my_tree_ru` and other `my_tree_{locale-id}`-like 15 | trees are considered internationalization sitetrees. These are used (if available) 16 | in accordance with current locale used in project. 17 | 18 | Example: 19 | 20 | ```python 21 | # This code usually belongs to urls.py (or `ready` method of a user defined 22 | # sitetree application config if Django 1.7+). 23 | 24 | # First import the register function. 25 | from sitetree.toolbox import register_i18n_trees 26 | 27 | 28 | # Now register i18n trees. 29 | register_i18n_trees(['my_tree', 'my_another_tree']) 30 | 31 | ``` 32 | After that you need to create trees for languages supported 33 | in your project, e.g.: `my_tree_en`, `my_tree_ru`, `my_tree_pt-br`. 34 | 35 | Then when we address `my_tree` from a template django-sitetree will render 36 | an appropriate tree for locale currently active in your project. 37 | See `activate` function from `django.utils.translation` 38 | and 39 | for more information. -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /docs/050_forms.md: -------------------------------------------------------------------------------- 1 | # Custom Forms and Fields 2 | 3 | Occasionally you may want to link some site entities (e.g. Polls, Articles) to certain sitetree items (as to categorize 4 | them). You can achieve it with the help of generic forms and fields shipped with SiteTree. 5 | 6 | 7 | ## TreeItemForm 8 | 9 | You can inherit from that form to have a dropdown with tree items for a certain tree: 10 | 11 | ```python 12 | from sitetree.forms import TreeItemForm 13 | 14 | 15 | class MyTreeItemForm(TreeItemForm): 16 | """We inherit from TreeItemForm to allow user link some title to sitetree item. 17 | This form besides `title` field will have `tree_item` dropdown. 18 | 19 | """ 20 | 21 | title = forms.CharField() 22 | 23 | # We instruct our form to work with `main` aliased sitetree. 24 | # And we set tree item with ID = 2 as initial. 25 | my_form = MyTreeItemForm(tree='main', tree_item=2) 26 | ``` 27 | 28 | You can also use a well known `initial={'tree_item': 2}` approach to set an initial sitetree item. 29 | 30 | After that deal with that form just as usual. 31 | 32 | 33 | ## TreeItemChoiceField 34 | 35 | `TreeItemChoiceField` is what `TreeItemForm` uses internally to represent sitetree items dropdown, 36 | and what used in Admin contrib on sitetree item create/edit pages. 37 | 38 | You can inherit from it (and customized it) or use it as it is in your own forms: 39 | 40 | ```python 41 | from sitetree.fields import TreeItemChoiceField 42 | 43 | 44 | class MyField(TreeItemChoiceField): 45 | 46 | # We override template used to build select choices. 47 | template = 'my_templates/tree_combo.html' 48 | # And override root item representation. 49 | root_title = '-** Root item **-' 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/040_customization.md: -------------------------------------------------------------------------------- 1 | # Custom tree handler 2 | 3 | What to do if a time comes, and you need some fancy stuff done to tree items that 4 | **django-sitetree** does not support? 5 | 6 | It might be that you need some special tree items ordering in a menu, or you want to render 7 | a huge site tree with all articles titles that are described by one tree item in Django admin, 8 | or god knows what else. 9 | 10 | **django-sitetree** can facilitate on that as it allows tree handler customization 11 | with the help of `SITETREE_CLS` setting. 12 | 13 | 1. Subclass `sitetreeapp.SiteTree` and place that class into a separate module for convenience. 14 | 2. You may now override **.apply_hook()** to manipulate tree items before render, or any other method to customize handler to your exact needs. 15 | 3. Define `SITETREE_CLS` in `settings.py` of your project, showing it a dotted path to the subclass. 16 | 17 | 18 | Example: 19 | 20 | ```python title="myapp/mysitetree.py" 21 | from sitetree.sitetreeapp import SiteTree 22 | 23 | 24 | class MySiteTree(SiteTree): 25 | """Custom tree handler to test deep customization abilities.""" 26 | 27 | def apply_hook(self, tree_items, sender): 28 | # Suppose we want to process only menu child items. 29 | if sender == 'menu.children': 30 | # Let's add 'Hooked: ' to resolved titles of every item. 31 | for item in tree_items: 32 | item.title_resolved = 'Hooked: %s' % item.title_resolved 33 | # Return items list mutated or not. 34 | return tree_items 35 | ``` 36 | 37 | ```python title="myproject/settings.py" 38 | ... 39 | SITETREE_CLS = 'myapp.mysitetree.MySiteTree' 40 | ... 41 | ``` 42 | 43 | !!! note 44 | You might also be interested in the notes on overriding admin representation. 45 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # django-sitetree 2 | 3 | *django-sitetree is a reusable application for Django, introducing site tree, menu and breadcrumbs navigation elements.* 4 | 5 | Site structure in `django-sitetree` is described through Django admin interface in a so-called site trees. 6 | 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. E.g. using site tree editor in Django admin: 7 | 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 | ``` 20 | URI Title 21 | / - Site Root 22 | |_users/ - Site Users 23 | |_users-personal user.id - User Called {{ user.first_name }} 24 | ``` 25 | 26 | 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). 27 | 28 | 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). 29 | 30 | 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. 31 | 32 | 33 | ## Requirements 34 | 35 | 1. Python 3.8+ 36 | 2. Django 2.0+ 37 | 3. Auth Django contrib package 38 | 4. Admin site Django contrib package (optional) 39 | 40 | ## See also 41 | 42 | If the application is not what you want for site navigation, you might be interested in considering the other choices — 43 | 44 | -------------------------------------------------------------------------------- /src/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 %} -------------------------------------------------------------------------------- /docs/045_admin.md: -------------------------------------------------------------------------------- 1 | # Custom Admin 2 | 3 | SiteTree allows you to override tree and tree item representation in Django Admin interface. 4 | 5 | That could be used not only for the purpose of enhancement of visual design but also 6 | for integration with other applications, using admin inlines. 7 | 8 | ## Overriding pages 9 | 10 | The following functions from `sitetree.admin` could be used to override tree and tree item representation: 11 | 12 | * **override_tree_admin()** is used to customize tree representation. 13 | * **override_item_admin()** is used to customize tree item representation. 14 | 15 | ```python title="admin.py" 16 | # Import two helper functions and two admin models to inherit our custom model from. 17 | from sitetree.admin import TreeItemAdmin, TreeAdmin, override_tree_admin, override_item_admin 18 | 19 | # This is our custom tree admin model. 20 | class CustomTreeAdmin(TreeAdmin): 21 | exclude = ('title',) # Here we exclude `title` field from form. 22 | 23 | # And our custom tree item admin model. 24 | class CustomTreeItemAdmin(TreeItemAdmin): 25 | # That will turn a tree item representation from the default variant 26 | # with collapsible groupings into a flat one. 27 | fieldsets= None 28 | 29 | # Now we tell the SiteTree to replace generic representations with custom. 30 | override_tree_admin(CustomTreeAdmin) 31 | override_item_admin(CustomTreeItemAdmin) 32 | ``` 33 | 34 | !!! note 35 | You might also be interested in using custom tree handler. 36 | 37 | 38 | ## Override inlines 39 | 40 | In the example below we'll use django-seo application from 41 | 42 | According to `django-seo` documentation it allows an addition of custom metadata fields to your models, 43 | so we use it to connect metadata to sitetree items. 44 | 45 | That's how one might render django-seo inline form on sitetree item create and edit pages: 46 | 47 | ```python 48 | from rollyourown.seo.admin import get_inline 49 | from sitetree.admin import TreeItemAdmin, override_item_admin 50 | # Let's suppose our application contains seo.py with django-seo metadata class defined. 51 | from myapp.seo import CustomMeta 52 | 53 | 54 | class CustomTreeItemAdmin(TreeItemAdmin): 55 | inlines = [get_inline(CustomMeta)] 56 | 57 | override_item_admin(CustomTreeItemAdmin) 58 | ``` 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-sitetree 2 | 3 | http://github.com/idlesign/django-sitetree 4 | 5 | [![PyPI - Version](https://img.shields.io/pypi/v/django-sitetree)](https://pypi.python.org/pypi/django-sitetree) 6 | [![License](https://img.shields.io/pypi/l/django-sitetree)](https://pypi.python.org/pypi/django-sitetree) 7 | [![Coverage](https://img.shields.io/coverallsCoverage/github/idlesign/django-sitetree)](https://coveralls.io/r/idlesign/django-sitetree) 8 | [![Docs](https://img.shields.io/readthedocs/django-sitetree)](https://django-sitetree.readthedocs.io/) 9 | 10 | 11 | ## What's that 12 | 13 | *django-sitetree is a reusable application for Django, introducing site tree, menu and breadcrumbs navigation elements.* 14 | 15 | Site structure in django-sitetree is described through Django admin interface in a so-called site trees. 16 | 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. E.g. using site tree editor in Django admin:: 17 | 18 | ``` 19 | URI Title 20 | / - Site Root 21 | |_users/ - Site Users 22 | |_users/13/ - Definite User 23 | ``` 24 | 25 | 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. 26 | 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:: 27 | 28 | ``` 29 | URI Title 30 | / - Site Root 31 | |_users/ - Site Users 32 | |_users-personal user.id - User Called {{ user.first_name }} 33 | ``` 34 | 35 | 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). 36 | 37 | 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). 38 | 39 | 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. 40 | 41 | 42 | ## Documentation 43 | 44 | https://django-sitetree.readthedocs.io/ 45 | -------------------------------------------------------------------------------- /src/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.compat import CommandOption, options_getter 6 | from sitetree.utils import get_tree_item_model, get_tree_model 7 | 8 | MODEL_TREE_CLASS = get_tree_model() 9 | MODEL_TREE_ITEM_CLASS = get_tree_item_model() 10 | 11 | 12 | get_options = options_getter(( 13 | CommandOption( 14 | '--indent', default=None, dest='indent', type=int, 15 | help='Specifies the indent level to use when pretty-printing output.'), 16 | 17 | CommandOption('--items_only', action='store_true', dest='items_only', default=False, 18 | help='Export tree items only.'), 19 | 20 | CommandOption('--database', action='store', dest='database', default=DEFAULT_DB_ALIAS, 21 | help='Nominates a specific database to export fixtures from. Defaults to the "default" database.'), 22 | )) 23 | 24 | 25 | class Command(BaseCommand): 26 | 27 | option_list = get_options() 28 | help = 'Output sitetrees from database as a fixture in JSON format.' 29 | args = '[tree_alias tree_alias ...]' 30 | 31 | def add_arguments(self, parser): 32 | parser.add_argument('args', metavar='tree', nargs='*', help='Tree aliases.', default=[]) 33 | get_options(parser.add_argument) 34 | 35 | def handle(self, *aliases, **options): 36 | 37 | indent = options.get('indent', None) 38 | using = options.get('database', DEFAULT_DB_ALIAS) 39 | items_only = options.get('items_only', False) 40 | 41 | objects = [] 42 | 43 | if aliases: 44 | trees = MODEL_TREE_CLASS._default_manager.using(using).filter(alias__in=aliases) 45 | else: 46 | trees = MODEL_TREE_CLASS._default_manager.using(using).all() 47 | 48 | if not items_only: 49 | objects.extend(trees) 50 | 51 | for tree in trees: 52 | objects.extend(MODEL_TREE_ITEM_CLASS._default_manager.using(using).filter(tree=tree).order_by('parent')) 53 | 54 | try: 55 | return serializers.serialize('json', objects, indent=indent) 56 | 57 | except Exception as e: # noqa: BLE001 58 | raise CommandError(f'Unable to serialize sitetree(s): {e}') from None 59 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /demo/settings/settings.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | USE_DEBUG_TOOLBAR = False 4 | BASE_DIR = Path(__file__).absolute().parent.parent 5 | SECRET_KEY = 'not-a-secret' 6 | DEBUG = True 7 | ALLOWED_HOSTS = [] 8 | INTERNAL_IPS = ['127.0.0.1'] 9 | 10 | 11 | # SITETREE_MODEL_TREE = 'demo.MyTree' 12 | # SITETREE_MODEL_TREE_ITEM = 'demo.MyTreeItem' 13 | 14 | 15 | INSTALLED_APPS = [ 16 | 'django.contrib.admin', 17 | 'django.contrib.auth', 18 | 'django.contrib.contenttypes', 19 | 'django.contrib.sessions', 20 | 'django.contrib.messages', 21 | 'django.contrib.staticfiles', 22 | 23 | 'sitetree', 24 | 25 | 'demo', 26 | ] 27 | 28 | MIDDLEWARE = [ 29 | 'django.middleware.security.SecurityMiddleware', 30 | 'django.contrib.sessions.middleware.SessionMiddleware', 31 | 'django.middleware.common.CommonMiddleware', 32 | 'django.middleware.csrf.CsrfViewMiddleware', 33 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 34 | 'django.contrib.messages.middleware.MessageMiddleware', 35 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 36 | 37 | 'demo.middleware.language_activator', 38 | 'demo.middleware.theme_activator', 39 | ] 40 | 41 | ROOT_URLCONF = 'settings.urls' 42 | 43 | TEMPLATES = [ 44 | { 45 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 46 | 'DIRS': [], 47 | 'APP_DIRS': True, 48 | 'OPTIONS': { 49 | 'context_processors': [ 50 | 'django.template.context_processors.debug', 51 | 'django.template.context_processors.request', 52 | 'django.contrib.auth.context_processors.auth', 53 | 'django.contrib.messages.context_processors.messages', 54 | ], 55 | }, 56 | }, 57 | ] 58 | 59 | WSGI_APPLICATION = 'settings.wsgi.application' 60 | 61 | DATABASES = { 62 | 'default': { 63 | 'ENGINE': 'django.db.backends.sqlite3', 64 | 'NAME': BASE_DIR / 'db.sqlite3', 65 | } 66 | } 67 | 68 | LANGUAGE_CODE = 'en-us' 69 | TIME_ZONE = 'UTC' 70 | USE_I18N = True 71 | USE_L10N = True 72 | USE_TZ = True 73 | STATIC_URL = '/static/' 74 | 75 | 76 | CACHES = { 77 | 'default': { 78 | 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', 79 | } 80 | } 81 | 82 | 83 | if USE_DEBUG_TOOLBAR: 84 | INSTALLED_APPS.append('debug_toolbar') 85 | MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware') 86 | -------------------------------------------------------------------------------- /src/sitetree/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | SITETREE_CLS: str = getattr(settings, 'SITETREE_CLS', None) 4 | """Allows deep tree handling customization. Accepts sitetreeap.SiteTree subclass.""" 5 | 6 | MODEL_TREE: str = getattr(settings, 'SITETREE_MODEL_TREE', 'sitetree.Tree') 7 | """Path to a tree model (app.class).""" 8 | 9 | MODEL_TREE_ITEM: str = getattr(settings, 'SITETREE_MODEL_TREE_ITEM', 'sitetree.TreeItem') 10 | """Path to a tree item model (app.class).""" 11 | 12 | APP_MODULE_NAME: str = getattr(settings, 'SITETREE_APP_MODULE_NAME', 'sitetrees') 13 | """Module name where applications store trees shipped with them.""" 14 | 15 | UNRESOLVED_ITEM_MARKER: str = getattr(settings, 'SITETREE_UNRESOLVED_ITEM_MARKER', '#unresolved') 16 | """This string is place instead of item URL if actual URL cannot be resolved.""" 17 | 18 | RAISE_ITEMS_ERRORS_ON_DEBUG: bool = getattr(settings, 'SITETREE_RAISE_ITEMS_ERRORS_ON_DEBUG', True) 19 | """Whether to raise exceptions in DEBUG mode if current page item is unresolved.""" 20 | 21 | DYNAMIC_ONLY: bool = getattr(settings, 'SITETREE_DYNAMIC_ONLY', False) 22 | """Whether to query DB for static trees items or use dynamic only.""" 23 | 24 | ITEMS_FIELD_ROOT_ID: str = getattr(settings, 'SITETREE_ITEMS_FIELD_ROOT_ID', '') 25 | """Item ID to be used for root item in TreeItemChoiceField. 26 | This is adjustable to be able to workaround client-side field validation issues in thirdparties. 27 | 28 | """ 29 | 30 | CACHE_TIMEOUT: int = getattr(settings, 'SITETREE_CACHE_TIMEOUT', 31536000) 31 | """Sitetree objects are stored in Django cache for a year (60 * 60 * 24 * 365 = 31536000 sec). 32 | Cache is only invalidated on sitetree or sitetree item change. 33 | 34 | """ 35 | 36 | CACHE_NAME: str = getattr(settings, 'SITETREE_CACHE_NAME', 'default') 37 | """Sitetree cache name to use (Defined in django CACHES hash).""" 38 | 39 | ADMIN_APP_NAME: str = getattr(settings, 'SITETREE_ADMIN_APP_NAME', 'admin') 40 | """Admin application name. In cases custom admin application is used.""" 41 | 42 | 43 | # Reserved tree items aliases. 44 | ALIAS_TRUNK = 'trunk' 45 | ALIAS_THIS_CHILDREN = 'this-children' 46 | ALIAS_THIS_SIBLINGS = 'this-siblings' 47 | ALIAS_THIS_ANCESTOR_CHILDREN = 'this-ancestor-children' 48 | ALIAS_THIS_PARENT_SIBLINGS = 'this-parent-siblings' 49 | 50 | TREE_ITEMS_ALIASES = [ 51 | ALIAS_TRUNK, 52 | ALIAS_THIS_CHILDREN, 53 | ALIAS_THIS_SIBLINGS, 54 | ALIAS_THIS_ANCESTOR_CHILDREN, 55 | ALIAS_THIS_PARENT_SIBLINGS 56 | ] 57 | -------------------------------------------------------------------------------- /.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.8, 3.9, "3.10", 3.11, 3.12, 3.13] 18 | django-version: [2.0, 2.1, 2.2, 3.0, 3.1, 3.2, 4.0, 5.0, 5.1, 5.2] 19 | 20 | exclude: 21 | - python-version: 3.13 22 | django-version: 2.0 23 | - python-version: 3.13 24 | django-version: 2.1 25 | - python-version: 3.13 26 | django-version: 2.2 27 | - python-version: 3.13 28 | django-version: 3.0 29 | - python-version: 3.13 30 | django-version: 3.1 31 | - python-version: 3.13 32 | django-version: 3.2 33 | 34 | - python-version: 3.12 35 | django-version: 2.0 36 | - python-version: 3.12 37 | django-version: 2.1 38 | - python-version: 3.12 39 | django-version: 2.2 40 | - python-version: 3.12 41 | django-version: 3.1 42 | 43 | - python-version: 3.11 44 | django-version: 2.1 45 | 46 | - python-version: 3.9 47 | django-version: 2.0 48 | - python-version: 3.9 49 | django-version: 2.1 50 | - python-version: 3.9 51 | django-version: 2.2 52 | - python-version: 3.9 53 | django-version: 3.1 54 | - python-version: 3.9 55 | django-version: 5.0 56 | 57 | - python-version: 3.8 58 | django-version: 2.0 59 | - python-version: 3.8 60 | django-version: 2.1 61 | - python-version: 3.8 62 | django-version: 2.2 63 | - python-version: 3.8 64 | django-version: 3.1 65 | - python-version: 3.8 66 | django-version: 5.0 67 | 68 | steps: 69 | - uses: actions/checkout@v4 70 | - name: Set up Python ${{ matrix.python-version }} & Django ${{ matrix.django-version }} 71 | uses: actions/setup-python@v5 72 | with: 73 | python-version: ${{ matrix.python-version }} 74 | - name: Setup uv 75 | uses: astral-sh/setup-uv@v6 76 | - name: Install deps 77 | run: | 78 | uv sync --only-group tests 79 | uv pip install coveralls "Django~=${{ matrix.django-version }}.0" 80 | - name: Run tests 81 | env: 82 | GITHUB_TOKEN: ${{ secrets.github_token }} 83 | run: | 84 | uv run coverage run -m pytest 85 | uv run coveralls --service=github 86 | -------------------------------------------------------------------------------- /docs/065_thirdparty.md: -------------------------------------------------------------------------------- 1 | # Thirdparties 2 | 3 | Here belongs some notes on thirdparty Django applications support in SiteTree. 4 | 5 | ## django-smuggler 6 | 7 | 8 | 9 | `Smuggler` dump and load buttons will be available on trees listing page if this app is installed 10 | allowing to dump and load site trees and items right from your browser. 11 | 12 | 13 | ## django-modeltranslation 14 | 15 | 16 | 17 | If you do not want to use the built-in `sitetree` Internationalization machinery, with `modeltranslation` you can 18 | localize your tree items into different languages. This requires some work though. 19 | 20 | 1. Create a custom sitetree item model: 21 | 22 | ```python title="myapp/models.py" 23 | from sitetree.models import TreeItemBase 24 | 25 | 26 | class MyTranslatableTreeItem(TreeItemBase): 27 | """This model will be used by modeltranslation.""" 28 | ``` 29 | 30 | 2. Instruct Django to use your custom model: 31 | 32 | ```python title="settings.py" 33 | SITETREE_MODEL_TREE_ITEM = 'myapp.MyTreeItem' 34 | ``` 35 | 36 | 3. Tune up Admin contrib to handle translatable tree items: 37 | 38 | ```python title="admin.py" 39 | from modeltranslation.admin import TranslationAdmin 40 | from sitetree.admin import TreeItemAdmin, override_item_admin 41 | 42 | 43 | class CustomTreeItemAdmin(TreeItemAdmin, TranslationAdmin): 44 | """This allows admin contrib to support translations for tree items.""" 45 | 46 | override_item_admin(CustomTreeItemAdmin) 47 | ``` 48 | 49 | 4. Instruct `modeltranslation` how to handle your tree item model: 50 | 51 | ```python title="myapp/translation.py" 52 | from modeltranslation.translator import translator, TranslationOptions 53 | 54 | from .models import MyTranslatableTreeItem 55 | 56 | 57 | class TreeItemTranslationOptions(TranslationOptions): 58 | 59 | # These fields are for translation. 60 | fields = ('title', 'hint', 'description') 61 | 62 | 63 | translator.register(MyTreeItem, TreeItemTranslationOptions) 64 | ``` 65 | 66 | That's how you made `sitetree` work with `modeltranslation`. 67 | 68 | Read `django-modeltranslation` documentation for more information on tuning. 69 | 70 | 71 | ## django-tenants 72 | 73 | 74 | 75 | You should use a custom cache config to make it work, configure something like this on the django cache. 76 | 77 | ```python title="settings.py" 78 | 79 | CACHES = { 80 | ... 81 | "sitetree_cache": { 82 | "BACKEND": "django.core.cache.backends.dummy.DummyCache", 83 | "KEY_FUNCTION": "django_tenants.cache.make_key", 84 | "REVERSE_KEY_FUNCTION": "django_tenants.cache.reverse_key", 85 | }, 86 | } 87 | 88 | SITETREE_CACHE_NAME = "sitetree_cache" 89 | ``` 90 | 91 | -------------------------------------------------------------------------------- /docs/025_management.md: -------------------------------------------------------------------------------- 1 | # Management commands 2 | 3 | SiteTree comes with two management commands which can facilitate development and deployment processes. 4 | 5 | ## sitetreedump 6 | 7 | Sends sitetrees from database as a fixture in JSON format to output. 8 | 9 | Output all trees and items into `treedump.json` file example: 10 | ```shell 11 | python manage.py sitetreedump > treedump.json 12 | ``` 13 | 14 | You can export only trees that you need by supplying their aliases separated with spaces: 15 | ```shell 16 | python manage.py sitetreedump my_tree my_another_tree > treedump.json 17 | ``` 18 | 19 | If you need to export only tree items without trees use `--items_only` command switch: 20 | ```shell 21 | python manage.py sitetreedump --items_only my_tree > items_only_dump.json 22 | ``` 23 | 24 | Use `--help` command switch to get quick help on the command: 25 | ```shell 26 | python manage.py sitetreedump --help 27 | ``` 28 | 29 | 30 | ## sitetreeload 31 | 32 | This command loads sitetrees from a fixture in JSON format into database. 33 | 34 | !!! warning 35 | `sitetreeload` won't even try to restore permissions for sitetree items, as those should probably 36 | be tuned in production rather than exported from dev. 37 | 38 | If required you can use Django's `loaddata` management command with `sitetreedump` created dump, 39 | or the `dumpscript` from `django-extensions` to restore the permissions. 40 | 41 | 42 | The command makes use of `--mode` command switch to control import strategy. 43 | 44 | * **append** (default) mode should be used when you need to extend sitetree data 45 | that is now in DB with that from a fixture. 46 | 47 | !!! note 48 | In this mode trees and tree items identifiers from a fixture will be changed 49 | to fit existing tree structure. 50 | 51 | * **replace** mode should be used when you need to remove all sitetree data existing 52 | in DB and replace it with that from a fixture. 53 | 54 | !!! warning 55 | Replacement is irreversible. You should probably dump sitetree data 56 | if you think that you might need it someday. 57 | 58 | Using `replace` mode: 59 | ```shell 60 | python manage.py sitetreeload --mode=replace treedump.json 61 | ``` 62 | 63 | 64 | Import all trees and items from `treedump.json` file example: 65 | ```shell 66 | python manage.py sitetreeload treedump.json 67 | ``` 68 | 69 | Use `--items_into_tree` command switch and alias of target tree to import all tree 70 | items from a fixture there. This will not respect any trees information from fixture file - 71 | only tree items will be considered. **Keep in mind** also that this switch will automatically 72 | change `sitetreeload` commmand into `append` mode: 73 | ```shell 74 | python manage.py sitetreeload --items_into_tree=my_tree items_only_dump.json 75 | ``` 76 | 77 | Use `--help` command switch to get quick help on the command:: 78 | ```shell 79 | python manage.py sitetreeload --help 80 | ``` 81 | -------------------------------------------------------------------------------- /src/sitetree/fields.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from django import template 4 | from django.forms import ChoiceField 5 | from django.template.base import Parser, Token 6 | from django.utils.safestring import mark_safe 7 | 8 | from .compat import TOKEN_BLOCK 9 | from .settings import ITEMS_FIELD_ROOT_ID 10 | from .templatetags.sitetree import sitetree_tree 11 | from .utils import get_tree_item_model, get_tree_model 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 | *args, 36 | required: bool = True, 37 | widget=None, 38 | label=None, 39 | initial=None, 40 | help_text=None, 41 | **kwargs 42 | ): 43 | super().__init__( 44 | *args, 45 | required=required, widget=widget, label=label, initial=initial, 46 | help_text=help_text, **kwargs) 47 | 48 | self.tree = None 49 | self.choices_init(tree) 50 | 51 | def choices_init(self, tree: Optional['TreeBase']): 52 | """Initialize choices for the given tree. 53 | 54 | :param tree: 55 | 56 | """ 57 | if not tree: 58 | return 59 | 60 | if isinstance(tree, MODEL_TREE_CLASS): 61 | tree = tree.alias 62 | 63 | self.tree = tree 64 | self.choices = self._build_choices() 65 | 66 | def _build_choices(self): 67 | """Build choices list runtime using 'sitetree_tree' tag""" 68 | tree_token = f'sitetree_tree from "{self.tree}" template "{self.template}"' 69 | 70 | context_kwargs = {'current_app': 'admin'} 71 | context = template.Context(context_kwargs) 72 | context.update({'request': object()}) 73 | 74 | choices_str = sitetree_tree( 75 | Parser([]), Token(token_type=TOKEN_BLOCK, contents=tree_token) 76 | ).render(context) 77 | 78 | tree_choices = [(ITEMS_FIELD_ROOT_ID, self.root_title)] 79 | 80 | for line in choices_str.splitlines(): 81 | if line.strip(): 82 | splitted = line.split(':::') 83 | tree_choices.append((splitted[0], mark_safe(splitted[1]))) 84 | 85 | return tree_choices 86 | 87 | def clean(self, value): 88 | if not value: 89 | return None 90 | 91 | try: 92 | return MODEL_TREE_ITEM_CLASS.objects.get(pk=value) 93 | 94 | except MODEL_TREE_ITEM_CLASS.DoesNotExist: 95 | return None 96 | -------------------------------------------------------------------------------- /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', ['tests.testapp']) 76 | out, _ = capsys.readouterr() 77 | 78 | assert 'Sitetrees found in' in out 79 | assert len(TreeItem.objects.all()) == 2 80 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "django-sitetree" 3 | dynamic = ["version"] 4 | description = "This reusable Django app introduces site tree, menu and breadcrumbs navigation elements." 5 | authors = [ 6 | { name = "Igor Starikov", email = "idlesign@yandex.ru" } 7 | ] 8 | readme = "README.md" 9 | 10 | classifiers = [ 11 | "Development Status :: 5 - Production/Stable", # 3 - Alpha; 5 - Production/Stable 12 | "Framework :: Django", 13 | "Environment :: Web Environment", 14 | "Intended Audience :: Developers", 15 | "Operating System :: OS Independent", 16 | "Programming Language :: Python", 17 | "Programming Language :: Python :: 3", 18 | "Programming Language :: Python :: 3.10", 19 | "Programming Language :: Python :: 3.11", 20 | "Programming Language :: Python :: 3.12", 21 | "Programming Language :: Python :: 3.13", 22 | "License :: OSI Approved :: BSD License" 23 | ] 24 | 25 | license = "BSD-3-Clause" 26 | license-files = ["LICENSE"] 27 | requires-python = ">=3.10" 28 | keywords = ["navigation", "django"] 29 | dependencies = [] 30 | 31 | [project.urls] 32 | Homepage = "https://github.com/idlesign/django-sitetree" 33 | Documentation = "https://django-sitetree.readthedocs.io/" 34 | 35 | [dependency-groups] 36 | dev = [ 37 | {include-group = "docs"}, 38 | {include-group = "linters"}, 39 | {include-group = "tests"}, 40 | ] 41 | docs = [ 42 | "mkdocs-material", 43 | "mkdocs-apidescribed-plugin", 44 | "mkdocs-navsorted-plugin", 45 | ] 46 | linters = [ 47 | "ruff", 48 | ] 49 | tests = [ 50 | "pytest", 51 | "pytest-djangoapp>=1.4.2", 52 | ] 53 | 54 | [build-system] 55 | requires = ["hatchling"] 56 | build-backend = "hatchling.build" 57 | 58 | [tool.hatch.version] 59 | path = "src/sitetree/__init__.py" 60 | 61 | [tool.hatch.build.targets.wheel] 62 | packages = ["src/sitetree"] 63 | 64 | [tool.hatch.build.targets.sdist] 65 | packages = ["src/"] 66 | 67 | [tool.pytest.ini_options] 68 | testpaths = [ 69 | "tests", 70 | ] 71 | addopts = "--pyargs" 72 | 73 | [tool.coverage.run] 74 | source = [ 75 | "src/", 76 | ] 77 | omit = [ 78 | "src/sitetree/migrations/*" 79 | ] 80 | 81 | [tool.coverage.report] 82 | fail_under = 96.00 83 | exclude_also = [ 84 | "raise NotImplementedError", 85 | "if TYPE_CHECKING:", 86 | ] 87 | 88 | [tool.tox] 89 | skip_missing_interpreters = true 90 | env_list = [ 91 | "py{38,39}-dj{30,31,32,40,41,42}", 92 | "py{310,311}-dj{30,31,32,40,41,42,50,51,52}", 93 | "py{312,313}-dj{40,41,42,50,51,52}", 94 | ] 95 | 96 | [tool.tox.env_run_base] 97 | dependency_groups = ["tests"] 98 | deps = [ 99 | "dj20: Django>=2.0,<2.1", 100 | "dj21: Django>=2.1,<2.2", 101 | "dj22: Django>=2.2,<2.3", 102 | "dj30: Django>=3.0,<3.1", 103 | "dj31: Django>=3.1,<3.2", 104 | "dj32: Django>=3.2,<3.3", 105 | "dj40: Django>=4.0,<4.1", 106 | "dj41: Django>=4.1,<4.2", 107 | "dj42: Django>=4.2,<4.3", 108 | "dj50: Django>=5.0,<5.1", 109 | "dj51: Django>=5.1,<5.2", 110 | "dj52: Django>=5.2,<5.3", 111 | ] 112 | commands = [ 113 | ["pytest", { replace = "posargs", default = ["tests"], extend = true }], 114 | ] 115 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # django-sitetree Authors 2 | 3 | Created by Igor `idle sign` Starikov. 4 | 5 | 6 | ## Contributors 7 | 8 | * Anatoly Kudinov 9 | * clincher 10 | * Andrey Chibisov 11 | * Vladimir Tartynskyi 12 | * Arcady Usov 13 | * Pavel Shiryaev 14 | * Alexander Koshelev 15 | * Danilo Bargen 16 | * Silveron 17 | * Brendtron5000 18 | * Dmitry Slepichev 19 | * Arturs Vonda 20 | * Jeff Triplett 21 | * Jacob Kaplan-Moss 22 | * Sanja Zivotic 23 | * Roberto Abdelkader 24 | * Scott Adams 25 | * Rob Charlwood 26 | * thenewguy 27 | * Erika Reinhardt 28 | * Dmitry Voronin 29 | * Dave Pretty 30 | * Alexander Artemenko 31 | * ibooj 32 | * Patrick Altman 33 | * Ben Cole 34 | * Vitaliy Ivanov 35 | * Sergey Maranchuk 36 | * Martey Dodoo 37 | * Michał Suszko 38 | * Piter Vergara 39 | * Chris Lamb 40 | * stop5 41 | * PetrDlouhy 42 | * Richard Price 43 | * Walter Lorenzetti 44 | * Ramon Saraiva 45 | * Jon Kiparsky 46 | * Thomas Güttler 47 | * Bart van der Schoor 48 | * Eduardo Garcia Cebollero 49 | * Kishor Kunal Raj 50 | * Ben Finney 51 | * witwar 52 | * Jon Kiparsky 53 | * Jeffrey de Lange 54 | * Simon Klein 55 | 56 | 57 | ## Translators 58 | 59 | * Russian: Igor Starikov 60 | * Ukranian: Sergiy Gavrylov 61 | * German: Danilo Bargen 62 | * German: Markus Maurer 63 | * Persian: Ali Javadi 64 | * Spanish: Adrián López Calvo 65 | * Norwegian: Eirik Krogstad 66 | * French: Jean Traullé 67 | * Japanese: Hajime Nishida 68 | * Japanese: ToitaYuka 69 | -------------------------------------------------------------------------------- /src/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.compat import CommandOption, options_getter 5 | from sitetree.settings import APP_MODULE_NAME 6 | from sitetree.sitetreeapp import Cache 7 | from sitetree.utils import get_tree_model, import_project_sitetree_modules 8 | 9 | MODEL_TREE_CLASS = get_tree_model() 10 | 11 | 12 | get_options = options_getter(( 13 | CommandOption( 14 | '--database', action='store', dest='database', default=DEFAULT_DB_ALIAS, 15 | help='Nominates a specific database to place trees and items into. Defaults to the "default" database.' 16 | ), 17 | )) 18 | 19 | 20 | class Command(BaseCommand): 21 | 22 | help = ( 23 | '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 | -------------------------------------------------------------------------------- /docs/055_models.md: -------------------------------------------------------------------------------- 1 | # Custom Models 2 | 3 | SiteTree comes with `SiteTree` and `SiteTreeItem` built-in models to store sitetree data, 4 | but that could be customized. 5 | 6 | 7 | ## Inherit 8 | 9 | Now let's pretend you are not satisfied with `SiteTree` built-in models and want to customize them. 10 | 11 | 1. First thing you should do is to define your own `tree` and `tree item` models inherited from `TreeBase` 12 | and `TreeItemBase` classes respectively: 13 | 14 | ```python title="myapp/models.py" 15 | from sitetree.models import TreeItemBase, TreeBase 16 | 17 | 18 | class MyTree(TreeBase): 19 | """This is your custom tree model. 20 | And here you add `my_tree_field` to all fields existing in `TreeBase`. 21 | 22 | """ 23 | my_tree_field = models.CharField('My tree field', max_length=50, null=True, blank=True) 24 | 25 | 26 | class MyTreeItem(TreeItemBase): 27 | """And that's a tree item model with additional `css_class` field.""" 28 | css_class = models.CharField('Tree item CSS class', max_length=50) 29 | ``` 30 | 31 | 2. Now when `models.py` in your `myapp` application has the definitions of custom sitetree models, you need 32 | to instruct Django to use them for your project instead of built-in ones: 33 | 34 | ```python title="settings.py" 35 | # Here `myapp` is the name of your application, `MyTree` and `MyTreeItem` 36 | # are the names of your customized models. 37 | 38 | SITETREE_MODEL_TREE = 'myapp.MyTree' 39 | SITETREE_MODEL_TREE_ITEM = 'myapp.MyTreeItem' 40 | ``` 41 | 42 | 3. Run `manage.py syncdb` to install your customized models into DB. 43 | 44 | !!! note 45 | As you've added new fields to your models, you'll probably need to tune their Django Admin representation. 46 | See section on custom Admin for more information. 47 | 48 | 49 | ## Use 50 | 51 | Given the example model given above, you can now use the extra fields when defining a sitetree programmatically: 52 | 53 | ```python 54 | from sitetree.toolbox import tree, item 55 | 56 | # Be sure you defined `sitetrees` in your module. 57 | sitetrees = ( 58 | # Define a tree with `tree` function. 59 | tree('books', items=[ 60 | # Then define items and their children with `item` function. 61 | item('Books', 'books-listing', children=[ 62 | item('Book named "{{ book.title }}"', 63 | 'books-details', 64 | in_menu=False, 65 | in_sitetree=False, 66 | css_class='book-detail'), 67 | item('Add a book', 68 | 'books-add', 69 | css_class='book-add'), 70 | item('Edit "{{ book.title }}"', 71 | 'books-edit', 72 | in_menu=False, 73 | in_sitetree=False, 74 | css_class='book-edit') 75 | ]) 76 | ], title='My books tree'), 77 | # ... You can define more than one tree for your app. 78 | ) 79 | ``` 80 | 81 | ## Reference 82 | 83 | You can reference sitetree models (including customized) from other models, with the help 84 | of `MODEL_TREE`, `MODEL_TREE_ITEM` settings: 85 | 86 | 87 | ```python 88 | from sitetree.settings import MODEL_TREE, MODEL_TREE_ITEM 89 | 90 | # As taken from the above given examples 91 | # MODEL_TREE will contain `myapp.MyTree`, MODEL_TREE_ITEM - `myapp.MyTreeItem` 92 | ``` 93 | 94 | 95 | If you need to get current `tree` or `tree item` classes use `get_tree_model` and `get_tree_item_model` functions: 96 | 97 | ```python 98 | from sitetree.utils import get_tree_model, get_tree_item_model 99 | 100 | current_tree_class = get_tree_model() # MyTree from myapp.models (from the example above) 101 | current_tree_item_class = get_tree_item_model() # MyTreeItem from myapp.models (from the example above) 102 | ``` 103 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/005_quickstart.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | 1. Add the `sitetree` application to `INSTALLED_APPS` in your settings file (usually `settings.py`). 4 | 2. Check that `django.core.context_processors.request` is added to `TEMPLATE_CONTEXT_PROCESSORS` in your settings file. 5 | 6 | !!! note 7 | For Django 1.8+: it should be defined in `TEMPLATES/OPTIONS/context_processors`. 8 | 9 | 3. Check that `django.contrib.auth.context_processors.auth` is enabled in `TEMPLATE_CONTEXT_PROCESSORS` too. 10 | 4. Run `./manage.py migrate` to install sitetree tables into database. 11 | 5. Go to Django Admin site and add some trees and tree items. 12 | 6. Add `{% load sitetree %}` tag to the top of a template. 13 | 14 | 15 | ## Making a tree 16 | 17 | Taken from [StackOverflow](http://stackoverflow.com/questions/4766807/how-to-use-django-sitetree/4887916#4887916). 18 | 19 | In this tutorial we create a sitetree that could handle URI like `/categoryname/entryname`. 20 | 21 | --- 22 | 23 | To create a tree: 24 | 25 | !!! note 26 | Here we create a tree in Django admin. You can also define trees right in your code. See section on dynamic trees. 27 | 28 | 1. Go to site administration panel; 29 | 2. Click `+Add` near `Site Trees`; 30 | 3. Enter alias for your sitetree, e.g. `maintree`. You'll address your tree by this alias in template tags; 31 | 4. Push `Add Site Tree Item`; 32 | 5. Create the first item: 33 | 34 | * `Parent` - As it is root item that would have no parent. 35 | * `Title` - Let it be `My site`. 36 | * `URL` - This URL is static, so put here `/`. 37 | 38 | 6. Create a second item (that one would handle `categoryname` from your `categoryname/entryname`): 39 | 40 | * `Parent` - Choose `My site` item from step 5. 41 | * `Title` - Put here `Category #{{ category.id }}`. 42 | * `URL` - Put named URL `category-detailed category.name`. 43 | 44 | In `Additional settings`: check `URL as Pattern` checkbox. 45 | 46 | 7. Create a third item (that one would handle `entryname` from your `categoryname/entryname`): 47 | 48 | * `Parent` - Choose `Category #{{ category.id }}` item from step 6. 49 | * `Title` - Put here `Entry #{{ entry.id }}`. 50 | * `URL` - Put named URL `entry-detailed category.name entry.name`. 51 | 52 | In `Additional settings`: check `URL as Pattern` checkbox. 53 | 54 | 8. Put `{% load sitetree %}` into your template to have access to sitetree tags; 55 | 9. Put `{% sitetree_menu from "maintree" include "trunk" %}` into your template to render menu from tree trunk; 56 | 10. Put `{% sitetree_breadcrumbs from "maintree" %}` into your template to render breadcrumbs. 57 | 58 | --- 59 | 60 | Steps 6 and 7 clarifications: 61 | 62 | * In titles we use Django template variables, which would be resolved just like they do in your templates. 63 | 64 | E.g.: You made your view for `categoryname` (let's call it 'detailed_category') to pass category object 65 | into template as `category` variable. Suppose that category object has `id` property. 66 | 67 | In your template you use `{{ category.id }}` to render id. And we do just the same for site tree item in step 6. 68 | 69 | * In URLs we use Django's named URL patterns ([documentation](http://docs.djangoproject.com/en/dev/topics/http/urls/#naming-url-patterns)). 70 | That is almost identical to the usage of Django [url](http://docs.djangoproject.com/en/dev/ref/templates/builtins/#url) tag in templates. 71 | 72 | Your urls configuration for steps 6, 7 supposed to include: 73 | 74 | ```python 75 | url(r'^(?P\S+)/(?P\S+)/$', 'detailed_entry', name='entry-detailed'), 76 | url(r'^(?P\S+)/$', 'detailed_category', name='category-detailed'), 77 | ``` 78 | 79 | Take a not on `name` argument values. 80 | 81 | So, putting `entry-detailed category.name entry.name` in step 7 into URL field we tell sitetree to associate 82 | that sitetree item with URL named `entry-detailed`, passing to it `category_name` and `entry_name` parameters. 83 | 84 | Now you're ready to move to templates and use template tags. 85 | -------------------------------------------------------------------------------- /docs/020_apps.md: -------------------------------------------------------------------------------- 1 | # Dynamic trees & Trees in apps 2 | 3 | SiteTree allows you to define sitetrees within your apps. 4 | 5 | ## Define a sitetree 6 | 7 | Let's suppose you have `books` application and want to define a sitetree for it. 8 | 9 | * First create `sitetrees.py` in the directory of `books` app. 10 | 11 | * Then define a sitetree with the help of `tree` and `item` functions from `sitetree.utils` module 12 | and assign it to `sitetrees` module attribute 13 | 14 | ```python 15 | from sitetree.toolbox import tree, item 16 | 17 | # Be sure you defined `sitetrees` in your module. 18 | sitetrees = ( 19 | # Define a tree with `tree` function. 20 | tree('books', items=[ 21 | # Then define items and their children with `item` function. 22 | item('Books', 'books-listing', children=[ 23 | item('Book named "{{ book.title }}"', 'books-details', in_menu=False, in_sitetree=False), 24 | item('Add a book', 'books-add', access_by_perms=['booksapp.allow_add']), 25 | item('Edit "{{ book.title }}"', 'books-edit', in_menu=False, in_sitetree=False) 26 | ]) 27 | ]), 28 | # ... You can define more than one tree for your app. 29 | ) 30 | ``` 31 | 32 | Please see `tree` and `item` signatures for possible options. 33 | 34 | !!! note 35 | If you added extra fields to the `Tree` and `TreeItem` models, 36 | then you can specify their values when instantiating `item` see sections on custom models. 37 | 38 | 39 | ## Export sitetree to DB 40 | 41 | Now when your app has a defined sitetree you can use `sitetree_resync_apps` management command 42 | to instantly move sitetrees from every (or certain) applications into DB: 43 | 44 | ```shell 45 | python manage.py sitetree_resync_apps 46 | ``` 47 | 48 | Or solely for `books` application: 49 | 50 | ```shell 51 | python manage.py sitetree_resync_apps books 52 | ``` 53 | 54 | ## Dynamic trees 55 | 56 | Optionally you can structure app-defined sitetrees into existing or new trees runtime. 57 | 58 | Basically one should compose a dynamic tree with **compose_dynamic_tree()** and register it with **register_dynamic_trees()**. 59 | 60 | Let's suppose the following code somewhere where app registry is already created, e.g. **config.ready()** or even 61 | in `urls.py` of your project. 62 | 63 | ```python 64 | from sitetree.toolbox import tree, item, register_dynamic_trees, compose_dynamic_tree 65 | 66 | 67 | register_dynamic_trees( 68 | 69 | # Gather all the trees from `books`, 70 | compose_dynamic_tree('books'), 71 | 72 | # or gather all the trees from `books` and attach them to `main` tree root, 73 | compose_dynamic_tree('books', target_tree_alias='main'), 74 | 75 | # or gather all the trees from `books` and attach them to `for_books` aliased item in `main` tree, 76 | compose_dynamic_tree('books', target_tree_alias='main', parent_tree_item_alias='for_books'), 77 | 78 | # or even define a tree right at the process of registration. 79 | compose_dynamic_tree(( 80 | tree('dynamic', items=( 81 | item('dynamic_1', 'dynamic_1_url', children=( 82 | item('dynamic_1_sub_1', 'dynamic_1_sub_1_url'), 83 | )), 84 | item('dynamic_2', 'dynamic_2_url'), 85 | )), 86 | )), 87 | 88 | # Line below tells sitetree to drop and recreate cache, so that all newly registered 89 | # dynamic trees are rendered immediately. 90 | reset_cache=True 91 | ) 92 | ``` 93 | 94 | !!! note 95 | If you use only dynamic trees you can set `SITETREE_DYNAMIC_ONLY = True` to prevent the application 96 | from querying trees and items stored in DB. 97 | 98 | 99 | #### Access check 100 | 101 | For dynamic trees you can implement access on per tree item basis. 102 | 103 | Pass an access checking function in `access_check` argument. 104 | 105 | !!! note 106 | This function must accept `tree` argument and support pickling (e.g. be exposed on a module level). 107 | 108 | ```python 109 | def check_user_is_staff(tree): 110 | return tree.current_request.user.is_staff 111 | 112 | ... 113 | 114 | item('dynamic_2', 'dynamic_2_url', access_check=check_user_is_staff), 115 | 116 | ... 117 | ``` 118 | -------------------------------------------------------------------------------- /tests/test_admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib.admin.sites import site 2 | 3 | 4 | def get_item_admin(): 5 | from sitetree.admin import TreeItemAdmin 6 | from sitetree.models import TreeItem 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 Tree, TreeItem 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 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pytest_djangoapp import configure_djangoapp_plugin 3 | 4 | 5 | def hook(settings): 6 | settings['TEMPLATES'][0]['OPTIONS']['context_processors'].append('django.template.context_processors.request') 7 | 8 | return settings 9 | 10 | 11 | pytest_plugins = configure_djangoapp_plugin( 12 | settings=dict( 13 | SITETREE_CLS='tests.testapp.mysitetree.MySiteTree', 14 | ), 15 | admin_contrib=True, 16 | settings_hook=hook 17 | ) 18 | 19 | 20 | @pytest.fixture 21 | def build_tree(): 22 | """Builds a sitetree from dict definition. 23 | Returns items indexed by urls. 24 | 25 | Example: 26 | items_map = build_tree( 27 | {'alias': 'mytree'}, 28 | [{ 29 | 'title': 'one', 'url': '/one/', 'children': [ 30 | {'title': 'subone', 'url': '/subone/'} 31 | ] 32 | }] 33 | ) 34 | 35 | """ 36 | from django.contrib.auth.models import Permission 37 | 38 | from sitetree.models import Tree, TreeItem 39 | 40 | def build(tree_dict, items): 41 | 42 | def attach_items(tree, items, parent=None): 43 | for item_dict in items: 44 | children = item_dict.pop('children', []) 45 | 46 | access_permissions = item_dict.pop('access_permissions', []) 47 | 48 | item = TreeItem(**item_dict) 49 | item.tree = tree 50 | item.parent = parent 51 | item.save() 52 | 53 | for permission in access_permissions: 54 | item.access_permissions.add(Permission.objects.get(codename=permission)) 55 | 56 | items_map[f'{item.url}'] = item 57 | 58 | children and attach_items(tree, children, parent=item) 59 | 60 | items_map = {} 61 | 62 | tree = Tree(**tree_dict) 63 | tree.save() 64 | attach_items(tree, items) 65 | 66 | return items_map 67 | 68 | return build 69 | 70 | 71 | @pytest.fixture 72 | def common_tree(build_tree): 73 | items = build_tree( 74 | {'alias': 'mytree'}, 75 | [{ 76 | 'title': 'Home', 'url': '/home/', 'children': [ 77 | {'title': 'Users', 'url': '/users/', 'children': [ 78 | {'title': 'Moderators', 'url': '/users/moderators/'}, 79 | {'title': 'Ordinary', 'url': '/users/ordinary/'}, 80 | {'title': 'Hidden', 'hidden': True, 'url': '/users/hidden/'}, 81 | ]}, 82 | {'title': 'Articles', 'url': '/articles/', 'children': [ 83 | {'title': 'About cats', 'url': '/articles/cats/', 'children': [ 84 | {'title': 'Good', 'url': '/articles/cats/good/'}, 85 | {'title': 'Bad', 'url': '/articles/cats/bad/'}, 86 | {'title': 'Ugly', 'url': '/articles/cats/ugly/'}, 87 | ]}, 88 | {'title': 'About dogs', 'url': '/articles/dogs/'}, 89 | {'title': 'About mice', 'inmenu': False, 'url': '/articles/mice/'}, 90 | ]}, 91 | {'title': 'Contacts', 'inbreadcrumbs': False, 'url': '/contacts/', 'children': [ 92 | {'title': 'Russia', 'url': '/contacts/russia/', 93 | 'hint': 'The place', 'description': 'Russian Federation', 'children': [ 94 | {'title': 'Web', 'alias': 'ruweb', 'url': '/contacts/russia/web/', 'children': [ 95 | {'title': 'Public {{ subtitle }}', 'url': '/contacts/russia/web/public/'}, 96 | {'title': 'my model {{ model }}', 'url': '/mymodel/'}, 97 | {'title': 'Private', 98 | 'url': '/contacts/russia/web/private/', 99 | 'hint': 'Private Area Hint', 100 | 'description': 'Private Area Description', 101 | }, 102 | ]}, 103 | {'title': 'Postal', 'insitetree': False, 'url': '/contacts/russia/postal/'}, 104 | ]}, 105 | {'title': 'Australia', 'urlaspattern': True, 'url': 'contacts_australia australia_var', 106 | 'children': [ 107 | {'title': 'Alice Springs', 'access_loggedin': True, 'url': '/contacts/australia/alice/'}, 108 | {'title': 'Darwin', 'access_guest': True, 'url': '/contacts/australia/darwin/'}, 109 | ]}, 110 | {'title': 'China', 'urlaspattern': True, 'url': 'contacts_china china_var'}, 111 | ]}, 112 | ] 113 | }] 114 | ) 115 | items[''] = items['/home/'] 116 | return items 117 | -------------------------------------------------------------------------------- /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, item, register_dynamic_trees, tree 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.sitetreeapp import _IDX_ORPHAN_TREES 30 | from sitetree.toolbox import compose_dynamic_tree, get_dynamic_trees, item, register_dynamic_trees, tree 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, item, register_dynamic_trees, tree 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('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, match='Unable to register dynamic sitetree'): 113 | compose_dynamic_tree('nonexistent') 114 | 115 | from sitetree.sitetreeapp import _DYNAMIC_TREES 116 | _DYNAMIC_TREES.clear() 117 | -------------------------------------------------------------------------------- /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 compose_dynamic_tree, item, register_dynamic_trees, 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, match='(P|p)ermission'): 108 | template_render_tag( 109 | 'sitetree', '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 compose_dynamic_tree, item, register_dynamic_trees, 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, match='(P|p)ermission'): 120 | template_render_tag( 121 | 'sitetree', '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 compose_dynamic_tree, item, register_dynamic_trees, 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, match='(P|p)ermission'): 132 | template_render_tag( 133 | 'sitetree', '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 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /docs/010_tags.md: -------------------------------------------------------------------------------- 1 | # Template tags 2 | 3 | To use template tags available in SiteTree you should add `{% load sitetree %}` tag to the top of chosen template. 4 | 5 | Tree tag argument (part in double quotes, following `from` word) of SiteTree tags should contain the tree alias. 6 | 7 | !!! hint 8 | 9 | + Tree tag argument could be a template variable (do not use quotes for those). 10 | 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 | !!! example 15 | 16 | ``` 17 | {% sitetree_menu from "mytree" include "trunk,topmenu" template "mytrees/mymenu.html" %} 18 | {% sitetree_breadcrumbs from "mytree" template "mytrees/mybreadcrumbs.html" %} 19 | ``` 20 | 21 | ## sitetree_menu 22 | 23 | This tag renders menu based on sitetree. 24 | 25 | !!! example 26 | ``` 27 | {% sitetree_menu from "mytree" include "trunk,topmenu" %} 28 | ``` 29 | 30 | This command renders as a menu sitetree items from tree named `mytree`, including items **under** `trunk` and `topmenu` aliased items. 31 | 32 | That means that `trunk` and `topmenu` themselves won't appear in a menu, but rather all their ancestors. 33 | 34 | !!! hint 35 | If you need item filtering behaviour consider using a customized tree handler. 36 | 37 | Aliases are given to items through Django's admin site. 38 | 39 | ### Reserved aliases 40 | 41 | Note that there are some reserved aliases. To illustrate how do they work, take a look at the sample tree: 42 | 43 | ``` 44 | Home 45 | |-- Users 46 | | |-- Moderators 47 | | |-- Ordinary 48 | | 49 | |-- Articles 50 | | |-- About cats 51 | | | |-- Good 52 | | | |-- Bad 53 | | | |-- Ugly 54 | | | 55 | | |-- About dogs 56 | | |-- About mice 57 | | 58 | |-- Contacts 59 | | |-- Russia 60 | | | |-- Web 61 | | | | |-- Public 62 | | | | |-- Private 63 | | | | 64 | | | |-- Postal 65 | | | 66 | | |-- Australia 67 | | |-- China 68 | Exit 69 | ``` 70 | 71 | !!! note 72 | As it mentioned above, basic built-in templates won't limit the depth of rendered tree, if you need to render 73 | the limited number of levels, you ought to override the built-in templates. 74 | For brevity rendering examples below will show only top levels rendered for each alias. 75 | 76 | #### trunk 77 | 78 | Get hierarchy under trunk, i.e. root item(s) - items without parents: 79 | ``` 80 | Home 81 | Exit 82 | ``` 83 | 84 | #### this-children 85 | 86 | Get items under item resolved as current for the current page. 87 | 88 | Considering that we are now at `Articles` renders: 89 | ``` 90 | About cats 91 | About dogs 92 | About mice 93 | ``` 94 | 95 | #### this-siblings 96 | 97 | Get items under parent of item resolved as current for the current page (current item included). 98 | 99 | Considering that we are now at `Bad` renders: 100 | ``` 101 | Good 102 | Bad 103 | Ugly 104 | ``` 105 | 106 | #### this-parent-siblings 107 | 108 | Items under parent item for the item resolved as current for the current page. 109 | 110 | Considering that we are now at `Public` renders:: 111 | ``` 112 | Web 113 | Postal 114 | ``` 115 | 116 | #### this-ancestor-children 117 | 118 | Items under grandparent item (closest to root) for the item resolved as current for the current page. 119 | 120 | Considering that we are now at `Public` renders all items under `Home` (which is closest to the root). 121 | Thus, in the template tag example above `trunk` is reserved alias, and `topmenu` alias is given to an item through the admin site. 122 | 123 | !!! note 124 | Sitetree items could be addressed not only by aliases but also by IDs:: 125 | 126 | !!! example 127 | ``` 128 | {% sitetree_menu from "mytree" include "10" %} 129 | ``` 130 | 131 | ## sitetree_breadcrumbs 132 | 133 | This tag renders breadcrumbs path (from tree root to current page) based on sitetree. 134 | 135 | !!! example 136 | ``` 137 | {% sitetree_breadcrumbs from "mytree" %} 138 | ``` 139 | 140 | This command renders breadcrumbs from tree named `mytree`. 141 | 142 | ## sitetree_tree 143 | 144 | This tag renders entire site tree. 145 | 146 | !!! example 147 | ``` 148 | {% sitetree_tree from "mytree" %} 149 | ``` 150 | This command renders sitetree from tree named `mytree`. 151 | 152 | 153 | ## sitetree_page_title 154 | 155 | This tag renders current page title resolved against definite sitetree. 156 | The title is taken from a sitetree item title resolved as current for the current page. 157 | 158 | !!! example 159 | ``` 160 | {% sitetree_page_title from "mytree" %} 161 | ``` 162 | 163 | This command renders current page title from tree named `mytree`. 164 | 165 | ## sitetree_page_description 166 | 167 | This tag renders current page description resolved against definite sitetree. 168 | The description is taken from a sitetree item description resolved as current for the current page. 169 | 170 | That can be useful for meta description for an HTML page. 171 | 172 | !!! example 173 | ``` 174 | {% sitetree_page_description from "mytree" %} 175 | ``` 176 | 177 | This command renders current page description from tree named `mytree`. 178 | 179 | 180 | ## sitetree_page_hint 181 | 182 | This tag is similar to `sitetree_page_description`, but it uses data from 183 | tree item `hint` field instead of a `description` fields. 184 | 185 | !!! example 186 | ``` 187 | {% sitetree_page_hint from "mytree" %} 188 | ``` 189 | 190 | ## Settings 191 | 192 | ### SITETREE_RAISE_ITEMS_ERRORS_ON_DEBUG 193 | 194 | DEFAULT: `True` 195 | 196 | There are some rare occasions when you want to turn off errors that are thrown by sitetree even during debug. 197 | 198 | Setting `SITETREE_RAISE_ITEMS_ERRORS_ON_DEBUG = False` will turn them off. 199 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/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".' 83 | ) % '", "'.join(TREE_ITEMS_ALIASES), 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): # noqa: FBT002 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 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/sitetree/management/commands/sitetreeload.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import traceback 3 | from collections import defaultdict 4 | 5 | from django.core import serializers 6 | from django.core.exceptions import ObjectDoesNotExist 7 | from django.core.management.base import BaseCommand, CommandError 8 | from django.core.management.color import no_style 9 | from django.db import DEFAULT_DB_ALIAS, connections, router 10 | 11 | from sitetree.compat import CommandOption, options_getter 12 | from sitetree.utils import get_tree_item_model, get_tree_model 13 | 14 | MODEL_TREE_CLASS = get_tree_model() 15 | MODEL_TREE_ITEM_CLASS = get_tree_item_model() 16 | 17 | 18 | get_options = options_getter(( 19 | CommandOption( 20 | '--database', action='store', dest='database', 21 | default=DEFAULT_DB_ALIAS, help='Nominates a specific database to load fixtures into. ' 22 | 'Defaults to the "default" database.'), 23 | 24 | CommandOption( 25 | '--mode', action='store', dest='mode', default='append', 26 | help='Mode to put data into DB. Variants: `replace`, `append`.'), 27 | 28 | CommandOption( 29 | '--items_into_tree', action='store', dest='items_into_tree', default=None, 30 | help='Import only tree items data into tree with given alias.'), 31 | )) 32 | 33 | 34 | class Command(BaseCommand): 35 | 36 | option_list = get_options() 37 | 38 | help = 'Loads sitetrees from fixture in JSON format into database.' 39 | args = '[fixture_file fixture_file ...]' 40 | 41 | def add_arguments(self, parser): 42 | parser.add_argument('args', metavar='fixture', nargs='+', help='Fixture files.') 43 | get_options(parser.add_argument) 44 | 45 | def handle(self, *fixture_files, **options): 46 | 47 | using = options.get('database', DEFAULT_DB_ALIAS) 48 | mode = options.get('mode', 'append') 49 | items_into_tree = options.get('items_into_tree', None) 50 | 51 | if items_into_tree is not None: 52 | try: 53 | items_into_tree = MODEL_TREE_CLASS.objects.get(alias=items_into_tree) 54 | except ObjectDoesNotExist: 55 | raise CommandError( 56 | f'Target tree aliased `{items_into_tree}` does not exist. Please create it before import.' 57 | ) from None 58 | else: 59 | mode = 'append' 60 | 61 | connection = connections[using] 62 | cursor = connection.cursor() 63 | 64 | self.style = no_style() 65 | 66 | loaded_object_count = 0 67 | 68 | if mode == 'replace': 69 | MODEL_TREE_CLASS.objects.all().delete() 70 | MODEL_TREE_ITEM_CLASS.objects.all().delete() 71 | 72 | for fixture_file in fixture_files: 73 | 74 | self.stdout.write(f'Loading fixture from `{fixture_file}` ...\n') 75 | 76 | fixture = open(fixture_file) # noqa: PTH123 77 | 78 | try: 79 | objects = serializers.deserialize('json', fixture, using=using) 80 | except (SystemExit, KeyboardInterrupt): 81 | raise 82 | 83 | trees = [] 84 | tree_items = defaultdict(list) 85 | tree_item_parents = defaultdict(list) 86 | tree_items_new_indexes = {} 87 | 88 | try: 89 | allow_migrate = router.allow_migrate 90 | except AttributeError: 91 | # Django < 1.7 92 | allow_migrate = router.allow_syncdb 93 | 94 | for obj in objects: 95 | if allow_migrate(using, obj.object.__class__): 96 | if isinstance(obj.object, (MODEL_TREE_CLASS, MODEL_TREE_ITEM_CLASS)): 97 | if isinstance(obj.object, MODEL_TREE_CLASS): 98 | trees.append(obj.object) 99 | else: 100 | if items_into_tree is not None: 101 | obj.object.tree_id = items_into_tree.id 102 | tree_items[obj.object.tree_id].append(obj.object) 103 | tree_item_parents[obj.object.parent_id].append(obj.object.id) 104 | 105 | if items_into_tree is not None: 106 | trees = [items_into_tree,] 107 | 108 | try: 109 | 110 | for tree in trees: 111 | 112 | self.stdout.write(f'\nImporting tree `{tree.alias}` ...\n') 113 | orig_tree_id = tree.id 114 | 115 | if items_into_tree is None: 116 | if mode == 'append': 117 | tree.pk = None 118 | tree.id = None 119 | 120 | tree.save(using=using) 121 | loaded_object_count += 1 122 | 123 | parents_ahead = [] 124 | 125 | # Parents go first: enough for simple cases. 126 | tree_items[orig_tree_id].sort(key=lambda item: item.id not in tree_item_parents.keys()) 127 | 128 | for tree_item in tree_items[orig_tree_id]: 129 | parent_ahead = False 130 | self.stdout.write(f'Importing item `{tree_item.title}` ...\n') 131 | tree_item.tree_id = tree.id 132 | orig_item_id = tree_item.id 133 | 134 | if mode == 'append': 135 | tree_item.pk = None 136 | tree_item.id = None 137 | 138 | if tree_item.id in tree_items_new_indexes: 139 | tree_item.pk = tree_item.id = tree_items_new_indexes[tree_item.id] 140 | 141 | if tree_item.parent_id is not None: 142 | if tree_item.parent_id in tree_items_new_indexes: 143 | tree_item.parent_id = tree_items_new_indexes[tree_item.parent_id] 144 | else: 145 | parent_ahead = True 146 | 147 | tree_item.save(using=using) 148 | loaded_object_count += 1 149 | 150 | if mode == 'append': 151 | tree_items_new_indexes[orig_item_id] = tree_item.id 152 | if parent_ahead: 153 | parents_ahead.append(tree_item) 154 | 155 | # Second pass is necessary for tree items being imported before their parents. 156 | for tree_item in parents_ahead: 157 | tree_item.parent_id = tree_items_new_indexes[tree_item.parent_id] 158 | tree_item.save(using=using) 159 | 160 | except (SystemExit, KeyboardInterrupt): 161 | raise 162 | 163 | except Exception: # noqa: BLE001 164 | fixture.close() 165 | 166 | self.stderr.write( 167 | self.style.ERROR( 168 | f"Fixture `{fixture_file}` import error: " 169 | f"{''.join(traceback.format_exception(*sys.exc_info()))}\n") 170 | ) 171 | 172 | fixture.close() 173 | 174 | # Reset DB sequences, for DBMS with sequences support. 175 | if loaded_object_count > 0: 176 | sequence_sql = connection.ops.sequence_reset_sql(self.style, [MODEL_TREE_CLASS, MODEL_TREE_ITEM_CLASS]) 177 | if sequence_sql: 178 | self.stdout.write('Resetting DB sequences ...\n') 179 | for line in sequence_sql: 180 | cursor.execute(line) 181 | 182 | connection.close() 183 | -------------------------------------------------------------------------------- /docs/030_templatesmod.md: -------------------------------------------------------------------------------- 1 | # Built-in templates 2 | 3 | Default templates shipped with SiteTree created to have as little markup as possible in a try to fit most common website need. 4 | 5 | 6 | ### Styling 7 | 8 | Use CSS to style default templates for your needs. Templates are deliberately made simple, and only consist of **ul**, **li** and **a** tags. 9 | 10 | Nevertheless, pay attention that menu template also uses two CSS classes marking tree items: 11 | 12 | * **current_item** — marks item in the tree, corresponding to current page; 13 | * **current_branch** — marks all ancestors of current item, and current item itself. 14 | 15 | If needed, you can set extra CSS classes to the **ul** element with `extra_class_ul` variable. For example: 16 | ```html 17 | {% with extra_class_ul="flex-wrap flex-row" %} 18 | {% sitetree_menu from "footer_3" include "trunk,topmenu" template "sitetree/menu_bootstrap5.html" %} 19 | {% endwith %} 20 | ``` 21 | 22 | ## Overriding 23 | 24 | To customize visual representation of navigation elements you should override the built-in SiteTree templates as follows: 25 | 26 | 1. Switch to sitetree folder 27 | 2. Switch further to `templates/sitetree` 28 | 3. There among others you'll find the following templates: 29 | 30 | * `breadcrumbs.html` basic breadcrumbs 31 | * `breadcrumbs-title.html` breadcrumbs that can be put inside html `title` tag 32 | * `menu.html` basic menu 33 | * `tree.html` basic tree 34 | 35 | 4. Copy whichever of them you need into your project templates directory and feel free to customize it. 36 | 5. See section on advanced tags for clarification on two advanced SiteTree template tags. 37 | 38 | 39 | ## Templates for Frameworks 40 | 41 | ### Foundation 42 | 43 | Information about Foundation Framework is available at 44 | 45 | The following templates are bundled with SiteTree: 46 | 47 | * `sitetree/breadcrumbs_foundation.html` 48 | 49 | This template can be used to construct a breadcrumb navigation from a sitetree. 50 | 51 | * `sitetree/menu_foundation.html` 52 | 53 | This template can be used to construct Foundation Nav Bar (classic horizontal top menu) from a sitetree. 54 | 55 | !!! note 56 | The template renders no more than two levels of a tree with hover dropdowns for root items having children. 57 | 58 | * `sitetree/menu_foundation-vertical.html` 59 | 60 | This template can be used to construct a vertical version of Foundation Nav Bar, suitable for sidebar navigation. 61 | 62 | !!! note 63 | The template renders no more than two levels of a tree with hover dropdowns for root items having children. 64 | 65 | * `sitetree/sitetree/menu_foundation_sidenav.html` 66 | 67 | This template can be used to construct a Foundation Side Nav. 68 | !!! note 69 | The template renders only one tree level. 70 | 71 | !!! hint 72 | You can take a look at Foundation navigation elements examples at 73 | 74 | 75 | ### Bootstrap 76 | 77 | Information about Bootstrap Framework is available at 78 | 79 | The following templates are bundled with SiteTree: 80 | 81 | * `sitetree/breadcrumbs_bootstrap.html` 82 | 83 | This template can be used to construct a breadcrumb navigation from a sitetree. 84 | 85 | * `sitetree/breadcrumbs_bootstrap3.html` 86 | 87 | The same as above but for Bootstrap version 3. 88 | 89 | * `sitetree/breadcrumbs_bootstrap4.html` 90 | 91 | The same as above but for Bootstrap version 4. 92 | 93 | * `sitetree/menu_bootstrap.html` 94 | 95 | This template can be used to construct *menu contents* for Bootstrap Navbar. 96 | 97 | !!! warning 98 | To widen the number of possible use-cases for which this template can be applied, 99 | it renders only menu contents, but not Navbar container itself. 100 | 101 | This means that one should wrap `sitetree_menu` call into the appropriately styled divs 102 | (i.e. having classes `navbar`, `navbar-inner`, etc.). 103 | 104 | ```html 105 | 111 | ``` 112 | Please see Bootstrap Navbar documentation for more information on subject. 113 | 114 | !!! note 115 | The template renders no more than two levels of a tree with hover dropdowns for root items having children. 116 | 117 | * `sitetree/menu_bootstrap3.html` 118 | 119 | The same as above but for Bootstrap version 3. 120 | 121 | * `sitetree/menu_bootstrap4.html` 122 | 123 | The same as above but for Bootstrap version 4. 124 | 125 | * `sitetree/menu_bootstrap5.html` 126 | 127 | The same as above but for Bootstrap version 5. 128 | 129 | * `sitetree/menu_bootstrap_dropdown.html` 130 | 131 | One level deep dropdown menu. 132 | 133 | * `sitetree/menu_bootstrap3_dropdown.html` 134 | 135 | The same as above but for Bootstrap version 3. 136 | 137 | * `sitetree/menu_bootstrap4_dropdown.html` 138 | 139 | The same as above but for Bootstrap version 4. 140 | 141 | * `sitetree/menu_bootstrap5_dropdown.html` 142 | 143 | The same as above but for Bootstrap version 5. 144 | 145 | * `sitetree/menu_bootstrap_navlist.html` 146 | 147 | This template can be used to construct a Bootstrap Nav list. 148 | 149 | !!! note 150 | The template renders only a single level. 151 | 152 | * `sitetree/menu_bootstrap3_navpills.html` 153 | 154 | Constructs nav-pills Bootstrap 3 horizontal navigation. 155 | 156 | * `sitetree/menu_bootstrap3_deep.html` 157 | 158 | Constructs Bootstrap 3 menu with infinite submenus. 159 | Requires adding extra CSS: 160 | 161 | ```html 162 | 163 | ``` 164 | 165 | * `sitetree/menu_bootstrap4_navpills.html` 166 | 167 | The same as above but for Bootstrap version 4. 168 | 169 | * `sitetree/menu_bootstrap3_navpills-stacked.html` 170 | 171 | Constructs nav-pills Bootstrap 3 vertical navigation similar to navlist from Bootstrap 2. 172 | 173 | * `sitetree/menu_bootstrap4_navpills-stacked.html` 174 | 175 | The same as above but for Bootstrap version 4. 176 | 177 | 178 | You can find Bootstrap navigation elements examples at 179 | 180 | 181 | ### Semantic UI 182 | 183 | Information about Semantic UI Framework is available at https://semantic-ui.com/ 184 | 185 | The following templates are bundled with SiteTree: 186 | 187 | * `sitetree/breadcrumbs_semantic.html` 188 | 189 | This template can be used to construct a breadcrumb navigation from a sitetree. 190 | 191 | * `sitetree/menu_semantic.html` 192 | 193 | This template can be used to construct Semantic Menu (classic horizontal top menu) from a sitetree. 194 | 195 | !!! warning 196 | To widen the number of possible use-cases for which this template can be applied, 197 | it renders only menu contents, but not the UI Menu container itself. 198 | 199 | This means that one should wrap `sitetree_menu` call into the appropriately styled divs 200 | (i.e. having `ui menu` classes). 201 | 202 | 203 | ```html 204 | 208 | ``` 209 | 210 | Please see Semantic UI Menu documentation for more information on subject. 211 | 212 | !!! note 213 | The template renders no more than two levels of a tree with hover dropdowns for root items having children. 214 | 215 | 216 | * `sitetree/menu_semantic-vertical.html` 217 | 218 | This template can be used to construct a vertical version of Semantic UI Menu, suitable for sidebar navigation. 219 | 220 | !!! note 221 | The template renders no more than two levels of a tree with hover dropdowns for root items having children. 222 | -------------------------------------------------------------------------------- /src/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 | --------------------------------------------------------------------------------