6 | {% sitetree_menu from "main" include "trunk" template "sitetree/menu_bootstrap4.html" %}
7 |
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 |
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 %}
48 | {% for url_rule in adminform.form.known_url_rules %}
49 |
{{ url_rule|safe }}
50 | {% endfor %}
51 |
52 |
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 | [](https://pypi.python.org/pypi/django-sitetree)
6 | [](https://pypi.python.org/pypi/django-sitetree)
7 | [](https://coveralls.io/r/idlesign/django-sitetree)
8 | [](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 |
14 |
15 |
16 |
17 |
18 |
19 |
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 |
108 | {% sitetree_menu from "main" include "topmenu" template "sitetree/menu_bootstrap.html" %}
109 |
110 |
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 |
205 | MY SITE
206 | {% sitetree_menu from "main" include "topmenu" template "sitetree/menu_semantic.html" %}
207 |
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 |
--------------------------------------------------------------------------------