Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deep tree handler customization support #265

Merged
merged 2 commits into from
May 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/source/admin.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Example:

.. note::

You might also be interested in using :ref:`Tree hooks <tree-hooks>`.
You might also be interested in using :ref:`Tree handler customization <tree-custom>`.


Inlines override example
Expand Down
52 changes: 52 additions & 0 deletions docs/source/customization.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
Tree handler customization
==========================

What to do if a time comes and you need some fancy stuff done to tree items that
*django-sitetree* does not support?

.. _tree-custom:

It might be that you need some special tree items ordering in a menu, or you want to render
in a huge site tree with all articles titles that are described by one tree item in Django admin,
or god knows what else.

*django-sitetree* can facilitate on that as it allows tree handler customization
with the help of `SITETREE_CLS` setting.

1. Subclass `sitetreeapp.SiteTree` and place that class into a separate module for convenience.
2. Override methods you need for customization (usually `.apply_hook()`).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From that description I don't really understand how .apply_hook() comes into play.

Copy link
Owner Author

@idlesign idlesign May 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.appy_hook() is most generic — this description doesn't necessarily cover your very case from #263
Rather you override .get_permissions().

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, thats right. I just wanted to point out, that somebody who reads this sentence might not understand if he has to overwrite .apply_hook() or if this is just one option to deal with a usecase.

But I guess if people are doing such things they must have a look at the code and then everything is pretty self-explanatory anyways.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. If you can propose a better wording, I'd be glad.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be a little bit more explicit, like this:

You can now overwrite .apply_hook() to manipulate values at pre-defined hooks. You can also overwrite any other methods to customize to your exact needs.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Thank you.

3. Define `SITETREE_CLS` in `settings.py` of your project, showing it a dotted path to subclass.


Example:

.. code-block:: python

# myapp/mysitetree.py
from sitetree.sitetreeapp import SiteTree


class MySiteTree(SiteTree):
"""Custom tree handler to test deep customization abilities."""

def apply_hook(self, items, sender):
# Suppose we want to process only menu child items.
if tree_sender == 'menu.children':
# Lets add 'Hooked: ' to resolved titles of every item.
for item in tree_items:
item.title_resolved = 'Hooked: %s' % item.title_resolved
# Return items list mutated or not.
return tree_items

# pyproject/settings.py
...

SITETREE_CLS = 'myapp.mysitetree.MySiteTree'

...



.. note::

You might also be interested in the notes on :ref:`Overriding SiteTree Admin representation <admin-ext>`.
52 changes: 0 additions & 52 deletions docs/source/hooks.rst

This file was deleted.

2 changes: 1 addition & 1 deletion docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Table of Contents
management
templatesmod
tagsadv
hooks
customization
admin
forms
models
Expand Down
2 changes: 1 addition & 1 deletion docs/source/tags.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Usage example::

This command renders as a menu sitetree items from tree named 'mytree', including items **under** 'trunk' and 'topmenu' aliased items.
That means that 'trunk' and 'topmenu' themselves won't appear in a menu, but rather all their ancestors. If you need item filtering behaviour
please use :ref:`tree hooks <tree-hooks>`.
please use :ref:`tree handler customizations <tree-custom>`.

Aliases are given to items through Django's admin site.

Expand Down
3 changes: 3 additions & 0 deletions sitetree/settings.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from django.conf import settings


SITETREE_CLS = getattr(settings, 'SITETREE_CLS', None)
"""Allows deep tree handling customization. Accepts sitetreeap.SiteTree subclass."""

MODEL_TREE = getattr(settings, 'SITETREE_MODEL_TREE', 'sitetree.Tree')
"""Path to a tree model (app.class)."""

Expand Down
47 changes: 37 additions & 10 deletions sitetree/sitetreeapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from collections import defaultdict
from copy import deepcopy
from functools import partial
from inspect import getargspec
from inspect import getfullargspec
from threading import local

from django import VERSION
Expand All @@ -16,7 +16,7 @@
VARIABLE_TAG_START)
from django.template.defaulttags import url as url_tag
from django.template.loader import get_template
from django.utils import six
from django.utils import six, module_loading
from django.utils.encoding import python_2_unicode_compatible
from django.utils.http import urlquote
from django.utils.translation import get_language
Expand All @@ -25,11 +25,12 @@
from .exceptions import SiteTreeError
from .settings import (
ALIAS_TRUNK, ALIAS_THIS_CHILDREN, ALIAS_THIS_SIBLINGS, ALIAS_THIS_PARENT_SIBLINGS, ALIAS_THIS_ANCESTOR_CHILDREN,
UNRESOLVED_ITEM_MARKER, RAISE_ITEMS_ERRORS_ON_DEBUG, CACHE_TIMEOUT, DYNAMIC_ONLY, ADMIN_APP_NAME)
UNRESOLVED_ITEM_MARKER, RAISE_ITEMS_ERRORS_ON_DEBUG, CACHE_TIMEOUT, DYNAMIC_ONLY, ADMIN_APP_NAME, SITETREE_CLS)
from .utils import get_tree_model, get_tree_item_model, import_app_sitetree_module, generate_id_for

if False: # pragma: nocover
from django.template import Context
from django.contrib.auth.models import User
from .models import TreeItemBase


