Skip to content
10 changes: 5 additions & 5 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ recursive-include docs .rst
recursive-include docs .py

recursive-include sitetree/locale *
recursive-include sitetree/migrations .py
recursive-include sitetree/south_migrations .py
recursive-include sitetree/templates .html
recursive-include sitetree/templatetags .py
recursive-include sitetree/management .py
recursive-include sitetree/migrations *.py
recursive-include sitetree/south_migrations *.py
recursive-include sitetree/templates *.html
recursive-include sitetree/templatetags *.py
recursive-include sitetree/management *.py
4 changes: 2 additions & 2 deletions sitetree/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,15 @@ class TreeItemAdmin(admin.ModelAdmin):
exclude = ('tree', 'sort_order')
fieldsets = (
(_('Basic settings'), {
'fields': ('parent', 'title', 'url',)
'fields': ('parent', 'title', 'url', 'softroot_for')
}),
(_('Access settings'), {
'classes': ('collapse',),
'fields': ('access_loggedin', 'access_guest', 'access_restricted', 'access_permissions', 'access_perm_type')
}),
(_('Display settings'), {
'classes': ('collapse',),
'fields': ('hidden', 'inmenu', 'inbreadcrumbs', 'insitetree')
'fields': ('hidden', 'inmenu', 'inbreadcrumbs', 'insitetree', 'hide_from',)
}),
(_('Additional settings'), {
'classes': ('collapse',),
Expand Down
10 changes: 5 additions & 5 deletions sitetree/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,18 @@ class Migration(migrations.Migration):
'verbose_name': 'Site Tree',
'verbose_name_plural': 'Site Trees',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='TreeItem',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('title', models.CharField(help_text='Site tree item title. Can contain template variables E.g.: {{ mytitle }}.', max_length=100, verbose_name='Title')),
('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)),
('hint', models.CharField(default=b'', help_text='Some additional information about this item that is used as a hint.', max_length=200, verbose_name='Hint', blank=True)),
('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)),
('urlaspattern', models.BooleanField(default=False, help_text='Whether the given URL should be treated as a pattern.<br /><b>Note:</b> Refer to Django "URL dispatcher" documentation (e.g. "Naming URL patterns" part).', db_index=True, verbose_name='URL as Pattern')),
('hidden', models.BooleanField(default=False, help_text='Whether to show this item in navigation.', db_index=True, verbose_name='Hidden')),
('alias', sitetree.models.CharFieldNullable(max_length=80, blank=True, help_text='Short name to address site tree item from a template.<br /><b>Reserved aliases:</b> "trunk", "this-children", "this-siblings", "this-ancestor-children", "this-parent-siblings".', null=True, verbose_name='Alias', db_index=True)),
('description', models.TextField(default='', help_text='Additional comments on this item.', verbose_name='Description', blank=True)),
('alias', sitetree.models.CharFieldNullable(max_length=80, blank=True, help_text='Short name to address site tree item from a template.<br /><b>Reserved aliases:</b> "trunk", "this-children", "this-siblings", "this-ancestor-children", "this-parent-siblings", "this-softroot".', null=True, verbose_name='Alias', db_index=True)),
('description', models.TextField(default=b'', help_text='Additional comments on this item.', verbose_name='Description', blank=True)),
('inmenu', models.BooleanField(default=True, help_text='Whether to show this item in a menu.', db_index=True, verbose_name='Show in menu')),
('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')),
('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')),
Expand All @@ -45,6 +44,8 @@ class Migration(migrations.Migration):
('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')),
('access_perm_type', models.IntegerField(default=1, help_text='<b>Any</b> &mdash; user should have any of chosen permissions. <b>All</b> &mdash; user should have all chosen permissions.', verbose_name='Permissions interpretation', choices=[(1, 'Any'), (2, 'All')])),
('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)),
('softroot_for', models.CharField(default=b'', max_length=200, verbose_name='Soft root for menu', blank=True)),
('hide_from', models.CharField(default=b'', max_length=200, verbose_name='Hide from menu', blank=True)),
('access_permissions', models.ManyToManyField(to='auth.Permission', verbose_name='Permissions granting access', blank=True)),
('parent', models.ForeignKey(related_name='treeitem_parent', blank=True, to='sitetree.TreeItem', help_text='Parent site tree item.', null=True, verbose_name='Parent')),
('tree', models.ForeignKey(related_name='treeitem_tree', verbose_name='Site Tree', to='sitetree.Tree', help_text='Site tree this item belongs to.')),
Expand All @@ -54,7 +55,6 @@ class Migration(migrations.Migration):
'verbose_name': 'Site Tree Item',
'verbose_name_plural': 'Site Tree Items',
},
bases=(models.Model,),
),
migrations.AlterUniqueTogether(
name='treeitem',
Expand Down
10 changes: 10 additions & 0 deletions sitetree/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ class TreeItemBase(models.Model):
sort_order = models.IntegerField(
_('Sort order'),
help_text=_('Item position among other site tree items under the same parent.'), db_index=True, default=0)
softroot_for = models.CharField(
_('Soft root for menu'), max_length=200, blank=True, default='')
hide_from = models.CharField(
_('Hide from menu'), max_length=200, blank=True, default='')

def save(self, force_insert=False, force_update=False, **kwargs):
"""We override parent save method to set item's sort order to its' primary
Expand All @@ -133,6 +137,12 @@ def save(self, force_insert=False, force_update=False, **kwargs):
if self.sort_order == 0:
self.sort_order = self.id
self.save()

def softroot_for_list(self):
return map(unicode.strip, self.softroot_for.replace(';', ',').split(','))

def hide_from_list(self):
return map(unicode.strip, self.hide_from.replace(';', ',').split(','))

class Meta(object):
abstract = True
Expand Down
4 changes: 3 additions & 1 deletion sitetree/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
ALIAS_THIS_SIBLINGS = 'this-siblings'
ALIAS_THIS_ANCESTOR_CHILDREN = 'this-ancestor-children'
ALIAS_THIS_PARENT_SIBLINGS = 'this-parent-siblings'
ALIAS_THIS_SOFTROOT = 'this-softroot'

TREE_ITEMS_ALIASES = [
ALIAS_TRUNK,
ALIAS_THIS_CHILDREN,
ALIAS_THIS_SIBLINGS,
ALIAS_THIS_ANCESTOR_CHILDREN,
ALIAS_THIS_PARENT_SIBLINGS
ALIAS_THIS_PARENT_SIBLINGS,
ALIAS_THIS_SOFTROOT
]
120 changes: 92 additions & 28 deletions sitetree/sitetreeapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
from django.conf import settings
from django import VERSION
from django.core.cache import cache
from django.core.urlresolvers import get_resolver, LocaleRegexURLResolver
from django.db.models import signals
from django.utils import six
from django.utils.http import urlquote
from django.utils.translation import get_language
from django.utils.translation import get_language, get_language_from_path
from django.utils.encoding import python_2_unicode_compatible
from django.template import Context
from django.template.loader import get_template
Expand All @@ -23,7 +24,7 @@

from .utils import get_tree_model, get_tree_item_model, import_app_sitetree_module, generate_id_for
from .settings import (
ALIAS_TRUNK, ALIAS_THIS_CHILDREN, ALIAS_THIS_SIBLINGS, ALIAS_THIS_PARENT_SIBLINGS, ALIAS_THIS_ANCESTOR_CHILDREN,
ALIAS_TRUNK, ALIAS_THIS_CHILDREN, ALIAS_THIS_SIBLINGS, ALIAS_THIS_PARENT_SIBLINGS, ALIAS_THIS_ANCESTOR_CHILDREN, ALIAS_THIS_SOFTROOT,
UNRESOLVED_ITEM_MARKER)


Expand All @@ -47,6 +48,9 @@
_IDX_TPL = '%s|:|%s'
# SiteTree app-wise object.
_SITETREE = None
#
_LOCALE_URL_PATTERNS = None


_THREAD_LOCAL = local()
_THREAD_LANG = 'sitetree_lang'
Expand Down Expand Up @@ -487,6 +491,16 @@ def get_tree_current_item(self, tree_alias):
urls_cache[url_item][1].is_current = False
if urls_cache[url_item][0] == current_url:
current_item = urls_cache[url_item][1]
# if not found, we should try url without language prefix
if current_item is None and self.is_locale_patterns_used():
language_from_path = get_language_from_path(current_url)
if language_from_path:
current_url = current_url.replace('/%s' % language_from_path, '', 1)
if self.translation_enabled_for_path(current_url):
for url_item in urls_cache:
urls_cache[url_item][1].is_current = False
if urls_cache[url_item][0] == current_url:
current_item = urls_cache[url_item][1]

if current_item is not None:
current_item.is_current = True
Expand Down Expand Up @@ -581,6 +595,9 @@ def url(self, sitetree_item, context=None):
resolved_url = url_pattern

self.update_cache_entry_value('urls', cache_key, {url_pattern: (resolved_url, sitetree_item)})

if self.is_locale_patterns_used() and self.translation_enabled_for_path(resolved_url):
resolved_url = '/%s%s' % (self.lang_get(), resolved_url)

return resolved_url

Expand Down Expand Up @@ -630,21 +647,23 @@ def get_ancestor_level(self, current_item, deep=1):
else:
return current_item

def menu(self, tree_alias, tree_branches, context):
def menu(self, tree_alias, tree_branches, context, menu_name=None, include_parent=None):
"""Builds and returns menu structure for 'sitetree_menu' tag."""
tree_alias, sitetree_items = self.init_tree(tree_alias, context)
# No items in tree, fail silently.
if not sitetree_items:
return ''
tree_branches = self.resolve_var(tree_branches)
if menu_name:
menu_name = self.resolve_var(menu_name)

parent_isnull = False
parent_ids = []
parent_aliases = []

current_item = self.get_tree_current_item(tree_alias)
self.tree_climber(tree_alias, current_item)

# Support item addressing both through identifiers and aliases.
for branch_id in tree_branches.split(','):
branch_id = branch_id.strip()
Expand All @@ -662,27 +681,44 @@ def menu(self, tree_alias, tree_branches, context):
elif branch_id == ALIAS_THIS_PARENT_SIBLINGS and current_item is not None:
branch_id = self.get_ancestor_level(current_item, deep=2).id
parent_ids.append(branch_id)
elif branch_id == ALIAS_THIS_SOFTROOT and current_item is not None:
softroot = self.get_softroot_item(tree_alias, current_item, menu_name)
if softroot is None:
parent_isnull = True
else:
branch_id = softroot.id
parent_ids.append(branch_id)
elif branch_id.isdigit():
parent_ids.append(int(branch_id))
else:
parent_aliases.append(branch_id)

menu_items = []
for item in sitetree_items:
if not item.hidden and item.inmenu and self.check_access(item, context):
if item.parent is None:
if parent_isnull:
menu_items.append(item)
else:
if item.parent.id in parent_ids or item.parent.alias in parent_aliases:
menu_items.append(item)
if include_parent and item.id in parent_ids or item.alias in parent_aliases:
menu_items.append(item)
elif item.parent is None:
if parent_isnull:
menu_items.append(item)
else:
if item.parent.id in parent_ids or item.parent.alias in parent_aliases:
menu_items.append(item)

# Parse titles for variables.
menu_items = self.apply_hook(menu_items, 'menu')
menu_items = self.update_has_children(tree_alias, menu_items, 'menu')
menu_items = self.filter_items(menu_items, 'menu', menu_name)
menu_items = self.apply_hook(menu_items, 'menu', menu_name)
menu_items = self.update_has_children(tree_alias, menu_items, 'menu', menu_name)

# clear has_children for parent items
if include_parent:
for item in menu_items:
if item.id in parent_ids or item.alias in parent_aliases:
item.has_children = False
setattr(item, 'is_parent', True)

return menu_items

def apply_hook(self, items, sender):
def apply_hook(self, items, sender, menu_name=None):
"""Applies item processing hook, registered with ``register_item_hook()``
to items supplied, and returns processed list.
Returns initial items list if no hook is registered.
Expand Down Expand Up @@ -740,44 +776,43 @@ def tree(self, tree_alias, context):
tree_items = self.update_has_children(tree_alias, tree_items, 'sitetree')
return tree_items

def children(self, parent_item, navigation_type, use_template, context):
def children(self, parent_item, navigation_type, context, menu_name=None):
"""Builds and returns site tree item children structure
for 'sitetree_children' tag.

"""
# Resolve parent item and current tree alias.
parent_item = self.resolve_var(parent_item, context)
if menu_name:
menu_name = self.resolve_var(menu_name, context)
tree_alias, tree_items = self.get_sitetree(parent_item.tree.alias)
# Mark path to current item.
self.tree_climber(tree_alias, self.get_tree_current_item(tree_alias))

tree_items = self.get_children(tree_alias, parent_item)
tree_items = self.filter_items(tree_items, navigation_type)
tree_items = self.apply_hook(tree_items, '%s.children' % navigation_type)
tree_items = self.update_has_children(tree_alias, tree_items, navigation_type)
tree_items = self.filter_items(tree_items, navigation_type, menu_name)
tree_items = self.apply_hook(tree_items, '%s.children' % navigation_type, menu_name)
tree_items = self.update_has_children(tree_alias, tree_items, navigation_type, menu_name)

my_template = get_template(use_template)
context.update({'sitetree_items': tree_items})
return my_template.render(context)
return tree_items

def get_children(self, tree_alias, item):
if not self.current_app_is_admin():
# We do not need i18n for a tree rendered in Admin dropdown.
tree_alias = self.resolve_tree_i18n_alias(tree_alias)
return self.get_cache_entry('parents', tree_alias)[item]

def update_has_children(self, tree_alias, tree_items, navigation_type):
def update_has_children(self, tree_alias, tree_items, navigation_type, menu_name=None):
"""Updates 'has_children' attribute for tree items."""
items = []
for tree_item in tree_items:
children = self.get_children(tree_alias, tree_item)
children = self.filter_items(children, navigation_type)
children = self.filter_items(children, navigation_type, menu_name)
children = self.apply_hook(children, '%s.has_children' % navigation_type)
tree_item.has_children = len(children) > 0
items.append(tree_item)
return items

def filter_items(self, items, navigation_type=None):
def filter_items(self, items, navigation_type=None, menu_name=None):
"""Filters site tree item's children if hidden and by navigation type.
NB: We do not apply any filters to sitetree in admin app.
"""
Expand All @@ -786,7 +821,8 @@ def filter_items(self, items, navigation_type=None):
for item in items:
no_access = not self.check_access(item, self._global_context)
hidden_for_nav_type = navigation_type is not None and not getattr(item, 'in' + navigation_type, False)
if item.hidden or no_access or hidden_for_nav_type:
hidden_for_menu_name = menu_name in item.hide_from_list()
if item.hidden or no_access or hidden_for_nav_type or hidden_for_menu_name:
items_out.remove(item)
return items_out

Expand All @@ -801,6 +837,14 @@ def get_ancestor_item(self, tree_alias, start_from):
return start_from

return parent

def get_softroot_item(self, tree_alias, item, menu_name):
"""Climbs up the site tree to find soft root item for chosen one, or None."""
if menu_name in item.softroot_for_list():
return item
if not hasattr(item, 'parent') or item.parent is None:
return None
return self.get_softroot_item(tree_alias, self.get_item_by_id(tree_alias, item.parent.id), menu_name)

def tree_climber(self, tree_alias, start_from):
"""Climbs up the site tree to mark items of current branch."""
Expand Down Expand Up @@ -837,7 +881,27 @@ def resolve_var(self, varname, context=None):
varname = varname

return varname


def get_locale_url_patterns(self):
global _LOCALE_URL_PATTERNS
if _LOCALE_URL_PATTERNS is None:
_LOCALE_URL_PATTERNS = []
for url_pattern in get_resolver(None).url_patterns:
if isinstance(url_pattern, LocaleRegexURLResolver):
_LOCALE_URL_PATTERNS.extend(url_pattern.url_patterns)
return _LOCALE_URL_PATTERNS

def is_locale_patterns_used(self):
return len(self.get_locale_url_patterns()) > 0

def translation_enabled_for_path(self, path):
if path.startswith('/'):
path = path[1:]
for url_pattern in self.get_locale_url_patterns():
match = url_pattern.regex.search(path)
if match:
return True
return False

class SiteTreeError(Exception):
"""Exception class for sitetree application."""
Expand Down
Loading