Expand Down Expand Up @@ -71,13 +72,14 @@

def get_sitetree():
"""Returns SiteTree (thread-singleton) object, implementing utility methods.
This can return the built-in or a customized (see SITETREE_CLS setting) sitetree handler.

:rtype: SiteTree
"""
sitetree = getattr(_THREAD_LOCAL, _THREAD_SITETREE, None)

if sitetree is None:
sitetree = SiteTree()
sitetree = _SITETREE_CLS()
setattr(_THREAD_LOCAL, _THREAD_SITETREE, sitetree)

return sitetree
Expand All @@ -86,6 +88,10 @@ def get_sitetree():
def register_items_hook(func):
"""Registers a hook callable to process tree items right before they are passed to templates.

.. deprecated:: 1.13.0
Items hooking with `register_items_hook` is deprecated, please use `SITETREE_CLS`
setting customization instead and override .apply_hook() method.

Callable should be able to:

a) handle ``tree_items`` and ``tree_sender`` key params.
Expand Down Expand Up @@ -118,13 +124,18 @@ def my_items_processor(tree_items, tree_sender):

:param func:
"""
warnings.warn(
'Items hooking with `register_items_hook` is deprecated, '
'please use `SITETREE_CLS` settings customization instead.',
DeprecationWarning, 2)

global _ITEMS_PROCESSOR
global _ITEMS_PROCESSOR_ARGS_LEN

_ITEMS_PROCESSOR = func

if func:
args_len = len(getargspec(func).args)
args_len = len(getfullargspec(func).args)
if args_len not in {2, 3}:
raise SiteTreeError('`register_items_hook()` expects a function with two or three arguments.')
_ITEMS_PROCESSOR_ARGS_LEN = args_len
Expand Down Expand Up @@ -372,6 +383,8 @@ def set_entry(self, entry_name, key, value):
class SiteTree(object):
"""Main logic handler."""

cache_cls = Cache # Allow customizations.

def __init__(self):
self.init(context=None)

Expand All @@ -380,7 +393,7 @@ def init(self, context):

:param Context|None context:
"""
self.cache = Cache()
self.cache = self.cache_cls()
self.current_page_context = context
self.current_request = context.get('request', None) if context else None
self.current_lang = get_language()
Expand Down Expand Up @@ -849,11 +862,13 @@ def menu(self, tree_alias, tree_branches, context):
return menu_items

def apply_hook(self, items, sender):
"""Applies item processing hook, registered with ``register_item_hook()``
to items supplied, and returns processed list.
"""Applies a custom items processing hook to items supplied, and returns processed list.
Suitable for items filtering and other manipulations.

Returns initial items list if no hook is registered.

.. note:: Use `SITETREE_CLS` setting customization and override this method.

:param list items:
:param str|unicode sender: menu, breadcrumbs, sitetree, {type}.children, {type}.has_children
:rtype: list
Expand Down Expand Up @@ -890,7 +905,7 @@ def check_access(self, item, context):
user_perms = self._current_user_permissions

if user_perms is _UNSET:
user_perms = set(context['user'].get_all_permissions())
user_perms = self.get_permissions(context['user'], item)
self._current_user_permissions = user_perms

if item.access_perm_type == MODEL_TREE_ITEM_CLASS.PERM_TYPE_ALL:
Expand All @@ -902,6 +917,15 @@ def check_access(self, item, context):

return True

def get_permissions(self, user, item):
"""Returns a set of user and group level permissions for a given user.

:param User user:
:param TreeItemBase item:
:rtype: set
"""
return user.get_all_permissions()

def breadcrumbs(self, tree_alias, context):
"""Builds and returns breadcrumb trail structure for 'sitetree_breadcrumbs' tag.

Expand Down Expand Up @@ -1085,7 +1109,7 @@ def resolve_var(self, varname, context=None):

If no context specified page context' is considered as context.

:param str|unicode varname:
:param str|unicode|FilterExpression varname:
:param Context context:
:return:
"""
Expand All @@ -1102,3 +1126,6 @@ def resolve_var(self, varname, context=None):
varname = varname

return varname


_SITETREE_CLS = module_loading.import_string(SITETREE_CLS) if SITETREE_CLS else SiteTree
3 changes: 3 additions & 0 deletions sitetree/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
from pytest_djangoapp import configure_djangoapp_plugin

pytest_plugins = configure_djangoapp_plugin(
settings=dict(
SITETREE_CLS='sitetree.tests.testapp.mysitetree.MySiteTree',
),
extend_INSTALLED_APPS=[
'django.contrib.admin',
],
Expand Down
7 changes: 7 additions & 0 deletions sitetree/tests/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,10 @@ def test_lazy_title(template_context):
get_sitetree().current_page_context = template_context()

assert title == 'herethere'


def test_customized_tree_handler(template_context):

from sitetree.sitetreeapp import get_sitetree

assert get_sitetree().customized # see MySiteTree
7 changes: 7 additions & 0 deletions sitetree/tests/testapp/mysitetree.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from sitetree.sitetreeapp import SiteTree


class MySiteTree(SiteTree):
"""Custom tree handler to test deep customization abilities."""

customized = True