diff --git a/alplora/urls.py b/alplora/urls.py
index 48bbee20..f415ddb4 100644
--- a/alplora/urls.py
+++ b/alplora/urls.py
@@ -1,4 +1,4 @@
-from django.conf.urls import url
+from django.urls import re_path as url
from .views import IndexView, LoginView, ContactView
diff --git a/datacenterlight/cms_models.py b/datacenterlight/cms_models.py
index e5a4fb86..38b2fb11 100644
--- a/datacenterlight/cms_models.py
+++ b/datacenterlight/cms_models.py
@@ -44,7 +44,7 @@ class CMSIntegration(models.Model):
class CMSFaviconExtension(PageExtension):
- favicon = FilerFileField(related_name="cms_favicon_image")
+ favicon = FilerFileField(related_name="cms_favicon_image", on_delete=models.CASCADE)
extension_pool.register(CMSFaviconExtension)
diff --git a/datacenterlight/urls.py b/datacenterlight/urls.py
index c44b94fd..ecbee052 100644
--- a/datacenterlight/urls.py
+++ b/datacenterlight/urls.py
@@ -1,4 +1,4 @@
-from django.conf.urls import url
+from django.urls import re_path as url
from django.views.generic import TemplateView, RedirectView
from utils.views import AskSSHKeyView
diff --git a/digitalglarus/models.py b/digitalglarus/models.py
index db48b81a..8006e701 100644
--- a/digitalglarus/models.py
+++ b/digitalglarus/models.py
@@ -338,7 +338,7 @@ class DGGallery(models.Model):
class DGPicture(models.Model):
gallery = models.ForeignKey(DGGallery, on_delete=models.SET_NULL, null=True,)
- image = FilerImageField(related_name='dg_gallery')
+ image = FilerImageField(related_name='dg_gallery', on_delete=models.CASCADE)
description = models.CharField(max_length=60)
def __str__(self):
diff --git a/digitalglarus/urls.py b/digitalglarus/urls.py
index 30015b2d..ce71a053 100644
--- a/digitalglarus/urls.py
+++ b/digitalglarus/urls.py
@@ -1,4 +1,4 @@
-from django.conf.urls import url
+from django.urls import re_path as url
from django.utils.translation import gettext_lazy as _
from django.contrib.auth import views as auth_views
diff --git a/dynamicweb/settings/base.py b/dynamicweb/settings/base.py
index 4b4fbc87..1b621581 100644
--- a/dynamicweb/settings/base.py
+++ b/dynamicweb/settings/base.py
@@ -112,12 +112,12 @@ INSTALLED_APPS = (
'cms', # django CMS itself
'aldryn_apphooks_config',
'aldryn_boilerplates',
- 'aldryn_categories',
+ #TODO:'aldryn_categories',
'aldryn_common',
#TODO:'aldryn_newsblog',
#TODO:'aldryn_people',
#TODO: 'aldryn_reversion',
- 'aldryn_translation_tools',
+ #TODO: 'aldryn_translation_tools',
'treebeard', # utilities for implementing a tree
'sekizai', # for javascript and css management
'menus', # helper for model independent hierarchical website navigation
diff --git a/dynamicweb/wsgi.py b/dynamicweb/wsgi.py
index 8b8d9981..9cd2e80e 100644
--- a/dynamicweb/wsgi.py
+++ b/dynamicweb/wsgi.py
@@ -2,18 +2,23 @@
WSGI config for dynamicweb project.
It exposes the WSGI callable as a module-level variable named ``application``.
-
-For more information on this file, see
-https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/
"""
import os
import sys
-from django.core.wsgi import get_wsgi_application
-# sys.path.append(os.path.dirname(__file__))
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+VENDOR_DIR = os.path.join(BASE_DIR, "vendor")
+
+# Load vendored packages before site-packages
+if VENDOR_DIR not in sys.path:
+ sys.path.insert(0, VENDOR_DIR)
+
+# Legacy Python 3.4 packages, if still required
sys.path.append('/home/app/pyvenv/lib/python3.4/site-packages/')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dynamicweb.settings.prod")
-application = get_wsgi_application()
+from django.core.wsgi import get_wsgi_application
+
+application = get_wsgi_application()
\ No newline at end of file
diff --git a/hosting/models.py b/hosting/models.py
index 12605c3e..5f472a7d 100644
--- a/hosting/models.py
+++ b/hosting/models.py
@@ -104,8 +104,8 @@ class HostingOrder(AssignPermissionsMixin, models.Model):
ORDER_DECLINED_STATUS = 'Declined'
vm_id = models.IntegerField(default=0)
- customer = models.ForeignKey(StripeCustomer)
- billing_address = models.ForeignKey(BillingAddress)
+ customer = models.ForeignKey(StripeCustomer, on_delete=models.CASCADE)
+ billing_address = models.ForeignKey(BillingAddress, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
approved = models.BooleanField(default=False)
last4 = models.CharField(max_length=4)
@@ -574,7 +574,7 @@ class VMDetail(models.Model):
class UserCardDetail(AssignPermissionsMixin, models.Model):
permissions = ('view_usercarddetail',)
- stripe_customer = models.ForeignKey(StripeCustomer)
+ stripe_customer = models.ForeignKey(StripeCustomer, on_delete=models.CASCADE)
last4 = models.CharField(max_length=4)
brand = models.CharField(max_length=128)
card_id = models.CharField(max_length=100, blank=True, default='')
diff --git a/hosting/urls.py b/hosting/urls.py
index dabc6519..b5ac1b41 100644
--- a/hosting/urls.py
+++ b/hosting/urls.py
@@ -1,4 +1,4 @@
-from django.conf.urls import url
+from django.urls import re_path as url
from django.contrib.auth import views as auth_views
from utils.views import SSHKeyCreateView, AskSSHKeyView
diff --git a/legacy_compat.py b/legacy_compat.py
index 717fc065..a97255a8 100644
--- a/legacy_compat.py
+++ b/legacy_compat.py
@@ -545,4 +545,10 @@ try:
filer_compat.python_2_unicode_compatible = python_2_unicode_compatible
except ImportError:
- pass
\ No newline at end of file
+ pass
+
+
+# Legacy packages expect django.utils.six
+if not hasattr(django.utils, "six"):
+ django.utils.six = six
+ sys.modules["django.utils.six"] = six
\ No newline at end of file
diff --git a/membership/urls.py b/membership/urls.py
index e4219d8d..72e70d45 100644
--- a/membership/urls.py
+++ b/membership/urls.py
@@ -1,5 +1,5 @@
__author__ = 'tomislav'
-from django.conf.urls import url
+from django.urls import re_path as url
from django.contrib.auth.decorators import login_required
from . import views
diff --git a/ungleich/models.py b/ungleich/models.py
index 194fdfef..9985f268 100644
--- a/ungleich/models.py
+++ b/ungleich/models.py
@@ -1,6 +1,7 @@
from cms.extensions import PageExtension
from cms.extensions.extension_pool import extension_pool
from filer.fields.image import FilerImageField
+from django.db import models
# Create your models here.
@@ -9,7 +10,7 @@ from filer.fields.image import FilerImageField
class UngleichPage(PageExtension):
# image_header = models.ImageField(upload_to='image_header')
image = FilerImageField(null=True, blank=True,
- related_name="ungleinch_page_image")
+ related_name="ungleinch_page_image", on_delete=models.CASCADE)
class Meta:
app_label = 'ungleich'
diff --git a/ungleich/urls.py b/ungleich/urls.py
index 0d78fe9e..43497640 100644
--- a/ungleich/urls.py
+++ b/ungleich/urls.py
@@ -1,4 +1,4 @@
-from django.conf.urls import url
+from django.urls import re_path as url
from . import views
from .views import PostDetailViewUngleich
diff --git a/ungleich_page/urls.py b/ungleich_page/urls.py
index 19fbe5b4..b7fb4efb 100644
--- a/ungleich_page/urls.py
+++ b/ungleich_page/urls.py
@@ -1,4 +1,4 @@
-from django.conf.urls import url
+from django.urls import re_path as url
from .views import ContactView, LandingView
from django.utils.translation import gettext_lazy as _
diff --git a/vendor/backport_collections.py b/vendor/backport_collections.py
new file mode 100644
index 00000000..3073dee3
--- /dev/null
+++ b/vendor/backport_collections.py
@@ -0,0 +1,732 @@
+__all__ = ['Counter', 'deque', 'defaultdict', 'namedtuple', 'OrderedDict']
+# For bootstrapping reasons, the collection ABCs are defined in _abcoll.py.
+# They should however be considered an integral part of collections.py.
+from backport_abcoll import *
+import backport_abcoll
+__all__ += backport_abcoll.__all__
+
+from _collections import defaultdict
+from collections import deque as _deque
+from operator import itemgetter as _itemgetter, eq as _eq
+from keyword import iskeyword as _iskeyword
+import sys as _sys
+import heapq as _heapq
+from itertools import repeat as _repeat, chain as _chain, starmap as _starmap
+from itertools import imap as _imap
+
+try:
+ from thread import get_ident as _get_ident
+except ImportError:
+ from dummy_thread import get_ident as _get_ident
+
+
+if _sys.version_info >= (2, 7):
+ import warnings as _warnings
+ _warnings.warn('Use the stock collections modules instead.', DeprecationWarning)
+
+
+################################################################################
+### OrderedDict
+################################################################################
+
+class OrderedDict(dict):
+ 'Dictionary that remembers insertion order'
+ # An inherited dict maps keys to values.
+ # The inherited dict provides __getitem__, __len__, __contains__, and get.
+ # The remaining methods are order-aware.
+ # Big-O running times for all methods are the same as regular dictionaries.
+
+ # The internal self.__map dict maps keys to links in a doubly linked list.
+ # The circular doubly linked list starts and ends with a sentinel element.
+ # The sentinel element never gets deleted (this simplifies the algorithm).
+ # Each link is stored as a list of length three: [PREV, NEXT, KEY].
+
+ def __init__(self, *args, **kwds):
+ '''Initialize an ordered dictionary. The signature is the same as
+ regular dictionaries, but keyword arguments are not recommended because
+ their insertion order is arbitrary.
+
+ '''
+ if len(args) > 1:
+ raise TypeError('expected at most 1 arguments, got %d' % len(args))
+ try:
+ self.__root
+ except AttributeError:
+ self.__root = root = [] # sentinel node
+ root[:] = [root, root, None]
+ self.__map = {}
+ self.__update(*args, **kwds)
+
+ def __setitem__(self, key, value, dict_setitem=dict.__setitem__):
+ 'od.__setitem__(i, y) <==> od[i]=y'
+ # Setting a new item creates a new link at the end of the linked list,
+ # and the inherited dictionary is updated with the new key/value pair.
+ if key not in self:
+ root = self.__root
+ last = root[0]
+ last[1] = root[0] = self.__map[key] = [last, root, key]
+ return dict_setitem(self, key, value)
+
+ def __delitem__(self, key, dict_delitem=dict.__delitem__):
+ 'od.__delitem__(y) <==> del od[y]'
+ # Deleting an existing item uses self.__map to find the link which gets
+ # removed by updating the links in the predecessor and successor nodes.
+ dict_delitem(self, key)
+ link_prev, link_next, _ = self.__map.pop(key)
+ link_prev[1] = link_next # update link_prev[NEXT]
+ link_next[0] = link_prev # update link_next[PREV]
+
+ def __iter__(self):
+ 'od.__iter__() <==> iter(od)'
+ # Traverse the linked list in order.
+ root = self.__root
+ curr = root[1] # start at the first node
+ while curr is not root:
+ yield curr[2] # yield the curr[KEY]
+ curr = curr[1] # move to next node
+
+ def __reversed__(self):
+ 'od.__reversed__() <==> reversed(od)'
+ # Traverse the linked list in reverse order.
+ root = self.__root
+ curr = root[0] # start at the last node
+ while curr is not root:
+ yield curr[2] # yield the curr[KEY]
+ curr = curr[0] # move to previous node
+
+ def clear(self):
+ 'od.clear() -> None. Remove all items from od.'
+ root = self.__root
+ root[:] = [root, root, None]
+ self.__map.clear()
+ dict.clear(self)
+
+ # -- the following methods do not depend on the internal structure --
+
+ def keys(self):
+ 'od.keys() -> list of keys in od'
+ return list(self)
+
+ def values(self):
+ 'od.values() -> list of values in od'
+ return [self[key] for key in self]
+
+ def items(self):
+ 'od.items() -> list of (key, value) pairs in od'
+ return [(key, self[key]) for key in self]
+
+ def iterkeys(self):
+ 'od.iterkeys() -> an iterator over the keys in od'
+ return iter(self)
+
+ def itervalues(self):
+ 'od.itervalues -> an iterator over the values in od'
+ for k in self:
+ yield self[k]
+
+ def iteritems(self):
+ 'od.iteritems -> an iterator over the (key, value) pairs in od'
+ for k in self:
+ yield (k, self[k])
+
+ update = MutableMapping.update
+
+ __update = update # let subclasses override update without breaking __init__
+
+ __marker = object()
+
+ def pop(self, key, default=__marker):
+ '''od.pop(k[,d]) -> v, remove specified key and return the corresponding
+ value. If key is not found, d is returned if given, otherwise KeyError
+ is raised.
+
+ '''
+ if key in self:
+ result = self[key]
+ del self[key]
+ return result
+ if default is self.__marker:
+ raise KeyError(key)
+ return default
+
+ def setdefault(self, key, default=None):
+ 'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od'
+ if key in self:
+ return self[key]
+ self[key] = default
+ return default
+
+ def popitem(self, last=True):
+ '''od.popitem() -> (k, v), return and remove a (key, value) pair.
+ Pairs are returned in LIFO order if last is true or FIFO order if false.
+
+ '''
+ if not self:
+ raise KeyError('dictionary is empty')
+ key = next(reversed(self) if last else iter(self))
+ value = self.pop(key)
+ return key, value
+
+ def __repr__(self, _repr_running={}):
+ 'od.__repr__() <==> repr(od)'
+ call_key = id(self), _get_ident()
+ if call_key in _repr_running:
+ return '...'
+ _repr_running[call_key] = 1
+ try:
+ if not self:
+ return '%s()' % (self.__class__.__name__,)
+ return '%s(%r)' % (self.__class__.__name__, self.items())
+ finally:
+ del _repr_running[call_key]
+
+ def __reduce__(self):
+ 'Return state information for pickling'
+ items = [[k, self[k]] for k in self]
+ inst_dict = vars(self).copy()
+ for k in vars(OrderedDict()):
+ inst_dict.pop(k, None)
+ if inst_dict:
+ return (self.__class__, (items,), inst_dict)
+ return self.__class__, (items,)
+
+ def copy(self):
+ 'od.copy() -> a shallow copy of od'
+ return self.__class__(self)
+
+ @classmethod
+ def fromkeys(cls, iterable, value=None):
+ '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S.
+ If not specified, the value defaults to None.
+
+ '''
+ self = cls()
+ for key in iterable:
+ self[key] = value
+ return self
+
+ def __eq__(self, other):
+ '''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive
+ while comparison to a regular mapping is order-insensitive.
+
+ '''
+ if isinstance(other, OrderedDict):
+ return dict.__eq__(self, other) and all(_imap(_eq, self, other))
+ return dict.__eq__(self, other)
+
+ def __ne__(self, other):
+ 'od.__ne__(y) <==> od!=y'
+ return not self == other
+
+ # -- the following methods support python 3.x style dictionary views --
+
+ def viewkeys(self):
+ "od.viewkeys() -> a set-like object providing a view on od's keys"
+ return KeysView(self)
+
+ def viewvalues(self):
+ "od.viewvalues() -> an object providing a view on od's values"
+ return ValuesView(self)
+
+ def viewitems(self):
+ "od.viewitems() -> a set-like object providing a view on od's items"
+ return ItemsView(self)
+
+
+################################################################################
+### namedtuple
+################################################################################
+
+_class_template = '''\
+class {typename}(tuple):
+ '{typename}({arg_list})'
+
+ __slots__ = ()
+
+ _fields = {field_names!r}
+
+ def __new__(_cls, {arg_list}):
+ 'Create new instance of {typename}({arg_list})'
+ return _tuple.__new__(_cls, ({arg_list}))
+
+ @classmethod
+ def _make(cls, iterable, new=tuple.__new__, len=len):
+ 'Make a new {typename} object from a sequence or iterable'
+ result = new(cls, iterable)
+ if len(result) != {num_fields:d}:
+ raise TypeError('Expected {num_fields:d} arguments, got %d' % len(result))
+ return result
+
+ def __repr__(self):
+ 'Return a nicely formatted representation string'
+ return '{typename}({repr_fmt})' % self
+
+ def _asdict(self):
+ 'Return a new OrderedDict which maps field names to their values'
+ return OrderedDict(zip(self._fields, self))
+
+ def _replace(_self, **kwds):
+ 'Return a new {typename} object replacing specified fields with new values'
+ result = _self._make(map(kwds.pop, {field_names!r}, _self))
+ if kwds:
+ raise ValueError('Got unexpected field names: %r' % kwds.keys())
+ return result
+
+ def __getnewargs__(self):
+ 'Return self as a plain tuple. Used by copy and pickle.'
+ return tuple(self)
+
+ __dict__ = _property(_asdict)
+
+ def __getstate__(self):
+ 'Exclude the OrderedDict from pickling'
+ pass
+
+{field_defs}
+'''
+
+_repr_template = '{name}=%r'
+
+_field_template = '''\
+ {name} = _property(_itemgetter({index:d}), doc='Alias for field number {index:d}')
+'''
+
+def namedtuple(typename, field_names, verbose=False, rename=False):
+ """Returns a new subclass of tuple with named fields.
+
+ >>> Point = namedtuple('Point', ['x', 'y'])
+ >>> Point.__doc__ # docstring for the new class
+ 'Point(x, y)'
+ >>> p = Point(11, y=22) # instantiate with positional args or keywords
+ >>> p[0] + p[1] # indexable like a plain tuple
+ 33
+ >>> x, y = p # unpack like a regular tuple
+ >>> x, y
+ (11, 22)
+ >>> p.x + p.y # fields also accessable by name
+ 33
+ >>> d = p._asdict() # convert to a dictionary
+ >>> d['x']
+ 11
+ >>> Point(**d) # convert from a dictionary
+ Point(x=11, y=22)
+ >>> p._replace(x=100) # _replace() is like str.replace() but targets named fields
+ Point(x=100, y=22)
+
+ """
+
+ # Validate the field names. At the user's option, either generate an error
+ # message or automatically replace the field name with a valid name.
+ if isinstance(field_names, basestring):
+ field_names = field_names.replace(',', ' ').split()
+ field_names = map(str, field_names)
+ typename = str(typename)
+ if rename:
+ seen = set()
+ for index, name in enumerate(field_names):
+ if (not all(c.isalnum() or c=='_' for c in name)
+ or _iskeyword(name)
+ or not name
+ or name[0].isdigit()
+ or name.startswith('_')
+ or name in seen):
+ field_names[index] = '_%d' % index
+ seen.add(name)
+ for name in [typename] + field_names:
+ if type(name) != str:
+ raise TypeError('Type names and field names must be strings')
+ if not all(c.isalnum() or c=='_' for c in name):
+ raise ValueError('Type names and field names can only contain '
+ 'alphanumeric characters and underscores: %r' % name)
+ if _iskeyword(name):
+ raise ValueError('Type names and field names cannot be a '
+ 'keyword: %r' % name)
+ if name[0].isdigit():
+ raise ValueError('Type names and field names cannot start with '
+ 'a number: %r' % name)
+ seen = set()
+ for name in field_names:
+ if name.startswith('_') and not rename:
+ raise ValueError('Field names cannot start with an underscore: '
+ '%r' % name)
+ if name in seen:
+ raise ValueError('Encountered duplicate field name: %r' % name)
+ seen.add(name)
+
+ # Fill-in the class template
+ class_definition = _class_template.format(
+ typename = typename,
+ field_names = tuple(field_names),
+ num_fields = len(field_names),
+ arg_list = repr(tuple(field_names)).replace("'", "")[1:-1],
+ repr_fmt = ', '.join(_repr_template.format(name=name)
+ for name in field_names),
+ field_defs = '\n'.join(_field_template.format(index=index, name=name)
+ for index, name in enumerate(field_names))
+ )
+ if verbose:
+ print class_definition
+
+ # Execute the template string in a temporary namespace and support
+ # tracing utilities by setting a value for frame.f_globals['__name__']
+ namespace = dict(_itemgetter=_itemgetter, __name__='namedtuple_%s' % typename,
+ OrderedDict=OrderedDict, _property=property, _tuple=tuple)
+ try:
+ exec class_definition in namespace
+ except SyntaxError as e:
+ raise SyntaxError(e.message + ':\n' + class_definition)
+ result = namespace[typename]
+
+ # For pickling to work, the __module__ variable needs to be set to the frame
+ # where the named tuple is created. Bypass this step in environments where
+ # sys._getframe is not defined (Jython for example) or sys._getframe is not
+ # defined for arguments greater than 0 (IronPython).
+ try:
+ result.__module__ = _sys._getframe(1).f_globals.get('__name__', '__main__')
+ except (AttributeError, ValueError):
+ pass
+
+ return result
+
+
+########################################################################
+### Counter
+########################################################################
+
+class Counter(dict):
+ '''Dict subclass for counting hashable items. Sometimes called a bag
+ or multiset. Elements are stored as dictionary keys and their counts
+ are stored as dictionary values.
+
+ >>> c = Counter('abcdeabcdabcaba') # count elements from a string
+
+ >>> c.most_common(3) # three most common elements
+ [('a', 5), ('b', 4), ('c', 3)]
+ >>> sorted(c) # list all unique elements
+ ['a', 'b', 'c', 'd', 'e']
+ >>> ''.join(sorted(c.elements())) # list elements with repetitions
+ 'aaaaabbbbcccdde'
+ >>> sum(c.values()) # total of all counts
+ 15
+
+ >>> c['a'] # count of letter 'a'
+ 5
+ >>> for elem in 'shazam': # update counts from an iterable
+ ... c[elem] += 1 # by adding 1 to each element's count
+ >>> c['a'] # now there are seven 'a'
+ 7
+ >>> del c['b'] # remove all 'b'
+ >>> c['b'] # now there are zero 'b'
+ 0
+
+ >>> d = Counter('simsalabim') # make another counter
+ >>> c.update(d) # add in the second counter
+ >>> c['a'] # now there are nine 'a'
+ 9
+
+ >>> c.clear() # empty the counter
+ >>> c
+ Counter()
+
+ Note: If a count is set to zero or reduced to zero, it will remain
+ in the counter until the entry is deleted or the counter is cleared:
+
+ >>> c = Counter('aaabbc')
+ >>> c['b'] -= 2 # reduce the count of 'b' by two
+ >>> c.most_common() # 'b' is still in, but its count is zero
+ [('a', 3), ('c', 1), ('b', 0)]
+
+ '''
+ # References:
+ # http://en.wikipedia.org/wiki/Multiset
+ # http://www.gnu.org/software/smalltalk/manual-base/html_node/Bag.html
+ # http://www.demo2s.com/Tutorial/Cpp/0380__set-multiset/Catalog0380__set-multiset.htm
+ # http://code.activestate.com/recipes/259174/
+ # Knuth, TAOCP Vol. II section 4.6.3
+
+ def __init__(self, iterable=None, **kwds):
+ '''Create a new, empty Counter object. And if given, count elements
+ from an input iterable. Or, initialize the count from another mapping
+ of elements to their counts.
+
+ >>> c = Counter() # a new, empty counter
+ >>> c = Counter('gallahad') # a new counter from an iterable
+ >>> c = Counter({'a': 4, 'b': 2}) # a new counter from a mapping
+ >>> c = Counter(a=4, b=2) # a new counter from keyword args
+
+ '''
+ super(Counter, self).__init__()
+ self.update(iterable, **kwds)
+
+ def __missing__(self, key):
+ 'The count of elements not in the Counter is zero.'
+ # Needed so that self[missing_item] does not raise KeyError
+ return 0
+
+ def most_common(self, n=None):
+ '''List the n most common elements and their counts from the most
+ common to the least. If n is None, then list all element counts.
+
+ >>> Counter('abcdeabcdabcaba').most_common(3)
+ [('a', 5), ('b', 4), ('c', 3)]
+
+ '''
+ # Emulate Bag.sortedByCount from Smalltalk
+ if n is None:
+ return sorted(self.iteritems(), key=_itemgetter(1), reverse=True)
+ return _heapq.nlargest(n, self.iteritems(), key=_itemgetter(1))
+
+ def elements(self):
+ '''Iterator over elements repeating each as many times as its count.
+
+ >>> c = Counter('ABCABC')
+ >>> sorted(c.elements())
+ ['A', 'A', 'B', 'B', 'C', 'C']
+
+ # Knuth's example for prime factors of 1836: 2**2 * 3**3 * 17**1
+ >>> prime_factors = Counter({2: 2, 3: 3, 17: 1})
+ >>> product = 1
+ >>> for factor in prime_factors.elements(): # loop over factors
+ ... product *= factor # and multiply them
+ >>> product
+ 1836
+
+ Note, if an element's count has been set to zero or is a negative
+ number, elements() will ignore it.
+
+ '''
+ # Emulate Bag.do from Smalltalk and Multiset.begin from C++.
+ return _chain.from_iterable(_starmap(_repeat, self.iteritems()))
+
+ # Override dict methods where necessary
+
+ @classmethod
+ def fromkeys(cls, iterable, v=None):
+ # There is no equivalent method for counters because setting v=1
+ # means that no element can have a count greater than one.
+ raise NotImplementedError(
+ 'Counter.fromkeys() is undefined. Use Counter(iterable) instead.')
+
+ def update(self, iterable=None, **kwds):
+ '''Like dict.update() but add counts instead of replacing them.
+
+ Source can be an iterable, a dictionary, or another Counter instance.
+
+ >>> c = Counter('which')
+ >>> c.update('witch') # add elements from another iterable
+ >>> d = Counter('watch')
+ >>> c.update(d) # add elements from another counter
+ >>> c['h'] # four 'h' in which, witch, and watch
+ 4
+
+ '''
+ # The regular dict.update() operation makes no sense here because the
+ # replace behavior results in the some of original untouched counts
+ # being mixed-in with all of the other counts for a mismash that
+ # doesn't have a straight-forward interpretation in most counting
+ # contexts. Instead, we implement straight-addition. Both the inputs
+ # and outputs are allowed to contain zero and negative counts.
+
+ if iterable is not None:
+ if isinstance(iterable, Mapping):
+ if self:
+ self_get = self.get
+ for elem, count in iterable.iteritems():
+ self[elem] = self_get(elem, 0) + count
+ else:
+ super(Counter, self).update(iterable) # fast path when counter is empty
+ else:
+ self_get = self.get
+ for elem in iterable:
+ self[elem] = self_get(elem, 0) + 1
+ if kwds:
+ self.update(kwds)
+
+ def subtract(self, iterable=None, **kwds):
+ '''Like dict.update() but subtracts counts instead of replacing them.
+ Counts can be reduced below zero. Both the inputs and outputs are
+ allowed to contain zero and negative counts.
+
+ Source can be an iterable, a dictionary, or another Counter instance.
+
+ >>> c = Counter('which')
+ >>> c.subtract('witch') # subtract elements from another iterable
+ >>> c.subtract(Counter('watch')) # subtract elements from another counter
+ >>> c['h'] # 2 in which, minus 1 in witch, minus 1 in watch
+ 0
+ >>> c['w'] # 1 in which, minus 1 in witch, minus 1 in watch
+ -1
+
+ '''
+ if iterable is not None:
+ self_get = self.get
+ if isinstance(iterable, Mapping):
+ for elem, count in iterable.items():
+ self[elem] = self_get(elem, 0) - count
+ else:
+ for elem in iterable:
+ self[elem] = self_get(elem, 0) - 1
+ if kwds:
+ self.subtract(kwds)
+
+ def copy(self):
+ 'Return a shallow copy.'
+ return self.__class__(self)
+
+ def __reduce__(self):
+ return self.__class__, (dict(self),)
+
+ def __delitem__(self, elem):
+ 'Like dict.__delitem__() but does not raise KeyError for missing values.'
+ if elem in self:
+ super(Counter, self).__delitem__(elem)
+
+ def __repr__(self):
+ if not self:
+ return '%s()' % self.__class__.__name__
+ items = ', '.join(map('%r: %r'.__mod__, self.most_common()))
+ return '%s({%s})' % (self.__class__.__name__, items)
+
+ # Multiset-style mathematical operations discussed in:
+ # Knuth TAOCP Volume II section 4.6.3 exercise 19
+ # and at http://en.wikipedia.org/wiki/Multiset
+ #
+ # Outputs guaranteed to only include positive counts.
+ #
+ # To strip negative and zero counts, add-in an empty counter:
+ # c += Counter()
+
+ def __add__(self, other):
+ '''Add counts from two counters.
+
+ >>> Counter('abbb') + Counter('bcc')
+ Counter({'b': 4, 'c': 2, 'a': 1})
+
+ '''
+ if not isinstance(other, Counter):
+ return NotImplemented
+ result = Counter()
+ for elem, count in self.items():
+ newcount = count + other[elem]
+ if newcount > 0:
+ result[elem] = newcount
+ for elem, count in other.items():
+ if elem not in self and count > 0:
+ result[elem] = count
+ return result
+
+ def __sub__(self, other):
+ ''' Subtract count, but keep only results with positive counts.
+
+ >>> Counter('abbbc') - Counter('bccd')
+ Counter({'b': 2, 'a': 1})
+
+ '''
+ if not isinstance(other, Counter):
+ return NotImplemented
+ result = Counter()
+ for elem, count in self.items():
+ newcount = count - other[elem]
+ if newcount > 0:
+ result[elem] = newcount
+ for elem, count in other.items():
+ if elem not in self and count < 0:
+ result[elem] = 0 - count
+ return result
+
+ def __or__(self, other):
+ '''Union is the maximum of value in either of the input counters.
+
+ >>> Counter('abbb') | Counter('bcc')
+ Counter({'b': 3, 'c': 2, 'a': 1})
+
+ '''
+ if not isinstance(other, Counter):
+ return NotImplemented
+ result = Counter()
+ for elem, count in self.items():
+ other_count = other[elem]
+ newcount = other_count if count < other_count else count
+ if newcount > 0:
+ result[elem] = newcount
+ for elem, count in other.items():
+ if elem not in self and count > 0:
+ result[elem] = count
+ return result
+
+ def __and__(self, other):
+ ''' Intersection is the minimum of corresponding counts.
+
+ >>> Counter('abbb') & Counter('bcc')
+ Counter({'b': 1})
+
+ '''
+ if not isinstance(other, Counter):
+ return NotImplemented
+ result = Counter()
+ for elem, count in self.items():
+ other_count = other[elem]
+ newcount = count if count < other_count else other_count
+ if newcount > 0:
+ result[elem] = newcount
+ return result
+
+
+if __name__ == '__main__':
+ # verify that instances can be pickled
+ from cPickle import loads, dumps
+ Point = namedtuple('Point', 'x, y', True)
+ p = Point(x=10, y=20)
+ assert p == loads(dumps(p))
+
+ # test and demonstrate ability to override methods
+ class Point(namedtuple('Point', 'x y')):
+ __slots__ = ()
+ @property
+ def hypot(self):
+ return (self.x ** 2 + self.y ** 2) ** 0.5
+ def __str__(self):
+ return 'Point: x=%6.3f y=%6.3f hypot=%6.3f' % (self.x, self.y, self.hypot)
+
+ for p in Point(3, 4), Point(14, 5/7.):
+ print p
+
+ class Point(namedtuple('Point', 'x y')):
+ 'Point class with optimized _make() and _replace() without error-checking'
+ __slots__ = ()
+ _make = classmethod(tuple.__new__)
+ def _replace(self, _map=map, **kwds):
+ return self._make(_map(kwds.get, ('x', 'y'), self))
+
+ print Point(11, 22)._replace(x=100)
+
+ Point3D = namedtuple('Point3D', Point._fields + ('z',))
+ print Point3D.__doc__
+
+ import doctest
+ TestResults = namedtuple('TestResults', 'failed attempted')
+ print TestResults(*doctest.testmod())
+
+
+########################################################################
+### deque
+########################################################################
+
+class deque(_deque):
+ """Extension of deque to support Python 2.7's operations."""
+ def __init__(self, iterable=[], maxlen=None):
+ _deque.__init__(self, iterable, maxlen)
+ self._maxlen = maxlen
+
+ @property
+ def maxlen(self):
+ return self._maxlen
+
+ def reverse(self):
+ data = []
+ while self:
+ data.append(self.pop())
+ self.extend(data)
+
+ def count(self, value):
+ return sum(1 for element in self if element == value)
diff --git a/vendor/sekizai/__init__.py b/vendor/sekizai/__init__.py
new file mode 100644
index 00000000..9d1bb721
--- /dev/null
+++ b/vendor/sekizai/__init__.py
@@ -0,0 +1 @@
+__version__ = '0.10.0'
diff --git a/vendor/sekizai/context.py b/vendor/sekizai/context.py
new file mode 100644
index 00000000..a595fa88
--- /dev/null
+++ b/vendor/sekizai/context.py
@@ -0,0 +1,12 @@
+from django.template import Context
+from sekizai.context_processors import sekizai
+
+
+class SekizaiContext(Context):
+ """
+ An alternative context to be used instead of RequestContext in places where
+ no request is available.
+ """
+ def __init__(self, *args, **kwargs):
+ super(SekizaiContext, self).__init__(*args, **kwargs)
+ self.update(sekizai())
diff --git a/vendor/sekizai/context_processors.py b/vendor/sekizai/context_processors.py
new file mode 100644
index 00000000..2c3051fb
--- /dev/null
+++ b/vendor/sekizai/context_processors.py
@@ -0,0 +1,12 @@
+from collections import defaultdict
+
+from sekizai.data import UniqueSequence
+from sekizai.helpers import get_varname
+
+
+def sekizai(request=None):
+ """
+ Simple context processor which makes sure that the SekizaiDictionary is
+ available in all templates.
+ """
+ return {get_varname(): defaultdict(UniqueSequence)}
diff --git a/vendor/sekizai/data.py b/vendor/sekizai/data.py
new file mode 100644
index 00000000..ddfda34e
--- /dev/null
+++ b/vendor/sekizai/data.py
@@ -0,0 +1,28 @@
+from collections.abc import MutableSequence
+
+
+class UniqueSequence(MutableSequence):
+ def __init__(self):
+ self.data = []
+
+ def __contains__(self, item):
+ return item in self.data
+
+ def __iter__(self):
+ return iter(self.data)
+
+ def __getitem__(self, item):
+ return self.data[item]
+
+ def __setitem__(self, key, value):
+ self.data[key] = value
+
+ def __delitem__(self, key):
+ del self.data[key]
+
+ def __len__(self):
+ return len(self.data)
+
+ def insert(self, index, value):
+ if value not in self:
+ self.data.insert(index, value)
diff --git a/vendor/sekizai/helpers.py b/vendor/sekizai/helpers.py
new file mode 100644
index 00000000..ce77fdc5
--- /dev/null
+++ b/vendor/sekizai/helpers.py
@@ -0,0 +1,177 @@
+# -*- coding: utf-8 -*-
+
+from django.conf import settings
+from django.template.base import VariableNode, Variable, Template
+from django.template import Context
+from django.template.loader import get_template
+from django.template.loader_tags import BlockNode, ExtendsNode
+
+try:
+ from django.template import engines
+except ImportError:
+ engines = None
+
+
+def _get_nodelist(tpl):
+ if isinstance(tpl, Template):
+ return tpl.nodelist
+ else:
+ return tpl.template.nodelist
+
+
+def is_variable_extend_node(node):
+ if hasattr(node, 'parent_name_expr') and node.parent_name_expr:
+ return True
+ if hasattr(node, 'parent_name') and hasattr(node.parent_name, 'filters'):
+ if (node.parent_name.filters or
+ isinstance(node.parent_name.var, Variable)):
+ return True
+ return False
+
+
+def get_context():
+ if engines is not None:
+ context = Context()
+ context.template = Template('')
+ return context
+ else:
+ return Context()
+
+
+def _extend_blocks(extend_node, blocks):
+ """
+ Extends the dictionary `blocks` with *new* blocks in the parent node
+ (recursive)
+ """
+ # we don't support variable extensions
+ if is_variable_extend_node(extend_node):
+ return
+ parent = extend_node.get_parent(get_context())
+ # Search for new blocks
+ for node in _get_nodelist(parent).get_nodes_by_type(BlockNode):
+ if node.name not in blocks:
+ blocks[node.name] = node
+ else:
+ # set this node as the super node (for {{ block.super }})
+ block = blocks[node.name]
+ seen_supers = []
+ while (hasattr(block.super, 'nodelist') and
+ block.super not in seen_supers):
+ seen_supers.append(block.super)
+ block = block.super
+ block.super = node
+ # search for further ExtendsNodes
+ for node in _get_nodelist(parent).get_nodes_by_type(ExtendsNode):
+ _extend_blocks(node, blocks)
+ break
+
+
+def _extend_nodelist(extend_node):
+ """
+ Returns a list of namespaces found in the parent template(s) of this
+ ExtendsNode
+ """
+ # we don't support variable extensions (1.3 way)
+ if is_variable_extend_node(extend_node):
+ return []
+ blocks = extend_node.blocks
+ _extend_blocks(extend_node, blocks)
+ found = []
+
+ for block in blocks.values():
+ found += _scan_namespaces(block.nodelist, block)
+
+ parent_template = extend_node.get_parent(get_context())
+ # if this is the topmost template, check for namespaces outside of blocks
+ if not _get_nodelist(parent_template).get_nodes_by_type(ExtendsNode):
+ found += _scan_namespaces(
+ _get_nodelist(parent_template),
+ None
+ )
+ else:
+ found += _scan_namespaces(
+ _get_nodelist(parent_template),
+ extend_node
+ )
+ return found
+
+
+def _scan_namespaces(nodelist, current_block=None):
+ from sekizai.templatetags.sekizai_tags import RenderBlock
+ found = []
+
+ for node in nodelist:
+ # check if this is RenderBlock node
+ if isinstance(node, RenderBlock):
+ # resolve it's name against a dummy context
+ found.append(node.kwargs['name'].resolve({}))
+ found += _scan_namespaces(node.blocks['nodelist'], node)
+ # handle {% extends ... %} tags if check_inheritance is True
+ elif isinstance(node, ExtendsNode):
+ found += _extend_nodelist(node)
+ # in block nodes we have to scan for super blocks
+ elif isinstance(node, VariableNode) and current_block:
+ if node.filter_expression.token == 'block.super':
+ if hasattr(current_block.super, 'nodelist'):
+ found += _scan_namespaces(
+ current_block.super.nodelist,
+ current_block.super
+ )
+ return found
+
+
+def get_namespaces(template):
+ compiled_template = get_template(template)
+ return _scan_namespaces(_get_nodelist(compiled_template))
+
+
+def validate_template(template, namespaces):
+ """
+ Validates that a template (or it's parents if check_inheritance is True)
+ contain all given namespaces
+ """
+ if getattr(settings, 'SEKIZAI_IGNORE_VALIDATION', False):
+ return True
+ found = get_namespaces(template)
+ for namespace in namespaces:
+ if namespace not in found:
+ return False
+ return True
+
+
+def get_varname():
+ return getattr(settings, 'SEKIZAI_VARNAME', 'SEKIZAI_CONTENT_HOLDER')
+
+
+class Watcher(object):
+ """
+ Watches a context for changes to the sekizai data, so it can be replayed
+ later. This is useful for caching.
+
+ NOTE: This class assumes you ONLY ADD, NEVER REMOVE data from the context!
+ """
+ def __init__(self, context):
+ self.context = context
+ self.frozen = dict(
+ (key, list(value)) for key, value in self.data.items()
+ )
+
+ @property
+ def data(self):
+ return self.context.get(get_varname(), {})
+
+ def get_changes(self):
+ sfrozen = set(self.frozen)
+ sdata = set(self.data)
+ new_keys = sfrozen ^ sdata
+ changes = {}
+ for key in new_keys:
+ changes[key] = list(self.data[key])
+ shared_keys = sfrozen & sdata
+ for key in shared_keys:
+ old_set = set(self.frozen[key])
+ new_values = [
+ item for item in self.data[key] if item not in old_set
+ ]
+ changes[key] = new_values
+ return changes
diff --git a/vendor/sekizai/models.py b/vendor/sekizai/models.py
new file mode 100644
index 00000000..e69de29b
diff --git a/vendor/sekizai/templatetags/__init__.py b/vendor/sekizai/templatetags/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/vendor/sekizai/templatetags/sekizai_tags.py b/vendor/sekizai/templatetags/sekizai_tags.py
new file mode 100644
index 00000000..8ae56077
--- /dev/null
+++ b/vendor/sekizai/templatetags/sekizai_tags.py
@@ -0,0 +1,164 @@
+try:
+ from importlib import import_module
+except ImportError:
+ from django.utils.importlib import import_module
+
+from classytags.arguments import Argument, Flag
+from classytags.core import Tag, Options
+from classytags.parser import Parser
+from django import template
+from django.conf import settings
+
+from sekizai.helpers import get_varname
+
+register = template.Library()
+
+
+def validate_context(context):
+ """
+ Validates a given context.
+
+ Returns True if the context is valid.
+
+ Returns False if the context is invalid but the error should be silently
+ ignored.
+
+ Raises a TemplateSyntaxError if the context is invalid and we're in debug
+ mode.
+ """
+ try:
+ template_debug = context.template.engine.debug
+ except AttributeError:
+ try:
+ # Get the default engine debug value
+ template_debug = template.Engine.get_default().debug
+ except AttributeError:
+ # Django 1.9 and below fallback
+ template_debug = settings.TEMPLATE_DEBUG
+
+ if get_varname() in context:
+ return True
+ if not template_debug:
+ return False
+ raise template.TemplateSyntaxError(
+ "You must enable the 'sekizai.context_processors.sekizai' template "
+ "context processor or use 'sekizai.context.SekizaiContext' to "
+ "render your templates."
+ )
+
+
+def import_processor(import_path):
+ if '.' not in import_path:
+ raise TypeError("Import paths must contain at least one '.'")
+ module_name, object_name = import_path.rsplit('.', 1)
+ module = import_module(module_name)
+ return getattr(module, object_name)
+
+
+class SekizaiParser(Parser):
+ def parse_blocks(self):
+ super(SekizaiParser, self).parse_blocks()
+ self.blocks['nodelist'] = self.parser.parse()
+
+
+class AddtoblockParser(Parser):
+ def parse_blocks(self):
+ name = self.kwargs['name'].var.token
+ self.blocks['nodelist'] = self.parser.parse(
+ ('endaddtoblock', 'endaddtoblock %s' % name)
+ )
+ self.parser.delete_first_token()
+
+
+class SekizaiTag(Tag):
+ def render(self, context):
+ if validate_context(context):
+ return super(SekizaiTag, self).render(context)
+ return ''
+
+
+class RenderBlock(Tag):
+ name = 'render_block'
+
+ options = Options(
+ Argument('name'),
+ 'postprocessor',
+ Argument('postprocessor', required=False, default=None, resolve=False),
+ parser_class=SekizaiParser,
+ )
+
+ def render_tag(self, context, name, postprocessor, nodelist):
+ if not validate_context(context):
+ return nodelist.render(context)
+ rendered_contents = nodelist.render(context)
+ varname = get_varname()
+ data = '\n'.join(context[varname][name])
+ if postprocessor:
+ func = import_processor(postprocessor)
+ data = func(context, data, name)
+ return '%s\n%s' % (data, rendered_contents)
+register.tag('render_block', RenderBlock)
+
+
+class AddData(SekizaiTag):
+ name = 'add_data'
+
+ options = Options(
+ Argument('key'),
+ Argument('value'),
+ )
+
+ def render_tag(self, context, key, value):
+ varname = get_varname()
+ context[varname][key].append(value)
+ return ''
+register.tag('add_data', AddData)
+
+
+class WithData(SekizaiTag):
+ name = 'with_data'
+
+ options = Options(
+ Argument('name'),
+ 'as',
+ Argument('variable', resolve=False),
+ blocks=[
+ ('end_with_data', 'inner_nodelist'),
+ ],
+ parser_class=SekizaiParser,
+ )
+
+ def render_tag(self, context, name, variable, inner_nodelist, nodelist):
+ rendered_contents = nodelist.render(context)
+ varname = get_varname()
+ data = context[varname][name]
+ context.push()
+ context[variable] = data
+ inner_contents = inner_nodelist.render(context)
+ context.pop()
+ return '%s\n%s' % (inner_contents, rendered_contents)
+register.tag('with_data', WithData)
+
+
+class Addtoblock(SekizaiTag):
+ name = 'addtoblock'
+
+ options = Options(
+ Argument('name'),
+ Flag('strip', default=False, true_values=['strip']),
+ 'preprocessor',
+ Argument('preprocessor', required=False, default=None, resolve=False),
+ parser_class=AddtoblockParser,
+ )
+
+ def render_tag(self, context, name, strip, preprocessor, nodelist):
+ rendered_contents = nodelist.render(context)
+ if strip:
+ rendered_contents = rendered_contents.strip()
+ if preprocessor:
+ func = import_processor(preprocessor)
+ rendered_contents = func(context, rendered_contents, name)
+ varname = get_varname()
+ context[varname][name].append(rendered_contents)
+ return ""
+register.tag('addtoblock', Addtoblock)
diff --git a/vendor/sekizai/test_templates/basic.html b/vendor/sekizai/test_templates/basic.html
new file mode 100644
index 00000000..d2f1771d
--- /dev/null
+++ b/vendor/sekizai/test_templates/basic.html
@@ -0,0 +1,19 @@
+{% load sekizai_tags %}
+
+{% render_block "css" %}
+
+some content
+
+{% addtoblock "css" %}
+my css file
+{% endaddtoblock %}
+
+more content
+
+{% addtoblock "js" %}
+my js file
+{% endaddtoblock %}
+
+final content
+
+{% render_block "js" %}
diff --git a/vendor/sekizai/test_templates/css.html b/vendor/sekizai/test_templates/css.html
new file mode 100644
index 00000000..29caf784
--- /dev/null
+++ b/vendor/sekizai/test_templates/css.html
@@ -0,0 +1,7 @@
+{% load sekizai_tags %}
+
+{% render_block "css-to-file" %}
+
+{% addtoblock "css-to-file" %}
+
+{% endaddtoblock %}
\ No newline at end of file
diff --git a/vendor/sekizai/test_templates/css2.html b/vendor/sekizai/test_templates/css2.html
new file mode 100644
index 00000000..a970a563
--- /dev/null
+++ b/vendor/sekizai/test_templates/css2.html
@@ -0,0 +1,6 @@
+{% load sekizai_tags %}
+
+{% render_block "css-onefile" %}
+
+{% addtoblock "css-onefile" %} {% endaddtoblock %}
+{% addtoblock "css-onefile" %} {% endaddtoblock %}
\ No newline at end of file
diff --git a/vendor/sekizai/test_templates/easy_base.html b/vendor/sekizai/test_templates/easy_base.html
new file mode 100644
index 00000000..9587dd58
--- /dev/null
+++ b/vendor/sekizai/test_templates/easy_base.html
@@ -0,0 +1,5 @@
+{% load sekizai_tags %}
+
+{% render_block "css" %}
+
+{% block main %}content{% endblock %}
\ No newline at end of file
diff --git a/vendor/sekizai/test_templates/easy_inherit.html b/vendor/sekizai/test_templates/easy_inherit.html
new file mode 100644
index 00000000..93b86362
--- /dev/null
+++ b/vendor/sekizai/test_templates/easy_inherit.html
@@ -0,0 +1,3 @@
+{% extends "easy_base.html" %}
+
+{% block main %}{{ block.super }}{% endblock %}
\ No newline at end of file
diff --git a/vendor/sekizai/test_templates/eat.html b/vendor/sekizai/test_templates/eat.html
new file mode 100644
index 00000000..ccfad256
--- /dev/null
+++ b/vendor/sekizai/test_templates/eat.html
@@ -0,0 +1,7 @@
+{% load sekizai_tags %}
+
+{% addtoblock "css" %}
+my css file
+{% endaddtoblock %}
+
+mycontent
\ No newline at end of file
diff --git a/vendor/sekizai/test_templates/errors/failadd.html b/vendor/sekizai/test_templates/errors/failadd.html
new file mode 100644
index 00000000..8631ed8a
--- /dev/null
+++ b/vendor/sekizai/test_templates/errors/failadd.html
@@ -0,0 +1,5 @@
+{% load sekizai_tags %}
+
+{% addtoblock %}
+file one
+{% endaddtoblock %}
\ No newline at end of file
diff --git a/vendor/sekizai/test_templates/errors/failbase.html b/vendor/sekizai/test_templates/errors/failbase.html
new file mode 100644
index 00000000..75c78de5
--- /dev/null
+++ b/vendor/sekizai/test_templates/errors/failbase.html
@@ -0,0 +1,9 @@
+{% load sekizai_tags %}
+
+{% render_block "js" %}
+
+{% include "errors/failinc.html" %}
+
+{% addtoblock "js" %}
+file one
+{% endaddtoblock %}
\ No newline at end of file
diff --git a/vendor/sekizai/test_templates/errors/failbase2.html b/vendor/sekizai/test_templates/errors/failbase2.html
new file mode 100644
index 00000000..4c8affeb
--- /dev/null
+++ b/vendor/sekizai/test_templates/errors/failbase2.html
@@ -0,0 +1,9 @@
+{% load sekizai_tags %}
+
+{% include "errors/failinc.html" %}
+
+{% addtoblock "js" %}
+file one
+{% endaddtoblock %}
+
+{% render_block "js" %}
\ No newline at end of file
diff --git a/vendor/sekizai/test_templates/errors/failinc.html b/vendor/sekizai/test_templates/errors/failinc.html
new file mode 100644
index 00000000..269a8e9a
--- /dev/null
+++ b/vendor/sekizai/test_templates/errors/failinc.html
@@ -0,0 +1,5 @@
+{% load sekizai_tags %}
+
+{% addtoblock "js %}
+file two
+{% endaddtoblock %}
\ No newline at end of file
diff --git a/vendor/sekizai/test_templates/errors/failrender.html b/vendor/sekizai/test_templates/errors/failrender.html
new file mode 100644
index 00000000..b03ebf8f
--- /dev/null
+++ b/vendor/sekizai/test_templates/errors/failrender.html
@@ -0,0 +1,3 @@
+{% load sekizai_tags %}
+
+{% render_block %}
\ No newline at end of file
diff --git a/vendor/sekizai/test_templates/inherit/base.html b/vendor/sekizai/test_templates/inherit/base.html
new file mode 100644
index 00000000..0c5dac11
--- /dev/null
+++ b/vendor/sekizai/test_templates/inherit/base.html
@@ -0,0 +1,29 @@
+{% load sekizai_tags %}
+
+head start
+
+{% render_block "css" %}
+
+head end
+
+include start
+
+{% include "inherit/baseinc.html" %}
+
+include end
+
+block main start
+{% block "main" %}
+block main base contents
+{% endblock %}
+block main end
+
+body pre-end
+
+{% render_block "js" %}
+
+body end
+
+{% addtoblock "css" %}
+some css file
+{% endaddtoblock %}
\ No newline at end of file
diff --git a/vendor/sekizai/test_templates/inherit/baseinc.html b/vendor/sekizai/test_templates/inherit/baseinc.html
new file mode 100644
index 00000000..f9866c81
--- /dev/null
+++ b/vendor/sekizai/test_templates/inherit/baseinc.html
@@ -0,0 +1,7 @@
+{% load sekizai_tags %}
+
+inc add js
+
+{% addtoblock "js" %}
+inc js file
+{% endaddtoblock %}
\ No newline at end of file
diff --git a/vendor/sekizai/test_templates/inherit/chain.html b/vendor/sekizai/test_templates/inherit/chain.html
new file mode 100644
index 00000000..f003b340
--- /dev/null
+++ b/vendor/sekizai/test_templates/inherit/chain.html
@@ -0,0 +1,7 @@
+{% extends "inherit/extend.html" %}
+
+{% block "main" %}
+{{ block.super }}
+
+{{ other_var }}
+{% endblock %}
\ No newline at end of file
diff --git a/vendor/sekizai/test_templates/inherit/extend.html b/vendor/sekizai/test_templates/inherit/extend.html
new file mode 100644
index 00000000..f9e734db
--- /dev/null
+++ b/vendor/sekizai/test_templates/inherit/extend.html
@@ -0,0 +1,11 @@
+{% extends "inherit/base.html" %}
+
+{% load sekizai_tags %}
+
+{% addtoblock "css" %}
+invisible css file
+{% endaddtoblock %}
+
+{% block "main" %}
+{% include "inherit/extinc.html" %}
+{% endblock %}
\ No newline at end of file
diff --git a/vendor/sekizai/test_templates/inherit/extinc.html b/vendor/sekizai/test_templates/inherit/extinc.html
new file mode 100644
index 00000000..b035c20e
--- /dev/null
+++ b/vendor/sekizai/test_templates/inherit/extinc.html
@@ -0,0 +1,3 @@
+{% load sekizai_tags %}
+
+extinc
diff --git a/vendor/sekizai/test_templates/inherit/nullbase.html b/vendor/sekizai/test_templates/inherit/nullbase.html
new file mode 100644
index 00000000..e69de29b
diff --git a/vendor/sekizai/test_templates/inherit/nullext.html b/vendor/sekizai/test_templates/inherit/nullext.html
new file mode 100644
index 00000000..8c8d0b03
--- /dev/null
+++ b/vendor/sekizai/test_templates/inherit/nullext.html
@@ -0,0 +1,5 @@
+{% extends "inherit/nullbase.html" %}
+
+{% load sekizai_tags %}
+
+{% render_block "js" %}
\ No newline at end of file
diff --git a/vendor/sekizai/test_templates/inherit/spacechain.html b/vendor/sekizai/test_templates/inherit/spacechain.html
new file mode 100644
index 00000000..154d083d
--- /dev/null
+++ b/vendor/sekizai/test_templates/inherit/spacechain.html
@@ -0,0 +1 @@
+ {% extends "inherit/extend.html" %}
\ No newline at end of file
diff --git a/vendor/sekizai/test_templates/inherit/subvarchain.html b/vendor/sekizai/test_templates/inherit/subvarchain.html
new file mode 100644
index 00000000..630ea1b9
--- /dev/null
+++ b/vendor/sekizai/test_templates/inherit/subvarchain.html
@@ -0,0 +1 @@
+{% extends "inherit/varchain.html" %}
\ No newline at end of file
diff --git a/vendor/sekizai/test_templates/inherit/super_blocks.html b/vendor/sekizai/test_templates/inherit/super_blocks.html
new file mode 100644
index 00000000..ba57ac33
--- /dev/null
+++ b/vendor/sekizai/test_templates/inherit/super_blocks.html
@@ -0,0 +1,15 @@
+{% extends "inherit/base.html" %}
+
+{% load sekizai_tags %}
+
+{% addtoblock "css" %}
+invisible css file
+{% endaddtoblock %}
+
+{% block "main" %}
+{{ block.super }}
+{% addtoblock "css" %}
+visible css file
+{% endaddtoblock %}
+more contents
+{% endblock %}
\ No newline at end of file
diff --git a/vendor/sekizai/test_templates/inherit/varchain.html b/vendor/sekizai/test_templates/inherit/varchain.html
new file mode 100644
index 00000000..d08942a3
--- /dev/null
+++ b/vendor/sekizai/test_templates/inherit/varchain.html
@@ -0,0 +1,2 @@
+{% extends var %}
+
diff --git a/vendor/sekizai/test_templates/named_end.html b/vendor/sekizai/test_templates/named_end.html
new file mode 100644
index 00000000..75e3b60d
--- /dev/null
+++ b/vendor/sekizai/test_templates/named_end.html
@@ -0,0 +1,7 @@
+{% load sekizai_tags %}
+
+{% addtoblock "myblock" %}
+mycontent
+{% endaddtoblock "myblock" %}
+
+{% render_block "myblock" %}
diff --git a/vendor/sekizai/test_templates/namespaces.html b/vendor/sekizai/test_templates/namespaces.html
new file mode 100644
index 00000000..b0b40836
--- /dev/null
+++ b/vendor/sekizai/test_templates/namespaces.html
@@ -0,0 +1,16 @@
+{% load sekizai_tags %}
+
+{% render_block "one" %}
+{% render_block "two" %}
+
+{% addtoblock "one" %}
+the same file
+{% endaddtoblock %}
+
+{% addtoblock "two" %}
+the same file
+{% endaddtoblock %}
+
+{% addtoblock "one" %}
+the same file
+{% endaddtoblock %}
\ No newline at end of file
diff --git a/vendor/sekizai/test_templates/processors/addtoblock_namespace.html b/vendor/sekizai/test_templates/processors/addtoblock_namespace.html
new file mode 100644
index 00000000..b5ee5821
--- /dev/null
+++ b/vendor/sekizai/test_templates/processors/addtoblock_namespace.html
@@ -0,0 +1,11 @@
+{% load sekizai_tags %}
+
+header
+
+{% addtoblock "js" preprocessor "sekizai.tests.namespace_processor" %}
+javascript
+{% endaddtoblock %}
+
+footer
+
+{% render_block "js" %}
diff --git a/vendor/sekizai/test_templates/processors/addtoblock_null.html b/vendor/sekizai/test_templates/processors/addtoblock_null.html
new file mode 100644
index 00000000..a483a29e
--- /dev/null
+++ b/vendor/sekizai/test_templates/processors/addtoblock_null.html
@@ -0,0 +1,11 @@
+{% load sekizai_tags %}
+
+header
+
+{% addtoblock "js" preprocessor "sekizai.tests.null_processor" %}
+javascript
+{% endaddtoblock %}
+
+footer
+
+{% render_block "js" %}
diff --git a/vendor/sekizai/test_templates/processors/namespace.html b/vendor/sekizai/test_templates/processors/namespace.html
new file mode 100644
index 00000000..0451a881
--- /dev/null
+++ b/vendor/sekizai/test_templates/processors/namespace.html
@@ -0,0 +1,11 @@
+{% load sekizai_tags %}
+
+header
+
+{% addtoblock "js" %}
+javascript
+{% endaddtoblock %}
+
+footer
+
+{% render_block "js" postprocessor "sekizai.tests.namespace_processor" %}
diff --git a/vendor/sekizai/test_templates/processors/null.html b/vendor/sekizai/test_templates/processors/null.html
new file mode 100644
index 00000000..fcea547f
--- /dev/null
+++ b/vendor/sekizai/test_templates/processors/null.html
@@ -0,0 +1,11 @@
+{% load sekizai_tags %}
+
+header
+
+{% addtoblock "js" %}
+javascript
+{% endaddtoblock %}
+
+footer
+
+{% render_block "js" postprocessor "sekizai.tests.null_processor" %}
diff --git a/vendor/sekizai/test_templates/unique.html b/vendor/sekizai/test_templates/unique.html
new file mode 100644
index 00000000..e6189df3
--- /dev/null
+++ b/vendor/sekizai/test_templates/unique.html
@@ -0,0 +1,6 @@
+{% load sekizai_tags %}
+
+{% render_block "unique" %}
+
+{% addtoblock "unique" %}unique data{% endaddtoblock %}
+{% addtoblock "unique" %}unique data{% endaddtoblock %}
diff --git a/vendor/sekizai/test_templates/variables.html b/vendor/sekizai/test_templates/variables.html
new file mode 100644
index 00000000..8c5250d5
--- /dev/null
+++ b/vendor/sekizai/test_templates/variables.html
@@ -0,0 +1,16 @@
+{% load sekizai_tags %}
+
+{% addtoblock blockname %}
+file two
+{% endaddtoblock %}
+
+{% addtoblock "one" %}
+file two
+{% endaddtoblock %}
+
+{% addtoblock blockname|upper %}
+file one
+{% endaddtoblock %}
+
+{% render_block "ONE" %}
+{% render_block blockname %}
\ No newline at end of file
diff --git a/vendor/sekizai/test_templates/with_data.html b/vendor/sekizai/test_templates/with_data.html
new file mode 100644
index 00000000..e714342b
--- /dev/null
+++ b/vendor/sekizai/test_templates/with_data.html
@@ -0,0 +1,9 @@
+{% load sekizai_tags %}
+{% with_data "key" as mylist %}
+{% for obj in mylist %}
+{{ obj }}
+{% endfor %}
+{% end_with_data %}
+
+{% add_data "key" 1 %}
+{% add_data "key" 2 %}
\ No newline at end of file
diff --git a/vendor/sekizai/tests.py b/vendor/sekizai/tests.py
new file mode 100644
index 00000000..8e55f61d
--- /dev/null
+++ b/vendor/sekizai/tests.py
@@ -0,0 +1,483 @@
+from __future__ import with_statement
+from difflib import SequenceMatcher
+import os
+from unittest import TestCase
+import sys
+
+import django
+from django import template
+from django.conf import settings
+from django.template.loader import render_to_string
+import pep8
+
+from sekizai.context import SekizaiContext
+from sekizai.helpers import get_namespaces
+from sekizai.helpers import get_varname
+from sekizai.helpers import validate_template
+from sekizai.helpers import Watcher
+from sekizai.templatetags.sekizai_tags import import_processor
+from sekizai.templatetags.sekizai_tags import validate_context
+
+try:
+ unicode_compat = unicode
+except NameError:
+ unicode_compat = str
+
+try:
+ from io import StringIO
+except ImportError:
+ from StringIO import StringIO
+
+
+def null_processor(context, data, namespace):
+ return ''
+
+
+def namespace_processor(context, data, namespace):
+ return namespace
+
+
+class SettingsOverride(object):
+ """
+ Overrides Django settings within a context and resets them to their inital
+ values on exit.
+
+ Example:
+
+ with SettingsOverride(DEBUG=True):
+ # do something
+ """
+ class NULL:
+ pass
+
+ def __init__(self, **overrides):
+ self.overrides = overrides
+
+ def __enter__(self):
+ self.old = {}
+ for key, value in self.overrides.items():
+ self.old[key] = getattr(settings, key, self.NULL)
+ setattr(settings, key, value)
+
+ def __exit__(self, type, value, traceback):
+ for key, value in self.old.items():
+ if value is self.NULL:
+ delattr(settings, key)
+ else:
+ setattr(settings, key, value)
+
+
+class CaptureStdout(object):
+ """
+ Overrides sys.stdout with a StringIO stream.
+ """
+ def __init__(self):
+ self.old = None
+
+ def __enter__(self):
+ self.old = sys.stdout
+ new = sys.stdout = StringIO()
+ return new
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ sys.stdout = self.old
+
+
+class Match(tuple): # pragma: no cover
+ @property
+ def a(self):
+ return self[0]
+
+ @property
+ def b(self):
+ return self[1]
+
+ @property
+ def size(self):
+ return self[2]
+
+
+def _backwards_compat_match(thing): # pragma: no cover
+ if isinstance(thing, tuple):
+ return Match(thing)
+ return thing
+
+
+class BitDiffResult(object):
+
+ def __init__(self, status, message):
+ self.status = status
+ self.message = message
+
+
+class BitDiff(object):
+
+ """
+ Visual aid for failing tests
+ """
+
+ def __init__(self, expected):
+ self.expected = [repr(unicode_compat(bit)) for bit in expected]
+
+ def test(self, result):
+ result = [repr(unicode_compat(bit)) for bit in result]
+ if self.expected == result:
+ return BitDiffResult(True, "success")
+ else: # pragma: no cover
+ longest = max(
+ [len(x) for x in self.expected] +
+ [len(x) for x in result] +
+ [len('Expected')]
+ )
+ sm = SequenceMatcher()
+ sm.set_seqs(self.expected, result)
+ matches = sm.get_matching_blocks()
+ lasta = 0
+ lastb = 0
+ data = []
+ for match in [_backwards_compat_match(match) for match in matches]:
+ unmatcheda = self.expected[lasta:match.a]
+ unmatchedb = result[lastb:match.b]
+ unmatchedlen = max([len(unmatcheda), len(unmatchedb)])
+ unmatcheda += ['' for x in range(unmatchedlen)]
+ unmatchedb += ['' for x in range(unmatchedlen)]
+ for i in range(unmatchedlen):
+ data.append((False, unmatcheda[i], unmatchedb[i]))
+ for i in range(match.size):
+ data.append((
+ True, self.expected[match.a + i], result[match.b + i]
+ ))
+ lasta = match.a + match.size
+ lastb = match.b + match.size
+ padlen = (longest - len('Expected'))
+ padding = ' ' * padlen
+ line1 = '-' * padlen
+ line2 = '-' * (longest - len('Result'))
+ msg = '\nExpected%s | | Result' % padding
+ msg += '\n--------%s-|---|-------%s' % (line1, line2)
+ for success, a, b in data:
+ pad = ' ' * (longest - len(a))
+ if success:
+ msg += '\n%s%s | | %s' % (a, pad, b)
+ else:
+ msg += '\n%s%s | ! | %s' % (a, pad, b)
+ return BitDiffResult(False, msg)
+
+
+def update_template_debug(debug=True):
+ """
+ Helper method for updating the template debug option based on
+ the django version. Use the results of this function as the context.
+
+ :return: SettingsOverride object
+ """
+ if django.VERSION[0] == 1 and django.VERSION[1] < 8:
+ return SettingsOverride(TEMPLATE_DEBUG=debug)
+ else:
+ # Create our overridden template settings with debug turned off.
+ templates_override = settings.TEMPLATES
+ templates_override[0]['OPTIONS'].update({
+ 'debug': debug
+ })
+
+ from django.template.engine import Engine
+ # Engine gets created based on template settings initial value so
+ # changing the settings after the fact won't update, so do it
+ # manually. Necessary when testing validate_context
+ # with render method and want debug off.
+ Engine.get_default().debug = debug
+ return SettingsOverride(TEMPLATES=templates_override)
+
+
+class SekizaiTestCase(TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls._template_dirs = settings.TEMPLATE_DIRS
+ template_dir = os.path.join(
+ os.path.dirname(__file__),
+ 'test_templates'
+ )
+ settings.TEMPLATE_DIRS = list(cls._template_dirs) + [template_dir]
+
+ @classmethod
+ def tearDownClass(cls):
+ settings.TEMPLATE_DIRS = cls._template_dirs
+
+ def _render(self, tpl, ctx=None, ctxclass=SekizaiContext):
+ ctx = ctx or {}
+ return render_to_string(tpl, ctxclass(ctx))
+
+ def _get_bits(self, tpl, ctx=None, ctxclass=SekizaiContext):
+ ctx = ctx or {}
+ rendered = self._render(tpl, ctx, ctxclass)
+ bits = [
+ bit for bit in [bit.strip('\n')
+ for bit in rendered.split('\n')] if bit
+ ]
+ return bits, rendered
+
+ def _test(self, tpl, res, ctx=None, ctxclass=SekizaiContext):
+ """
+ Helper method to render template and compare it's bits
+ """
+ ctx = ctx or {}
+ bits, rendered = self._get_bits(tpl, ctx, ctxclass)
+ differ = BitDiff(res)
+ result = differ.test(bits)
+ self.assertTrue(result.status, result.message)
+ return rendered
+
+ def test_pep8(self):
+ sekizai_dir = os.path.dirname(os.path.abspath(__file__))
+ pep8style = pep8.StyleGuide()
+ with CaptureStdout() as stdout:
+ result = pep8style.check_files([sekizai_dir])
+ errors = stdout.getvalue()
+ self.assertEqual(
+ result.total_errors, 0,
+ "Code not PEP8 compliant:\n{0}".format(errors)
+ )
+
+ def test_basic_dual_block(self):
+ """
+ Basic dual block testing
+ """
+ bits = [
+ 'my css file', 'some content', 'more content', 'final content',
+ 'my js file'
+ ]
+ self._test('basic.html', bits)
+
+ def test_named_endaddtoblock(self):
+ """
+ Testing with named endaddblock
+ """
+ bits = ["mycontent"]
+ self._test('named_end.html', bits)
+
+ def test_eat_content_before_render_block(self):
+ """
+ Testing that content get's eaten if no render_blocks is available
+ """
+ bits = ["mycontent"]
+ self._test("eat.html", bits)
+
+ def test_sekizai_context_required(self):
+ """
+ Test that the template tags properly fail if not used with either
+ SekizaiContext or the context processor.
+ """
+ self.assertRaises(
+ template.TemplateSyntaxError,
+ self._render, 'basic.html', {}, template.Context
+ )
+
+ def test_complex_template_inheritance(self):
+ """
+ Test that (complex) template inheritances work properly
+ """
+ bits = [
+ "head start",
+ "some css file",
+ "head end",
+ "include start",
+ "inc add js",
+ "include end",
+ "block main start",
+ "extinc",
+ "block main end",
+ "body pre-end",
+ "inc js file",
+ "body end"
+ ]
+ self._test("inherit/extend.html", bits)
+ """
+ Test that blocks (and block.super) work properly with sekizai
+ """
+ bits = [
+ "head start",
+ "visible css file",
+ "some css file",
+ "head end",
+ "include start",
+ "inc add js",
+ "include end",
+ "block main start",
+ "block main base contents",
+ "more contents",
+ "block main end",
+ "body pre-end",
+ "inc js file",
+ "body end"
+ ]
+ self._test("inherit/super_blocks.html", bits)
+
+ def test_namespace_isolation(self):
+ """
+ Tests that namespace isolation works
+ """
+ bits = ["the same file", "the same file"]
+ self._test('namespaces.html', bits)
+
+ def test_variable_namespaces(self):
+ """
+ Tests variables and filtered variables as block names.
+ """
+ bits = ["file one", "file two"]
+ self._test('variables.html', bits, {'blockname': 'one'})
+
+ def test_invalid_addtoblock(self):
+ """
+ Tests that template syntax errors are raised properly in templates
+ rendered by sekizai tags
+ """
+ self.assertRaises(
+ template.TemplateSyntaxError,
+ self._render, 'errors/failadd.html'
+ )
+
+ def test_invalid_renderblock(self):
+ self.assertRaises(
+ template.TemplateSyntaxError,
+ self._render, 'errors/failrender.html'
+ )
+
+ def test_invalid_include(self):
+ self.assertRaises(
+ template.TemplateSyntaxError,
+ self._render, 'errors/failinc.html'
+ )
+
+ def test_invalid_basetemplate(self):
+ self.assertRaises(
+ template.TemplateSyntaxError,
+ self._render, 'errors/failbase.html'
+ )
+
+ def test_invalid_basetemplate_two(self):
+ self.assertRaises(
+ template.TemplateSyntaxError,
+ self._render, 'errors/failbase2.html'
+ )
+
+ def test_with_data(self):
+ """
+ Tests the with_data/add_data tags.
+ """
+ bits = ["1", "2"]
+ self._test('with_data.html', bits)
+
+ def test_easy_inheritance(self):
+ self.assertEqual('content', self._render("easy_inherit.html").strip())
+
+ def test_validate_context(self):
+ sekizai_ctx = SekizaiContext()
+ django_ctx = template.Context()
+ self.assertRaises(
+ template.TemplateSyntaxError,
+ validate_context, django_ctx
+ )
+ self.assertEqual(validate_context(sekizai_ctx), True)
+
+ with update_template_debug(debug=False):
+ self.assertEqual(validate_context(django_ctx), False)
+ self.assertEqual(validate_context(sekizai_ctx), True)
+ bits = ['some content', 'more content', 'final content']
+ self._test('basic.html', bits, ctxclass=template.Context)
+
+ def test_post_processor_null(self):
+ bits = ['header', 'footer']
+ self._test('processors/null.html', bits)
+
+ def test_post_processor_namespace(self):
+ bits = ['header', 'footer', 'js']
+ self._test('processors/namespace.html', bits)
+
+ def test_import_processor_failfast(self):
+ self.assertRaises(TypeError, import_processor, 'invalidpath')
+
+ def test_unique(self):
+ bits = ['unique data']
+ self._test('unique.html', bits)
+
+ def test_strip(self):
+ tpl = template.Template("""
+ {% load sekizai_tags %}
+ {% addtoblock 'a' strip %} test{% endaddtoblock %}
+ {% addtoblock 'a' strip %}test {% endaddtoblock %}
+ {% render_block 'a' %}""")
+ context = SekizaiContext()
+ output = tpl.render(context)
+ self.assertEqual(output.count('test'), 1, output)
+
+ def test_addtoblock_processor_null(self):
+ bits = ['header', 'footer']
+ self._test('processors/addtoblock_null.html', bits)
+
+ def test_addtoblock_processor_namespace(self):
+ bits = ['header', 'footer', 'js']
+ self._test('processors/addtoblock_namespace.html', bits)
+
+
+class HelperTests(TestCase):
+
+ def test_validate_template_js_css(self):
+ self.assertTrue(validate_template('basic.html', ['js', 'css']))
+
+ def test_validate_template_js(self):
+ self.assertTrue(validate_template('basic.html', ['js']))
+
+ def test_validate_template_css(self):
+ self.assertTrue(validate_template('basic.html', ['css']))
+
+ def test_validate_template_empty(self):
+ self.assertTrue(validate_template('basic.html', []))
+
+ def test_validate_template_notfound(self):
+ self.assertFalse(validate_template('basic.html', ['notfound']))
+
+ def test_get_namespaces_easy_inherit(self):
+ self.assertEqual(get_namespaces('easy_inherit.html'), ['css'])
+
+ def test_get_namespaces_chain_inherit(self):
+ self.assertEqual(get_namespaces('inherit/chain.html'), ['css', 'js'])
+
+ def test_get_namespaces_space_chain_inherit(self):
+ self.assertEqual(
+ get_namespaces('inherit/spacechain.html'),
+ ['css', 'js']
+ )
+
+ def test_get_namespaces_var_inherit(self):
+ self.assertEqual(get_namespaces('inherit/varchain.html'), [])
+
+ def test_get_namespaces_sub_var_inherit(self):
+ self.assertEqual(get_namespaces('inherit/subvarchain.html'), [])
+
+ def test_get_namespaces_null_ext(self):
+ self.assertEqual(get_namespaces('inherit/nullext.html'), [])
+
+ def test_deactivate_validate_template(self):
+ with SettingsOverride(SEKIZAI_IGNORE_VALIDATION=True):
+ self.assertTrue(validate_template('basic.html', ['js', 'css']))
+ self.assertTrue(validate_template('basic.html', ['js']))
+ self.assertTrue(validate_template('basic.html', ['css']))
+ self.assertTrue(validate_template('basic.html', []))
+ self.assertTrue(validate_template('basic.html', ['notfound']))
+
+ def test_watcher_add_namespace(self):
+ context = SekizaiContext()
+ watcher = Watcher(context)
+ varname = get_varname()
+ context[varname]['key'].append('value')
+ changes = watcher.get_changes()
+ self.assertEqual(changes, {'key': ['value']})
+
+ def test_watcher_add_data(self):
+ context = SekizaiContext()
+ varname = get_varname()
+ context[varname]['key'].append('value')
+ watcher = Watcher(context)
+ context[varname]['key'].append('value2')
+ changes = watcher.get_changes()
+ self.assertEqual(changes, {'key': ['value2']})
diff --git a/vendor/stored_messages/__init__.py b/vendor/stored_messages/__init__.py
new file mode 100644
index 00000000..9080d090
--- /dev/null
+++ b/vendor/stored_messages/__init__.py
@@ -0,0 +1,11 @@
+# flake8: noqa
+from __future__ import absolute_import
+
+from django.contrib.messages.api import *
+from stored_messages.api import *
+from stored_messages.constants import *
+
+
+__version__ = '1.4.0'
+
+VERSION = __version__ # synonym
diff --git a/vendor/stored_messages/admin.py b/vendor/stored_messages/admin.py
new file mode 100644
index 00000000..8447ee02
--- /dev/null
+++ b/vendor/stored_messages/admin.py
@@ -0,0 +1,7 @@
+from django.contrib import admin
+from .models import Inbox, Message, MessageArchive
+
+
+admin.site.register(Inbox)
+admin.site.register(Message)
+admin.site.register(MessageArchive)
diff --git a/vendor/stored_messages/api.py b/vendor/stored_messages/api.py
new file mode 100644
index 00000000..697be93e
--- /dev/null
+++ b/vendor/stored_messages/api.py
@@ -0,0 +1,69 @@
+from .settings import stored_messages_settings
+
+
+__all__ = (
+ 'add_message_for',
+ 'broadcast_message',
+ 'mark_read',
+ 'mark_all_read',
+)
+
+
+def add_message_for(users, level, message_text, extra_tags='', date=None, url=None, fail_silently=False):
+ """
+ Send a message to a list of users without passing through `django.contrib.messages`
+
+ :param users: an iterable containing the recipients of the messages
+ :param level: message level
+ :param message_text: the string containing the message
+ :param extra_tags: like the Django api, a string containing extra tags for the message
+ :param date: a date, different than the default timezone.now
+ :param url: an optional url
+ :param fail_silently: not used at the moment
+ """
+ BackendClass = stored_messages_settings.STORAGE_BACKEND
+ backend = BackendClass()
+ m = backend.create_message(level, message_text, extra_tags, date, url)
+ backend.archive_store(users, m)
+ backend.inbox_store(users, m)
+
+
+def broadcast_message(level, message_text, extra_tags='', date=None, url=None, fail_silently=False):
+ """
+ Send a message to all users aka broadcast.
+
+ :param level: message level
+ :param message_text: the string containing the message
+ :param extra_tags: like the Django api, a string containing extra tags for the message
+ :param date: a date, different than the default timezone.now
+ :param url: an optional url
+ :param fail_silently: not used at the moment
+ """
+ from django.contrib.auth import get_user_model
+ users = get_user_model().objects.all()
+ add_message_for(users, level, message_text, extra_tags=extra_tags, date=date, url=url, fail_silently=fail_silently)
+
+
+def mark_read(user, message):
+ """
+ Mark message instance as read for user.
+ Returns True if the message was `unread` and thus actually marked as `read` or False in case
+ it is already `read` or it does not exist at all.
+
+ :param user: user instance for the recipient
+ :param message: a Message instance to mark as read
+ """
+ BackendClass = stored_messages_settings.STORAGE_BACKEND
+ backend = BackendClass()
+ backend.inbox_delete(user, message)
+
+
+def mark_all_read(user):
+ """
+ Mark all message instances for a user as read.
+
+ :param user: user instance for the recipient
+ """
+ BackendClass = stored_messages_settings.STORAGE_BACKEND
+ backend = BackendClass()
+ backend.inbox_purge(user)
diff --git a/vendor/stored_messages/backends/__init__.py b/vendor/stored_messages/backends/__init__.py
new file mode 100644
index 00000000..78ce3276
--- /dev/null
+++ b/vendor/stored_messages/backends/__init__.py
@@ -0,0 +1,3 @@
+# flake8: noqa
+from .default.backend import DefaultBackend
+from .redis.backend import RedisBackend
diff --git a/vendor/stored_messages/backends/base.py b/vendor/stored_messages/backends/base.py
new file mode 100644
index 00000000..df8a026b
--- /dev/null
+++ b/vendor/stored_messages/backends/base.py
@@ -0,0 +1,159 @@
+
+class StoredMessagesBackend(object):
+ """
+
+ """
+ def create_message(self, level, msg_text, extra_tags, date=None):
+ """
+ Create and return a `Message` instance.
+ Instance types depend on backends implementation.
+
+ Params:
+ `level`: message level (see django.contrib.messages)
+ `msg_text`: what you think it is
+ `extra_tags`: see django.contrib.messages
+ `date`: a DateTime (optional)
+
+ Return:
+ `Message` instance
+ """
+ raise NotImplementedError()
+
+ def inbox_list(self, user):
+ """
+ Retrieve all the messages in `user`'s Inbox.
+
+ Params:
+ `user`: Django User instance
+
+ Return:
+ An iterable containing `Message` instances
+ """
+ raise NotImplementedError()
+
+ def inbox_purge(self, user):
+ """
+ Delete all the messages in `user`'s Inbox.
+
+ Params:
+ user: Django User instance
+
+ Return:
+ None
+ """
+ raise NotImplementedError()
+
+ def inbox_store(self, users, msg_instance):
+ """
+ Store a `Message` instance in the inbox for a list
+ of users.
+
+ Params:
+ users: a list or iterable containing Django User instances
+ msg_instance: Message instance to persist in inbox
+
+ Return:
+ None
+
+ Raise:
+ MessageTypeNotSupported if `msg_instance` cannot be managed by current backend
+ """
+ raise NotImplementedError()
+
+ def inbox_delete(self, user, msg_id):
+ """
+ Remove a `Message` instance from `user`'s inbox.
+
+ Params:
+ user: Django User instance
+ msg_id: Message identifier
+
+ Return:
+ None
+
+ Raise:
+ MessageDoesNotExist if msg_id was not found
+ """
+ raise NotImplementedError()
+
+ def inbox_get(self, user, msg_id):
+ """
+ Retrieve a `Message` instance from `user`'s inbox.
+
+ Params:
+ user: Django User instance
+ msg_id: Message identifier
+
+ Return:
+ A `Message` instance
+
+ Raise:
+ MessageDoesNotExist if msg_id was not found
+ """
+
+ def archive_store(self, users, msg_instance):
+ """
+ Store a `Message` instance in the archive for a list
+ of users.
+
+ Params:
+ users: a list or iterable containing Django User instances
+ msg_instance: Message instance to persist in archive
+
+ Return:
+ None
+
+ Raise:
+ MessageTypeNotSupported if `msg_instance` cannot be managed by current backend
+ """
+ raise NotImplementedError()
+
+ def archive_list(self, user):
+ """
+ Retrieve all the messages in `user`'s archive.
+
+ Params:
+ user: Django User instance
+
+ Return:
+ An iterable containing `Message` instances
+ """
+ raise NotImplementedError()
+
+ def can_handle(self, msg_instance):
+ """
+ Determine if this backend can handle messages
+ of the same type of `msg_instance`.
+
+ Params:
+ `msg_instance`: `Message` instance
+
+ Return:
+ True if type is correct, False otherwise
+ """
+ raise NotImplementedError()
+
+ def expired_messages_cleanup(self):
+ """
+ Remove messages that have been expired.
+
+ Params:
+ None
+
+ Return:
+ None
+ """
+ raise NotImplementedError()
+
+ def _flush(self):
+ """
+ Clear all backend data.
+ Warning: heavily destructive! Here for convenience, not used by the API anyway.
+
+ Params:
+ None
+
+ Return:
+ None
+ """
+ raise NotImplementedError()
diff --git a/vendor/stored_messages/backends/default/__init__.py b/vendor/stored_messages/backends/default/__init__.py
new file mode 100644
index 00000000..e4a0cd42
--- /dev/null
+++ b/vendor/stored_messages/backends/default/__init__.py
@@ -0,0 +1,2 @@
+# flake8: noqa
+from .backend import DefaultBackend
diff --git a/vendor/stored_messages/backends/default/backend.py b/vendor/stored_messages/backends/default/backend.py
new file mode 100644
index 00000000..822b502a
--- /dev/null
+++ b/vendor/stored_messages/backends/default/backend.py
@@ -0,0 +1,81 @@
+from django.utils import timezone
+
+from ..base import StoredMessagesBackend
+from ..exceptions import MessageTypeNotSupported, MessageDoesNotExist
+from .. import signals
+from ...models import Inbox, Message, MessageArchive
+from ...settings import stored_messages_settings
+
+
+class DefaultBackend(StoredMessagesBackend):
+ """
+
+ """
+ def inbox_list(self, user):
+ if user.is_anonymous():
+ return []
+ inbox = Inbox.objects.filter(user=user).select_related("message")
+ return [m.message for m in inbox]
+
+ def inbox_purge(self, user):
+ if user.is_authenticated():
+ Inbox.objects.filter(user=user).delete()
+ signals.inbox_purged.send(sender=self.__class__, user=user)
+
+ def inbox_store(self, users, msg_instance):
+ if not self.can_handle(msg_instance):
+ raise MessageTypeNotSupported()
+
+ for user in users:
+ Inbox.objects.get_or_create(user=user, message=msg_instance)
+ signals.inbox_stored.send(sender=self.__class__, user=user, message=msg_instance)
+
+ def inbox_delete(self, user, msg_id):
+ try:
+ inbox_m = Inbox.objects.filter(user=user, message=msg_id).get()
+ inbox_m.delete()
+ signals.inbox_deleted.send(sender=self.__class__, user=user, message_id=msg_id)
+ except Inbox.DoesNotExist:
+ raise MessageDoesNotExist("Message with id %s does not exist" % msg_id)
+
+ def inbox_get(self, user, msg_id):
+ try:
+ return Inbox.objects.get(pk=msg_id).message
+ except Inbox.DoesNotExist:
+ raise MessageDoesNotExist("Message with id %s does not exist" % msg_id)
+
+ def create_message(self, level, msg_text, extra_tags='', date=None, url=None):
+ kwargs = {
+ 'message': msg_text,
+ 'level': level,
+ 'tags': extra_tags,
+ 'url': url,
+ }
+ if date:
+ kwargs['date'] = date
+ m_instance = Message.objects.create(**kwargs)
+ return m_instance
+
+ def archive_store(self, users, msg_instance):
+ if not self.can_handle(msg_instance):
+ raise MessageTypeNotSupported()
+
+ for user in users:
+ MessageArchive.objects.create(user=user, message=msg_instance)
+ signals.archive_stored.send(sender=self.__class__, user=user, message=msg_instance)
+
+ def archive_list(self, user):
+ return list(MessageArchive.objects.filter(user=user))
+
+ def can_handle(self, message):
+ return isinstance(message, Message)
+
+ def expired_messages_cleanup(self):
+ expiration_date = timezone.now() + timezone.timedelta(
+ days=-stored_messages_settings.MESSAGE_EXPIRE_DAYS)
+ Message.objects.filter(date__lte=expiration_date).delete()
+
+ def _flush(self):
+ Inbox.objects.all().delete()
+ MessageArchive.objects.all().delete()
+ Message.objects.all().delete()
diff --git a/vendor/stored_messages/backends/exceptions.py b/vendor/stored_messages/backends/exceptions.py
new file mode 100644
index 00000000..12046f4d
--- /dev/null
+++ b/vendor/stored_messages/backends/exceptions.py
@@ -0,0 +1,8 @@
+
+
+class MessageTypeNotSupported(Exception):
+ pass
+
+
+class MessageDoesNotExist(Exception):
+ pass
diff --git a/vendor/stored_messages/backends/redis/__init__.py b/vendor/stored_messages/backends/redis/__init__.py
new file mode 100644
index 00000000..cdfb2bbf
--- /dev/null
+++ b/vendor/stored_messages/backends/redis/__init__.py
@@ -0,0 +1,2 @@
+# flake8: noqa
+from .backend import RedisBackend, Message
diff --git a/vendor/stored_messages/backends/redis/backend.py b/vendor/stored_messages/backends/redis/backend.py
new file mode 100644
index 00000000..61c5fcf5
--- /dev/null
+++ b/vendor/stored_messages/backends/redis/backend.py
@@ -0,0 +1,143 @@
+from __future__ import unicode_literals
+
+from django.utils import timezone
+from django.utils.encoding import force_text
+from django.core.serializers.json import DjangoJSONEncoder
+
+import json
+from collections import namedtuple
+import hashlib
+
+from ..exceptions import MessageTypeNotSupported, MessageDoesNotExist
+from ..base import StoredMessagesBackend
+from .. import signals
+from ...settings import stored_messages_settings
+
+try:
+ # Let django project bootstrap anyway when not using this backend
+ import redis
+except ImportError:
+ pass
+
+
+Message = namedtuple('Message', ['id', 'message', 'level', 'tags', 'date', 'url'])
+
+
+class RedisBackend(StoredMessagesBackend):
+ """
+
+ """
+ def __init__(self):
+ self.client = redis.StrictRedis.from_url(stored_messages_settings.REDIS_URL)
+
+ def _flush(self):
+ self.client.flushdb()
+
+ def _toJSON(self, msg_instance):
+ """
+ Dump a Message instance into a JSON string
+ """
+ return json.dumps(msg_instance._asdict(), cls=DjangoJSONEncoder)
+
+ def _fromJSON(self, json_msg):
+ """
+ Return a Message instance built from data contained in a JSON string
+ """
+ return Message(**json.loads(force_text(json_msg)))
+
+ def _list_key(self, key):
+ """
+ boilerplate
+ """
+ ret = []
+ for msg_json in self.client.lrange(key, 0, -1):
+ ret.append(self._fromJSON(msg_json))
+ return ret
+
+ def _list(self, key_tpl, user):
+ return self._list_key(key_tpl % user.pk)
+
+ def create_message(self, level, msg_text, extra_tags='', date=None, url=None):
+ """
+ Message instances are namedtuples of type `Message`.
+ The date field is already serialized in datetime.isoformat ECMA-262 format
+ """
+ if not date:
+ now = timezone.now()
+ else:
+ now = date
+ r = now.isoformat()
+ if now.microsecond:
+ r = r[:23] + r[26:]
+ if r.endswith('+00:00'):
+ r = r[:-6] + 'Z'
+
+ fingerprint = r + msg_text
+
+ msg_id = hashlib.sha256(fingerprint.encode('ascii', 'ignore')).hexdigest()
+ return Message(id=msg_id, message=msg_text, level=level, tags=extra_tags, date=r, url=url)
+
+ def inbox_list(self, user):
+ if user.is_anonymous():
+ return []
+ return self._list('user:%d:notifications', user)
+
+ def inbox_purge(self, user):
+ if user.is_authenticated():
+ self.client.delete('user:%d:notifications' % user.pk)
+ self.client.delete('user:%d:notificationsidx' % user.pk)
+ signals.inbox_purged.send(sender=self.__class__, user=user)
+
+ def inbox_store(self, users, msg_instance):
+ if not self.can_handle(msg_instance):
+ raise MessageTypeNotSupported()
+
+ for user in users:
+ if self.client.sismember('user:%d:notificationsidx' % user.pk, msg_instance.id):
+ return # a duplicate, NOOP
+ else:
+ self.client.sadd('user:%d:notificationsidx' % user.pk, msg_instance.id)
+
+ self.client.rpush('user:%d:notifications' % user.pk, self._toJSON(msg_instance))
+ signals.inbox_stored.send(sender=self.__class__, user=user, message=msg_instance)
+
+ def inbox_delete(self, user, msg_id):
+ for m in self._list('user:%d:notifications', user):
+ if m.id == msg_id:
+ msg = self.client.lrem('user:%d:notifications' % user.pk, 0, json.dumps(m._asdict()))
+ signals.inbox_deleted.send(sender=self.__class__, user=user, message_id=msg_id)
+ return msg
+ raise MessageDoesNotExist("Message with id %s does not exist" % msg_id)
+
+ def inbox_get(self, user, msg_id):
+ for m in self._list('user:%d:notifications', user):
+ if m.id == msg_id:
+ return m
+ raise MessageDoesNotExist("Message with id %s does not exist" % msg_id)
+
+ def archive_store(self, users, msg_instance):
+ if not self.can_handle(msg_instance):
+ raise MessageTypeNotSupported()
+
+ for user in users:
+ self.client.rpush('user:%d:archive' % user.pk, self._toJSON(msg_instance))
+ signals.archive_stored.send(sender=self.__class__, user=user, message=msg_instance)
+
+ def archive_list(self, user):
+ return self._list('user:%d:archive', user)
+
+ def can_handle(self, msg_instance):
+ return isinstance(msg_instance, Message)
+
+ def expired_messages_cleanup(self):
+ expiration_date = timezone.now() + timezone.timedelta(
+ days=-stored_messages_settings.MESSAGE_EXPIRE_DAYS)
+ keys = self.client.keys('user:*:notifications')
+ for k in keys:
+ _, user, _ = k.split(':')
+ for m in self._list_key(k):
+ if m.date <= expiration_date:
+ msg = json.dumps(m._asdict())
+ self.client.lrem(k, 0, msg)
+ self.client.srem('user:%s:notificationsidx' % user, m.id)
+ self.client.rpop('user:%d:archive' % user, msg)
diff --git a/vendor/stored_messages/backends/signals.py b/vendor/stored_messages/backends/signals.py
new file mode 100644
index 00000000..8d394260
--- /dev/null
+++ b/vendor/stored_messages/backends/signals.py
@@ -0,0 +1,7 @@
+from django.dispatch import Signal
+
+inbox_stored = Signal(providing_args=["user", "message"])
+inbox_deleted = Signal(providing_args=["user", "message_id"])
+inbox_purged = Signal(providing_args=["user"])
+
+archive_stored = Signal(providing_args=["user", "message"])
diff --git a/vendor/stored_messages/compat.py b/vendor/stored_messages/compat.py
new file mode 100644
index 00000000..7bc3d2f1
--- /dev/null
+++ b/vendor/stored_messages/compat.py
@@ -0,0 +1,6 @@
+"""
+The `compat` module provides support for backwards compatibility with older
+versions of Django and Python. Please, add in this module the compatibility code.
+"""
+
+from __future__ import unicode_literals
diff --git a/vendor/stored_messages/constants.py b/vendor/stored_messages/constants.py
new file mode 100644
index 00000000..9499c2eb
--- /dev/null
+++ b/vendor/stored_messages/constants.py
@@ -0,0 +1,16 @@
+from django.contrib.messages.constants import *
+
+
+STORED_DEBUG = DEBUG + 1
+STORED_INFO = INFO + 1
+STORED_SUCCESS = SUCCESS + 1
+STORED_WARNING = WARNING + 1
+STORED_ERROR = ERROR + 1
+
+DEFAULT_TAGS.update({
+ STORED_DEBUG: 'persisted debug',
+ STORED_INFO: 'persisted info',
+ STORED_SUCCESS: 'persisted success',
+ STORED_WARNING: 'persisted warning',
+ STORED_ERROR: 'persisted error',
+})
diff --git a/vendor/stored_messages/migrations/0001_initial.py b/vendor/stored_messages/migrations/0001_initial.py
new file mode 100644
index 00000000..ef483a74
--- /dev/null
+++ b/vendor/stored_messages/migrations/0001_initial.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+import django.utils.timezone
+
+from django.db import models, migrations
+from django.conf import settings
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Inbox',
+ fields=[
+ ('id', models.AutoField(auto_created=True, serialize=False, primary_key=True, verbose_name='ID')),
+ ],
+ options={
+ 'verbose_name_plural': 'inboxes',
+ },
+ ),
+ migrations.CreateModel(
+ name='Message',
+ fields=[
+ ('id', models.AutoField(auto_created=True, serialize=False, primary_key=True, verbose_name='ID')),
+ ('message', models.TextField()),
+ ('level', models.IntegerField()),
+ ('tags', models.TextField()),
+ ('date', models.DateTimeField(default=django.utils.timezone.now)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='MessageArchive',
+ fields=[
+ ('id', models.AutoField(auto_created=True, serialize=False, primary_key=True, verbose_name='ID')),
+ ('message', models.ForeignKey(to='stored_messages.Message')),
+ ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ migrations.AddField(
+ model_name='inbox',
+ name='message',
+ field=models.ForeignKey(to='stored_messages.Message'),
+ ),
+ migrations.AddField(
+ model_name='inbox',
+ name='user',
+ field=models.ForeignKey(to=settings.AUTH_USER_MODEL),
+ ),
+ ]
diff --git a/vendor/stored_messages/migrations/0002_message_url.py b/vendor/stored_messages/migrations/0002_message_url.py
new file mode 100644
index 00000000..807368c8
--- /dev/null
+++ b/vendor/stored_messages/migrations/0002_message_url.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('stored_messages', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='message',
+ name='url',
+ field=models.URLField(blank=True, null=True),
+ ),
+ ]
diff --git a/vendor/stored_messages/migrations/0003_alter_inbox_id_alter_message_id_and_more.py b/vendor/stored_messages/migrations/0003_alter_inbox_id_alter_message_id_and_more.py
new file mode 100644
index 00000000..834987ca
--- /dev/null
+++ b/vendor/stored_messages/migrations/0003_alter_inbox_id_alter_message_id_and_more.py
@@ -0,0 +1,28 @@
+# Generated by Django 6.1.1 on 2026-10-01 14:16
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('stored_messages', '0002_message_url'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='inbox',
+ name='id',
+ field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+ ),
+ migrations.AlterField(
+ model_name='message',
+ name='id',
+ field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+ ),
+ migrations.AlterField(
+ model_name='messagearchive',
+ name='id',
+ field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+ ),
+ ]
diff --git a/vendor/stored_messages/migrations/__init__.py b/vendor/stored_messages/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/vendor/stored_messages/models.py b/vendor/stored_messages/models.py
new file mode 100644
index 00000000..ff40cdbd
--- /dev/null
+++ b/vendor/stored_messages/models.py
@@ -0,0 +1,66 @@
+from __future__ import unicode_literals
+
+from django.db import models
+from django.conf import settings
+from django.utils import timezone
+try:
+ from django.utils.encoding import python_2_unicode_compatible
+except ImportError:
+ def python_2_unicode_compatible(cls):
+ return cls
+from django.utils.translation import pgettext_lazy as _
+
+from .settings import stored_messages_settings
+
+
+@python_2_unicode_compatible
+class Message(models.Model):
+ """
+ This model represents a message on the database. Fields are the same as in
+ `contrib.messages`
+ """
+ message = models.TextField()
+ level = models.IntegerField()
+ tags = models.TextField()
+ url = models.URLField(null=True, blank=True)
+ date = models.DateTimeField(default=timezone.now)
+
+ def __str__(self):
+ return self.message
+
+
+@python_2_unicode_compatible
+class MessageArchive(models.Model):
+ """
+ This model holds all the messages users received. Corresponding
+ database table will grow indefinitely depending on messages traffic.
+ """
+ user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
+ message = models.ForeignKey(Message, on_delete=models.CASCADE)
+
+ def __str__(self):
+ return "[%s] %s" % (self.user, self.message)
+
+
+@python_2_unicode_compatible
+class Inbox(models.Model):
+ """
+ Inbox messages are stored in this model until users read them. Once read,
+ inbox messages are deleted. Inbox messages have an expire time, after
+ that they could be removed by a proper django command. We do not expect
+ database table corresponding to this model to grow much.
+ """
+ user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
+ message = models.ForeignKey(Message, models.CASCADE)
+
+ class Meta:
+ verbose_name_plural = _('inboxes')
+
+ def expired(self):
+ expiration_date = self.message.date + timezone.timedelta(
+ days=stored_messages_settings.INBOX_EXPIRE_DAYS)
+ return expiration_date <= timezone.now()
+ expired.boolean = True # show a nifty icon in the admin
+
+ def __str__(self):
+ return "[%s] %s" % (self.user, self.message)
diff --git a/vendor/stored_messages/serializers.py b/vendor/stored_messages/serializers.py
new file mode 100644
index 00000000..702a9f94
--- /dev/null
+++ b/vendor/stored_messages/serializers.py
@@ -0,0 +1,10 @@
+from rest_framework import serializers
+
+
+class InboxSerializer(serializers.Serializer):
+ id = serializers.CharField()
+ message = serializers.CharField()
+ level = serializers.IntegerField()
+ tags = serializers.CharField()
+ date = serializers.DateTimeField(format=None)
+ url = serializers.URLField(required=False)
diff --git a/vendor/stored_messages/settings.py b/vendor/stored_messages/settings.py
new file mode 100644
index 00000000..05722580
--- /dev/null
+++ b/vendor/stored_messages/settings.py
@@ -0,0 +1,130 @@
+"""
+This module is largely inspired by django-rest-framework settings.
+
+Settings for the Stored Messages framework are all namespaced in the
+STORED_MESSAGES setting.
+For example your project's `settings.py` file might look like this::
+
+ STORED_MESSAGES = {
+ 'INBOX_EXPIRE_DAYS': 30,
+ }
+
+This module provides the `stored_messages_settings` object, that is used to
+access Stored Messages framework settings, checking for user settings first,
+then falling back to the defaults.
+"""
+from __future__ import unicode_literals
+
+import importlib
+
+from django.conf import settings
+import six
+
+from .constants import *
+
+
+USER_SETTINGS = getattr(settings, 'STORED_MESSAGES', None)
+MESSAGE_TAGS = getattr(settings, 'MESSAGE_TAGS', {})
+
+DEFAULTS = {
+ 'STORE_LEVELS': (
+ STORED_DEBUG,
+ STORED_INFO,
+ STORED_SUCCESS,
+ STORED_WARNING,
+ STORED_ERROR,
+ ),
+ 'MESSAGE_TAGS': MESSAGE_TAGS.update({
+ STORED_DEBUG: 'stored debug',
+ STORED_INFO: 'stored info',
+ STORED_SUCCESS: 'stored success',
+ STORED_WARNING: 'stored warning',
+ STORED_ERROR: 'stored error',
+ }),
+ 'INBOX_EXPIRE_DAYS': 30,
+ 'MESSAGE_EXPIRE_DAYS': 120,
+ 'STORAGE_BACKEND': 'stored_messages.backends.DefaultBackend',
+ # Only for Redis backend
+ 'REDIS_URL': 'redis://username:password@localhost:6379/0',
+}
+
+# List of settings that cannot be empty
+MANDATORY = (
+
+)
+
+# List of settings that may be in string import notation.
+IMPORT_STRINGS = (
+ 'STORAGE_BACKEND',
+)
+
+
+def perform_import(val, setting_name):
+ """
+ If the given setting is a string import notation,
+ then perform the necessary import or imports.
+ """
+ if isinstance(val, six.string_types):
+ return import_from_string(val, setting_name)
+ elif isinstance(val, (list, tuple)):
+ return [import_from_string(item, setting_name) for item in val]
+ return val
+
+
+def import_from_string(val, setting_name):
+ """
+ Attempt to import a class from a string representation.
+ """
+ try:
+ parts = val.split('.')
+ module_path, class_name = '.'.join(parts[:-1]), parts[-1]
+ module = importlib.import_module(module_path)
+ return getattr(module, class_name)
+ except ImportError as e:
+ msg = "Could not import '%s' for setting '%s'. %s: %s." % (val, setting_name,
+ e.__class__.__name__, e)
+ raise ImportError(msg)
+
+
+class StoredMessagesSettings(object):
+ """
+ A settings object, that allows settings to be accessed as properties.
+
+ Any setting with string import paths will be automatically resolved
+ and return the class, rather than the string literal.
+ """
+
+ def __init__(self, user_settings=None, defaults=None, import_strings=None, mandatory=None):
+ self.user_settings = user_settings or {}
+ self.defaults = defaults or {}
+ self.import_strings = import_strings or ()
+ self.mandatory = mandatory or ()
+
+ def __getattr__(self, attr):
+ if attr not in self.defaults.keys():
+ raise AttributeError("Invalid Stored Messages setting: '%s'" % attr)
+
+ try:
+ # Check if present in user settings
+ val = self.user_settings[attr]
+ except KeyError:
+ # Fall back to defaults
+ val = self.defaults[attr]
+
+ # Coerce import strings into classes
+ if val and attr in self.import_strings:
+ val = perform_import(val, attr)
+
+ self.validate_setting(attr, val)
+
+ # Cache the result
+ setattr(self, attr, val)
+ return val
+
+ def validate_setting(self, attr, val):
+ if not val and attr in self.mandatory:
+ raise AttributeError("Stored Messages setting: '%s' is mandatory" % attr)
+
+
+stored_messages_settings = StoredMessagesSettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS,
+ MANDATORY)
diff --git a/vendor/stored_messages/static/js/stored_messages.js b/vendor/stored_messages/static/js/stored_messages.js
new file mode 100644
index 00000000..e69de29b
diff --git a/vendor/stored_messages/storage.py b/vendor/stored_messages/storage.py
new file mode 100644
index 00000000..c23bc3b9
--- /dev/null
+++ b/vendor/stored_messages/storage.py
@@ -0,0 +1,103 @@
+from django.contrib.messages.storage.fallback import FallbackStorage
+
+from .settings import stored_messages_settings
+from .backends.exceptions import MessageTypeNotSupported
+
+
+class StorageMixin(object):
+ """
+ This mixin implements a message storage compliant with
+ `django.contrib.messages` which stores messages on the database when
+ needed, delegating to the sibling class otherwise. Messages are stored
+ only when the user is authenticated and the message level is configured
+ for being persisted. The mixin can be derived together with another class
+ implementing a storage, tipically one of the three provided by Django out
+ of the box.
+ """
+ def __init__(self, request, *args, **kwargs):
+ self.user = request.user
+ BackendClass = stored_messages_settings.STORAGE_BACKEND
+ self.backend = BackendClass()
+ super(StorageMixin, self).__init__(request, *args, **kwargs)
+
+ def _get(self, *args, **kwargs):
+ """
+ Retrieve unread messages for current user, both from the inbox and
+ from other storages
+ """
+ messages, all_retrieved = super(StorageMixin, self)._get(*args, **kwargs)
+ if self.user.is_authenticated():
+ inbox_messages = self.backend.inbox_list(self.user)
+ else:
+ inbox_messages = []
+
+ return messages + inbox_messages, all_retrieved
+
+ def add(self, level, message, extra_tags=''):
+ """
+ If the message level was configured for being stored and request.user
+ is not anonymous, save it to the database. Otherwise, let some other
+ class handle the message.
+
+ Notice: controls like checking the message is not empty and the level
+ is above the filter need to be performed here, but it could happen
+ they'll be performed again later if the message does not need to be
+ stored.
+ """
+ if not message:
+ return
+ # Check that the message level is not less than the recording level.
+ level = int(level)
+ if level < self.level:
+ return
+ # Check if the message doesn't have a level that needs to be persisted
+ if level not in stored_messages_settings.STORE_LEVELS or self.user.is_anonymous():
+ return super(StorageMixin, self).add(level, message, extra_tags)
+
+ self.added_new = True
+ m = self.backend.create_message(level, message, extra_tags)
+ self.backend.archive_store([self.user], m)
+ self._queued_messages.append(m)
+
+ def _store(self, messages, response, *args, **kwargs):
+ """
+ persistent messages are already in the database inside the 'archive',
+ so we can say they're already "stored".
+ Here we put them in the inbox, or remove from the inbox in case the
+ messages were iterated.
+
+ messages contains only new msgs if self.used==True
+ else contains both new and unread messages
+ """
+ contrib_messages = []
+ if self.user.is_authenticated():
+ if not messages:
+ # erase inbox
+ self.backend.inbox_purge(self.user)
+ else:
+ for m in messages:
+ try:
+ self.backend.inbox_store([self.user], m)
+ except MessageTypeNotSupported:
+ contrib_messages.append(m)
+
+ super(StorageMixin, self)._store(contrib_messages, response, *args, **kwargs)
+
+ def _prepare_messages(self, messages):
+ """
+ Like the base class method, prepares a list of messages for storage
+ but avoid to do this for `models.Message` instances.
+ """
+ for message in messages:
+ if not self.backend.can_handle(message):
+ message._prepare()
+
+
+class PersistentStorage(StorageMixin, FallbackStorage):
+ """
+ This class is provided for convenience: it implements `StorageMixin` which
+ persists messages having a type configured to be persisted and passes
+ other messages to `FallbackStorage`, one of the defaults provided by
+ Django which stores messages inside cookies or sessions.
+ """
+ pass
diff --git a/vendor/stored_messages/templates/stored_messages/base.html b/vendor/stored_messages/templates/stored_messages/base.html
new file mode 100644
index 00000000..09d26f38
--- /dev/null
+++ b/vendor/stored_messages/templates/stored_messages/base.html
@@ -0,0 +1,21 @@
+
+{% comment %}
+As the developer of this package, don't place anything here if you can help it
+since this allows developers to have interoperability between your template
+structure and their own.
+
+Example: Developer melding the 2SoD pattern to fit inside with another pattern::
+
+ {% extends "base.html" %}
+ {% load static %}
+
+
+ {% block extra_js %}
+
+
+ {% block javascript %}
+
+ {% endblock javascript %}
+
+ {% endblock extra_js %}
+{% endcomment %}
diff --git a/vendor/stored_messages/templates/stored_messages/stored_messages_list.html b/vendor/stored_messages/templates/stored_messages/stored_messages_list.html
new file mode 100644
index 00000000..ec695c8f
--- /dev/null
+++ b/vendor/stored_messages/templates/stored_messages/stored_messages_list.html
@@ -0,0 +1,7 @@
+{% if messages %}
+
+ {% for message in messages %}
+ {{ message }}
+ {% endfor %}
+
+{% endif %}
\ No newline at end of file
diff --git a/vendor/stored_messages/templatetags/__init__.py b/vendor/stored_messages/templatetags/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/vendor/stored_messages/templatetags/stored_messages_tags.py b/vendor/stored_messages/templatetags/stored_messages_tags.py
new file mode 100644
index 00000000..49271694
--- /dev/null
+++ b/vendor/stored_messages/templatetags/stored_messages_tags.py
@@ -0,0 +1,49 @@
+from __future__ import unicode_literals
+
+from django import template
+
+from ..models import Inbox, MessageArchive
+
+
+register = template.Library()
+
+
+@register.inclusion_tag("stored_messages/stored_messages_list.html", takes_context=True)
+def stored_messages_list(context, num_elements=10):
+ """
+ Renders a list of unread stored messages for the current user
+ """
+ if "user" in context:
+ user = context["user"]
+ if user.is_authenticated():
+ qs = Inbox.objects.select_related("message").filter(user=user)
+ return {
+ "messages": qs[:num_elements],
+ "count": qs.count(),
+ }
+
+
+@register.simple_tag(takes_context=True)
+def stored_messages_count(context):
+ """
+ Renders a list of unread stored messages for the current user
+ """
+ if "user" in context:
+ user = context["user"]
+ if user.is_authenticated():
+ return Inbox.objects.select_related("message").filter(user=user).count()
+
+
+@register.inclusion_tag("stored_messages/stored_messages_list.html", takes_context=True)
+def stored_messages_archive(context, num_elements=10):
+ """
+ Renders a list of archived messages for the current user
+ """
+ if "user" in context:
+ user = context["user"]
+ if user.is_authenticated():
+ qs = MessageArchive.objects.select_related("message").filter(user=user)
+ return {
+ "messages": qs[:num_elements],
+ "count": qs.count(),
+ }
diff --git a/vendor/stored_messages/tests/__init__.py b/vendor/stored_messages/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/vendor/stored_messages/tests/base.py b/vendor/stored_messages/tests/base.py
new file mode 100644
index 00000000..5c276990
--- /dev/null
+++ b/vendor/stored_messages/tests/base.py
@@ -0,0 +1,40 @@
+from __future__ import unicode_literals
+
+from django.contrib.auth import get_user_model
+from django.test import TestCase, RequestFactory
+from django.utils.six.moves import reload_module
+
+import mock
+
+from stored_messages import storage
+from stored_messages import settings
+
+
+class BaseTest(TestCase):
+ def setUp(self):
+ # settings and storage modules should be reloaded
+ reload_module(settings)
+ reload_module(storage)
+
+ self.factory = RequestFactory()
+ self.user = get_user_model().objects.create_user("test_user", "t@user.com", "123456")
+ self.request = RequestFactory().get('/')
+ self.request.session = mock.MagicMock()
+ self.request.user = self.user
+
+ def tearDown(self):
+ self.user.delete()
+
+
+class BackendBaseTest(BaseTest):
+ """
+ Tests that need to access a Backend.
+ Given the dynamic nature of Stored Messages settings, retrieving the backend class when we
+ need to ovveride settings is a little bit tricky
+ """
+ def setUp(self):
+ super(BackendBaseTest, self).setUp()
+ self.backend = settings.stored_messages_settings.STORAGE_BACKEND()
+
+ def tearDown(self):
+ self.backend._flush()
diff --git a/vendor/stored_messages/tests/settings.py b/vendor/stored_messages/tests/settings.py
new file mode 100644
index 00000000..018ec8ba
--- /dev/null
+++ b/vendor/stored_messages/tests/settings.py
@@ -0,0 +1,69 @@
+# flake8: noqa
+import django
+
+
+INSTALLED_APPS = [
+ "django.contrib.auth",
+ "django.contrib.contenttypes",
+ "django.contrib.sites",
+ "django.contrib.messages",
+ "django.contrib.sessions",
+ "stored_messages",
+]
+
+try:
+ import rest_framework
+ INSTALLED_APPS.append("rest_framework")
+except ImportError:
+ pass
+
+DEBUG = True
+USE_TZ = True
+
+DATABASES = {
+ "default": {
+ "ENGINE": "django.db.backends.sqlite3",
+ "NAME": "stored_messages.sqlite",
+ }
+}
+
+ROOT_URLCONF = "stored_messages.tests.urls"
+
+# Django 1.8 has a new, minimal default set for MIDDLEWARE_CLASSES so be explicit
+MIDDLEWARE_CLASSES = (
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+ 'django.middleware.security.SecurityMiddleware',
+)
+
+# Django 1.10 requires the TEMPLATES settings. Deprecated since Django 1.8
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.debug',
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ ],
+ },
+ },
+]
+
+SITE_ID = 1
+
+MESSAGE_STORAGE = 'stored_messages.storage.PersistentStorage'
+STORED_MESSAGES = {
+ 'REDIS_URL': 'redis://localhost:6379/0',
+}
+
+MOCK_REDIS_SERVER = False
+SECRET_KEY = 'FUUUFU'
diff --git a/vendor/stored_messages/tests/test_api.py b/vendor/stored_messages/tests/test_api.py
new file mode 100644
index 00000000..e4db3ff6
--- /dev/null
+++ b/vendor/stored_messages/tests/test_api.py
@@ -0,0 +1,93 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+from .base import BaseTest
+
+from django.contrib.auth import get_user_model
+from django.utils import timezone
+
+from stored_messages import mark_read, add_message_for, broadcast_message, mark_all_read
+from stored_messages.models import Inbox, MessageArchive
+from stored_messages.backends.exceptions import MessageDoesNotExist
+import stored_messages
+
+
+class TestApi(BaseTest):
+ def test_mark_as_read(self):
+ self.client.login(username='test_user', password='123456')
+ self.client.get('/create')
+ inbox = Inbox.objects.filter(user=self.user)
+ self.assertEqual(len(inbox), 1)
+ mark_read(self.user, inbox.get().message)
+ self.assertEqual(Inbox.objects.filter(user=self.user).count(), 0)
+
+ def test_mark_as_read_idempotent(self):
+ self.client.login(username='test_user', password='123456')
+ self.client.get('/create')
+ msg_archive = MessageArchive.objects.filter(user=self.user).get()
+ mark_read(self.user, msg_archive.message)
+ self.assertRaises(MessageDoesNotExist, mark_read, self.user, msg_archive.message)
+
+ def test_add_message_for(self):
+ now = timezone.now() + timezone.timedelta(days=-1)
+ url = 'http://example.com/error'
+
+ user2 = get_user_model().objects.create_user("another_user", "u@user.com", "123456")
+ add_message_for([user2, self.user], stored_messages.STORED_ERROR, 'Multiple errors', 'extra', now, url)
+ self.assertEqual(Inbox.objects.count(), 2)
+ self.assertEqual(MessageArchive.objects.count(), 2)
+
+ self.assertEqual(Inbox.objects.get(user=user2.id).message.tags, 'extra')
+ self.assertEqual(Inbox.objects.get(user=self.user).message.tags, 'extra')
+
+ self.assertEqual(Inbox.objects.get(user=user2.id).message.date, now)
+ self.assertEqual(Inbox.objects.get(user=self.user).message.date, now)
+
+ self.assertEqual(Inbox.objects.get(user=user2.id).message.url, url)
+ self.assertEqual(Inbox.objects.get(user=self.user).message.url, url)
+
+ self.assertEqual(Inbox.objects.get(user=user2.id).message.message, "Multiple errors")
+ self.assertEqual(Inbox.objects.get(user=self.user).message.message, "Multiple errors")
+
+ self.assertEqual(MessageArchive.objects.get(user=user2.id).message.message,
+ "Multiple errors")
+ self.assertEqual(MessageArchive.objects.get(user=self.user).message.message,
+ "Multiple errors")
+
+ def test_broadcast_message(self):
+ user1 = get_user_model().objects.create_user("user1", "u1@user.com", "123456")
+ user2 = get_user_model().objects.create_user("user2", "u2@user.com", "123456")
+ user3 = get_user_model().objects.create_user("user3", "u3@user.com", "123456")
+
+ now = timezone.now() + timezone.timedelta(days=-1)
+ url = 'http://example.com/error'
+ broadcast_message(stored_messages.STORED_INFO, 'broadcast test message', 'extra', now, url)
+ self.assertEqual(Inbox.objects.get(user=user1.id).message.message, "broadcast test message")
+ self.assertEqual(Inbox.objects.get(user=user2.id).message.message, "broadcast test message")
+ self.assertEqual(Inbox.objects.get(user=user3.id).message.message, "broadcast test message")
+
+ self.assertEqual(Inbox.objects.get(user=user1.id).message.tags, 'extra')
+ self.assertEqual(Inbox.objects.get(user=user2.id).message.tags, 'extra')
+ self.assertEqual(Inbox.objects.get(user=user3.id).message.tags, 'extra')
+
+ self.assertEqual(Inbox.objects.get(user=user1.id).message.date, now)
+ self.assertEqual(Inbox.objects.get(user=user2.id).message.date, now)
+ self.assertEqual(Inbox.objects.get(user=user3.id).message.date, now)
+
+ self.assertEqual(Inbox.objects.get(user=user1.id).message.url, url)
+ self.assertEqual(Inbox.objects.get(user=user2.id).message.url, url)
+ self.assertEqual(Inbox.objects.get(user=user3.id).message.url, url)
+
+ self.assertEqual(MessageArchive.objects.get(user=user1.id).message.message,
+ "broadcast test message")
+ self.assertEqual(MessageArchive.objects.get(user=user2.id).message.message,
+ "broadcast test message")
+ self.assertEqual(MessageArchive.objects.get(user=user3.id).message.message,
+ "broadcast test message")
+
+ def test_mark_all_read(self):
+ for i in range(20):
+ stored_messages.add_message_for([self.user], stored_messages.INFO, "unicode message ❤")
+ inbox = Inbox.objects.filter(user=self.user)
+ self.assertEqual(len(inbox), 20)
+ mark_all_read(self.user)
+ self.assertEqual(Inbox.objects.filter(user=self.user).count(), 0)
diff --git a/vendor/stored_messages/tests/test_default_backend.py b/vendor/stored_messages/tests/test_default_backend.py
new file mode 100644
index 00000000..1fe2c134
--- /dev/null
+++ b/vendor/stored_messages/tests/test_default_backend.py
@@ -0,0 +1,134 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from .base import BaseTest
+
+from django.contrib.auth.models import AnonymousUser
+from django.utils import timezone
+
+from stored_messages.backends.exceptions import MessageTypeNotSupported, MessageDoesNotExist
+from stored_messages.backends.default import DefaultBackend
+from stored_messages.models import Message, Inbox, MessageArchive
+from stored_messages import STORED_ERROR
+from stored_messages.backends import signals
+
+
+class TestDefaultBackend(BaseTest):
+ def setUp(self):
+ super(TestDefaultBackend, self).setUp()
+ self.backend = DefaultBackend()
+ self.message = self.backend.create_message(STORED_ERROR, 'A message for you ♡')
+ self.anon = AnonymousUser()
+ self.signals = {}
+
+ def test_inbox_store(self):
+ self.backend.inbox_store([self.user], self.message)
+ m = Inbox.objects.filter(user=self.user).get()
+ self.assertEqual(m.message, self.message)
+ self.assertRaises(MessageTypeNotSupported, self.backend.inbox_store, [], {})
+
+ def test_inbox_list(self):
+ message = self.backend.create_message(STORED_ERROR, 'Another message for you ♡♡♡')
+ self.backend.inbox_store([self.user], message)
+ self.backend.inbox_store([self.user], self.message)
+ messages = self.backend.inbox_list(self.user)
+ self.assertEqual(messages[0], message)
+ self.assertEqual(messages[1], self.message)
+ self.assertEqual(len(self.backend.inbox_list(self.anon)), 0)
+
+ def test_inbox_purge(self):
+ message = self.backend.create_message(STORED_ERROR, 'Another message for you')
+ self.backend.inbox_store([self.user], self.message)
+ self.backend.inbox_store([self.user], message)
+ self.backend.inbox_purge(self.user)
+ self.assertEqual(len(self.backend.inbox_list(self.user)), 0)
+ self.backend.inbox_purge(self.anon)
+
+ def test_inbox_delete(self):
+ self.backend.inbox_store([self.user], self.message)
+ self.backend.inbox_delete(self.user, self.message.id)
+ self.assertEqual(len(self.backend.inbox_list(self.user)), 0)
+ self.assertRaises(MessageDoesNotExist, self.backend.inbox_delete, self.user, -1)
+
+ def test_archive_store(self):
+ self.backend.archive_store([self.user], self.message)
+ m = MessageArchive.objects.filter(user=self.user).all()[0]
+ self.assertEqual(m.message, self.message)
+ self.assertRaises(MessageTypeNotSupported, self.backend.archive_store, [], {})
+
+ def test_archive_list(self):
+ message = self.backend.create_message(STORED_ERROR, 'Another message for you')
+ self.backend.archive_store([self.user], message)
+ self.backend.archive_store([self.user], self.message)
+ messages = self.backend.archive_list(self.user)
+ self.assertEqual(messages[0].message, message)
+ self.assertEqual(messages[1].message, self.message)
+
+ def test_create_message(self):
+ message = self.backend.create_message(STORED_ERROR, 'Another message for you')
+ self.assertIsInstance(message, Message)
+
+ def test_inbox_get(self):
+ self.backend.inbox_store([self.user], self.message)
+ m = self.backend.inbox_get(self.user, self.message.id)
+ self.assertEqual(m, self.message)
+ self.assertRaises(MessageDoesNotExist, self.backend.inbox_get, self.user, -1)
+
+ def test_can_handle(self):
+ self.assertFalse(self.backend.can_handle({}))
+ self.assertTrue(self.backend.can_handle(self.message))
+
+ def test_message_expiration(self):
+ six_months_ago = timezone.now() + timezone.timedelta(days=-180)
+ self.message.date = six_months_ago
+ self.message.save()
+ self.backend.expired_messages_cleanup()
+
+ n_archives = MessageArchive.objects.count()
+ n_inbox = Inbox.objects.count()
+ n_messages = Message.objects.count()
+ self.assertEqual(n_archives, 0)
+ self.assertEqual(n_inbox, 0)
+ self.assertEqual(n_messages, 0)
+
+ def test_inbox_signals(self):
+ # connect
+ signals.inbox_stored.connect(self.inbox_stored)
+ signals.inbox_deleted.connect(self.inbox_deleted)
+ signals.inbox_purged.connect(self.inbox_purged)
+
+ self.backend.inbox_store([self.user], self.message)
+ self.backend.inbox_delete(self.user, self.message.id)
+ self.backend.inbox_purge(self.user)
+
+ # disconnect
+ signals.inbox_stored.disconnect(self.inbox_stored)
+ signals.inbox_deleted.disconnect(self.inbox_deleted)
+ signals.inbox_purged.disconnect(self.inbox_purged)
+
+ self.assertIn('inbox_stored', self.signals)
+ self.assertIn('inbox_deleted', self.signals)
+ self.assertIn('inbox_purged', self.signals)
+
+ def inbox_stored(self, **kwargs):
+ self.signals['inbox_stored'] = (kwargs['user'], kwargs['message'])
+
+ def inbox_deleted(self, **kwargs):
+ self.signals['inbox_deleted'] = (kwargs['user'], kwargs['message_id'])
+
+ def inbox_purged(self, **kwargs):
+ self.signals['inbox_purged'] = (kwargs['user'])
+
+ def test_archive_signals(self):
+ # connect
+ signals.archive_stored.connect(self.archive_stored)
+
+ self.backend.archive_store([self.user], self.message)
+
+ # disconnect
+ signals.archive_stored.disconnect(self.archive_stored)
+
+ self.assertIn('archive_stored', self.signals)
+
+ def archive_stored(self, **kwargs):
+ self.signals['archive_stored'] = (kwargs['user'], kwargs['message'])
diff --git a/vendor/stored_messages/tests/test_models.py b/vendor/stored_messages/tests/test_models.py
new file mode 100644
index 00000000..a5dbc908
--- /dev/null
+++ b/vendor/stored_messages/tests/test_models.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+from .base import BaseTest
+
+from django.utils.timezone import timedelta, now
+from django.contrib.messages.storage import default_storage
+
+from stored_messages import add_message, STORED_ERROR
+from stored_messages.models import MessageArchive, Inbox
+
+
+class TestInbox(BaseTest):
+ def test_expire(self):
+ self.request._messages = default_storage(self.request)
+ add_message(self.request, STORED_ERROR, "an SOS to the world ☢")
+ mas = MessageArchive.objects.all()
+ self.assertEqual(len(mas), 1)
+ ma = mas[0]
+ ma.message.date = now() - timedelta(days=365)
+ ma.message.save()
+ inbox = Inbox.objects.create(user=ma.user, message=ma.message)
+ self.assertTrue(inbox.expired())
diff --git a/vendor/stored_messages/tests/test_redis_backend.py b/vendor/stored_messages/tests/test_redis_backend.py
new file mode 100644
index 00000000..ccd9f90e
--- /dev/null
+++ b/vendor/stored_messages/tests/test_redis_backend.py
@@ -0,0 +1,201 @@
+from .base import BaseTest
+
+import unittest
+import mock
+
+from django.conf import settings
+from django.core.cache import cache
+from django.contrib.auth.models import AnonymousUser
+from django.utils import timezone
+
+from stored_messages.backends.exceptions import MessageTypeNotSupported, MessageDoesNotExist
+from stored_messages.backends.redis import RedisBackend
+from stored_messages.backends.redis.backend import Message
+from stored_messages.backends import signals
+
+from stored_messages import STORED_ERROR
+
+
+class RedisMock(object):
+ """
+ Mock the Redis server instance with Django in-memory cache
+ """
+ def rpush(self, key, data):
+ l = cache.get(key)
+ if l is None:
+ l = []
+ l.append(data)
+ cache.set(key, l)
+
+ def lrange(self, key, *args, **kwargs):
+ return cache.get(key) or []
+
+ def delete(self, key):
+ cache.delete(key)
+
+ def lrem(self, key, count, data):
+ l = cache.get(key)
+ l.remove(data)
+ cache.set(key, l)
+
+ def flushdb(self):
+ cache.clear()
+
+ def sismember(self, key, val):
+ r = cache.get(key)
+ return r is not None and val in r
+
+ def sadd(self, key, val):
+ l = cache.get(key)
+
+ if l is None:
+ l = []
+
+ if val not in l:
+ l.append(val)
+ cache.set(key, l)
+
+ @staticmethod
+ def from_url(*args, **kwargs):
+ return RedisMock()
+
+ def StrictRedis(self, *args, **kwargs):
+ return self
+
+
+try:
+ from stored_messages.backends.redis.backend import redis
+ REDISPY_MISSING = False
+
+ if getattr(settings, 'MOCK_REDIS_SERVER', True):
+ patcher = mock.patch('stored_messages.backends.redis.backend.redis')
+ redis = patcher.start() # noqa
+ redis.StrictRedis = RedisMock
+
+except ImportError:
+ REDISPY_MISSING = True
+
+
+@unittest.skipIf(REDISPY_MISSING, "redis-py not installed")
+class TestRedisBackend(BaseTest):
+ def setUp(self):
+ super(TestRedisBackend, self).setUp()
+ self.client = redis.StrictRedis.from_url(settings.STORED_MESSAGES['REDIS_URL'])
+ self.backend = RedisBackend()
+ self.backend._flush()
+ self.message = self.backend.create_message(STORED_ERROR, 'A message for you')
+ self.anon = AnonymousUser()
+ self.signals = {}
+
+ def test_inbox_store(self):
+ self.backend.inbox_store([self.user], self.message)
+ data = self.client.lrange('user:%d:notifications' % self.user.pk, 0, -1).pop()
+ self.assertEqual(self.backend._fromJSON(data), self.message)
+ self.assertRaises(MessageTypeNotSupported, self.backend.inbox_store, [], {})
+
+ def test_inbox_list(self):
+ message = self.backend.create_message(STORED_ERROR, 'Another message for you')
+ self.backend.inbox_store([self.user], message)
+ self.backend.inbox_store([self.user], self.message)
+ messages = self.backend.inbox_list(self.user)
+ self.assertEqual(messages[0], message)
+ self.assertEqual(messages[1], self.message)
+ self.assertEqual(self.backend.inbox_list(self.anon), [])
+
+ def test_inbox_purge(self):
+ message = self.backend.create_message(STORED_ERROR, 'Another message for you')
+ self.backend.inbox_store([self.user], self.message)
+ self.backend.inbox_store([self.user], message)
+ self.backend.inbox_purge(self.user)
+ self.assertEqual(len(self.backend.inbox_list(self.user)), 0)
+ self.backend.inbox_purge(self.anon)
+
+ def test_inbox_delete(self):
+ self.backend.inbox_store([self.user], self.message)
+ self.backend.inbox_delete(self.user, self.message.id)
+ self.assertEqual(len(self.backend.inbox_list(self.user)), 0)
+ self.assertRaises(MessageDoesNotExist, self.backend.inbox_delete, self.user, -1)
+
+ def test_archive_store(self):
+ self.backend.archive_store([self.user], self.message)
+ data = self.client.lrange('user:%d:archive' % self.user.pk, 0, -1).pop()
+ self.assertEqual(self.backend._fromJSON(data), self.message)
+ self.assertRaises(MessageTypeNotSupported, self.backend.archive_store, [], {})
+
+ def test_archive_list(self):
+ message = self.backend.create_message(STORED_ERROR, 'Another message for you')
+ self.backend.archive_store([self.user], message)
+ self.backend.archive_store([self.user], self.message)
+ messages = self.backend.archive_list(self.user)
+ self.assertEqual(messages[0], message)
+ self.assertEqual(messages[1], self.message)
+
+ def test_create_message(self):
+ message = self.backend.create_message(STORED_ERROR, 'Another message for you')
+ self.assertIsInstance(message, Message)
+
+ def test_inbox_get(self):
+ self.backend.inbox_store([self.user], self.message)
+ m = self.backend.inbox_get(self.user, self.message.id)
+ self.assertEqual(m, self.message)
+ self.assertRaises(MessageDoesNotExist, self.backend.inbox_get, self.user, -1)
+
+ def test_can_handle(self):
+ self.assertFalse(self.backend.can_handle({}))
+ self.assertTrue(self.backend.can_handle(self.message))
+
+ def test_message_expiration(self):
+ # start clean
+ self.backend._flush()
+ six_months_ago = timezone.now() + timezone.timedelta(days=-180)
+ self.backend.create_message(STORED_ERROR, 'A message for you', date=six_months_ago)
+ self.backend.expired_messages_cleanup()
+
+ keys = self.client.keys('user:*:notifications')
+ for k in keys:
+ _, user, _ = k.split(':')
+ self.assertEqual(self.backend._list_key(k), [])
+ self.assertEqual(self.backend._list('user:%s:notifications', user), [])
+ self.assertEqual(self.backend._list('user:%s:archive', user), [])
+
+ def test_inbox_signals(self):
+ # connect
+ signals.inbox_stored.connect(self.inbox_stored)
+ signals.inbox_deleted.connect(self.inbox_deleted)
+ signals.inbox_purged.connect(self.inbox_purged)
+
+ self.backend.inbox_store([self.user], self.message)
+ self.backend.inbox_delete(self.user, self.message.id)
+ self.backend.inbox_purge(self.user)
+
+ # disconnect
+ signals.inbox_stored.disconnect(self.inbox_stored)
+ signals.inbox_deleted.disconnect(self.inbox_deleted)
+ signals.inbox_purged.disconnect(self.inbox_purged)
+
+ self.assertIn('inbox_stored', self.signals)
+ self.assertIn('inbox_deleted', self.signals)
+ self.assertIn('inbox_purged', self.signals)
+
+ def inbox_stored(self, **kwargs):
+ self.signals['inbox_stored'] = (kwargs['user'], kwargs['message'])
+
+ def inbox_deleted(self, **kwargs):
+ self.signals['inbox_deleted'] = (kwargs['user'], kwargs['message_id'])
+
+ def inbox_purged(self, **kwargs):
+ self.signals['inbox_purged'] = (kwargs['user'])
+
+ def test_archive_signals(self):
+ # connect
+ signals.archive_stored.connect(self.archive_stored)
+
+ self.backend.archive_store([self.user], self.message)
+
+ # disconnect
+ signals.archive_stored.disconnect(self.archive_stored)
+
+ self.assertIn('archive_stored', self.signals)
+
+ def archive_stored(self, **kwargs):
+ self.signals['archive_stored'] = (kwargs['user'], kwargs['message'])
diff --git a/vendor/stored_messages/tests/test_restapi.py b/vendor/stored_messages/tests/test_restapi.py
new file mode 100644
index 00000000..8ab25688
--- /dev/null
+++ b/vendor/stored_messages/tests/test_restapi.py
@@ -0,0 +1,80 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+import json
+
+from unittest import skipUnless
+from django.core.urlresolvers import reverse
+from django.test.utils import override_settings
+
+from stored_messages.constants import STORED_ERROR
+
+from .base import BackendBaseTest
+
+try:
+ import rest_framework # noqa
+ rest_framework_installed = True
+except ImportError:
+ rest_framework_installed = False
+
+
+@skipUnless(rest_framework_installed, "Django restframework is not installed")
+class TestRESTApi(BackendBaseTest):
+ def test_list(self):
+ self.client.login(username='test_user', password='123456')
+ self.client.get('/create')
+ self.client.get('/create')
+ r = self.client.get(reverse('stored_messages:inbox-list'))
+ messages = json.loads(r.content.decode('utf-8'))
+ self.assertEqual(len(messages), 2)
+ self.assertEqual(messages[0]['message'], 'an error ☢')
+ self.assertEqual(messages[1]['message'], 'an error ☢')
+ self.assertEqual(messages[0]['level'], STORED_ERROR)
+ self.assertEqual(messages[1]['level'], STORED_ERROR)
+
+ def test_retrieve(self):
+ self.client.login(username='test_user', password='123456')
+ self.client.get('/create')
+ r = self.client.get(reverse('stored_messages:inbox-list'))
+ messages = json.loads(r.content.decode('utf-8'))
+ id = messages[0]['id']
+
+ r = self.client.get(reverse('stored_messages:inbox-detail', kwargs={'pk': id}))
+
+ message = json.loads(r.content.decode('utf-8'))
+ self.assertEqual(message['message'], 'an error ☢')
+ self.assertEqual(message['id'], id)
+
+ def test_make_read(self):
+ self.client.login(username='test_user', password='123456')
+ self.client.get('/create')
+ r = self.client.get(reverse('stored_messages:inbox-list'))
+ messages = json.loads(r.content.decode('utf-8'))
+ self.assertEqual(len(messages), 1)
+ msg_id = messages[0]['id']
+ r = self.client.post(reverse('stored_messages:inbox-read', args=(msg_id,)))
+ self.assertEqual(r.status_code, 200)
+ r = self.client.get(reverse('stored_messages:inbox-list'))
+ messages = json.loads(r.content.decode('utf-8'))
+ self.assertEqual(len(messages), 0)
+
+ def test_anon(self):
+ self.client.get('/create')
+ r = self.client.get(reverse('stored_messages:inbox-list'))
+ messages = json.loads(r.content.decode('utf-8'))
+ self.assertEqual(len(messages), 0)
+
+ def test_mark_all_read(self):
+ self.client.login(username='test_user', password='123456')
+ self.client.get('/create')
+ r = self.client.post(reverse('stored_messages:mark_all_read'))
+ self.assertEqual(r.status_code, 200)
+ r = self.client.get(reverse('stored_messages:inbox-list'))
+ messages = json.loads(r.content.decode('utf-8'))
+ self.assertEqual(len(messages), 0)
+
+
+@skipUnless(rest_framework_installed, "Django restframework is not installed")
+@override_settings(STORED_MESSAGES={'STORAGE_BACKEND': 'stored_messages.backends.RedisBackend'})
+class TestRESTApiWithRedis(TestRESTApi):
+ pass
diff --git a/vendor/stored_messages/tests/test_storage.py b/vendor/stored_messages/tests/test_storage.py
new file mode 100644
index 00000000..17d782c0
--- /dev/null
+++ b/vendor/stored_messages/tests/test_storage.py
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+from .base import BackendBaseTest
+
+from django.contrib.messages.storage import default_storage
+
+from stored_messages import add_message, get_messages, STORED_ERROR, DEBUG, ERROR
+
+import mock
+
+
+class TestStorage(BackendBaseTest):
+ """
+ Test Storage class using default backend
+ """
+ def test_store(self):
+ self.request._messages = default_storage(self.request)
+ self.request._messages.level = DEBUG
+ add_message(self.request, STORED_ERROR, "an SOS to the world ☢")
+ add_message(self.request, DEBUG, "this won't be persisted ☢")
+ self.assertEqual(len(get_messages(self.request)), 2)
+ self.assertEqual(len(self.backend.archive_list(self.user)), 1)
+
+ def test_store_with_middleware(self):
+ self.client.login(username='test_user', password='123456')
+ self.client.get('/create')
+ inbox_msg = len(self.backend.inbox_list(self.user))
+ self.assertEqual(inbox_msg, 1)
+ self.client.get('/consume')
+ self.assertEqual(len(self.backend.inbox_list(self.user)), 0)
+ self.assertEqual(len(self.backend.archive_list(self.user)), 1)
+
+ def test_store_keep_unread(self):
+ self.client.login(username='test_user', password='123456')
+ self.client.get('/create')
+ self.client.get('/consume', data={'keep_storage': True})
+ self.assertEqual(len(self.backend.inbox_list(self.user)), 1)
+ self.assertEqual(len(self.backend.archive_list(self.user)), 1)
+
+ def test_store_anonymous(self):
+ self.request.user = mock.MagicMock(wraps=self.user)
+ self.request.user.is_anonymous.return_value = True
+ self.request.user.is_authenticated.return_value = False
+ self.request._messages = default_storage(self.request)
+ add_message(self.request, STORED_ERROR, "an SOS to the world ☢")
+ add_message(self.request, ERROR, "this error won't be persisted ☢")
+ storage = get_messages(self.request)
+ self.assertEqual(len(storage), 2)
+
+ def test_add_empty(self):
+ self.request._messages = default_storage(self.request)
+ add_message(self.request, DEBUG, '')
+ self.assertEqual(len(get_messages(self.request)), 0)
+
+ def test_add_mixed(self):
+ self.client.login(username='test_user', password='123456')
+ self.client.get('/create_mixed')
+ self.client.get('/consume')
+ self.assertEqual(len(self.backend.inbox_list(self.user)), 0)
diff --git a/vendor/stored_messages/tests/test_templatetags.py b/vendor/stored_messages/tests/test_templatetags.py
new file mode 100644
index 00000000..a55fb5e7
--- /dev/null
+++ b/vendor/stored_messages/tests/test_templatetags.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import unicode_literals
+
+from django.template import RequestContext, Template
+
+from .base import BaseTest
+
+import stored_messages
+
+
+class TestStoredMessagesTags(BaseTest):
+ def _create_messages(self):
+ # create a message
+ for i in range(20):
+ stored_messages.add_message_for([self.user], stored_messages.INFO, "unicode message ❤")
+
+ def test_stored_messages_list(self):
+ self._create_messages()
+
+ t = Template("{% load stored_messages_tags %}"
+ "{% stored_messages_list 15 %}")
+ render = t.render(RequestContext(self.request))
+ self.assertInHTML("[test_user] unicode message ❤ ", render, 15)
+
+ def test_stored_messages_archive(self):
+ self._create_messages()
+
+ t = Template("{% load stored_messages_tags %}"
+ "{% stored_messages_archive 15 %}")
+ render = t.render(RequestContext(self.request))
+ self.assertInHTML("[test_user] unicode message ❤ ", render, 15)
+
+ def test_stored_messages_count(self):
+ self._create_messages()
+
+ t = Template("{% load stored_messages_tags %}"
+ "{% stored_messages_count as count %}"
+ "There are {{ count }} messages
")
+ render = t.render(RequestContext(self.request))
+ self.assertInHTML("There are 20 messages
", render, 1)
+
+ def test_stored_messages_list_empty_for_unauthenticated_user(self):
+ stored_messages.add_message_for([self.user], stored_messages.INFO, "unicode message ❤")
+
+ t = Template("{% load stored_messages_tags %}"
+ "{% stored_messages_list 15 %}")
+ render = t.render(RequestContext(self.factory.get("/")))
+
+ self.assertInHTML("[test_user] unicode message ❤ ", render, 0)
+
+ def test_stored_messages_archive_empty_for_unauthenticated_user(self):
+ stored_messages.add_message_for([self.user], stored_messages.INFO, "unicode message ❤")
+
+ t = Template("{% load stored_messages_tags %}"
+ "{% stored_messages_archive 15 %}")
+ render = t.render(RequestContext(self.factory.get("/")))
+
+ self.assertInHTML("[test_user] unicode message ❤ ", render, 0)
+
+ def assertInHTML(self, needle, haystack, count=None, msg_prefix=''):
+ import django
+ if django.VERSION < (1, 5):
+ real_count = haystack.count(needle)
+ return self.assertEqual(real_count, count)
+ else:
+ return super(TestStoredMessagesTags, self).assertInHTML(
+ needle, haystack, count, msg_prefix)
diff --git a/vendor/stored_messages/tests/urls.py b/vendor/stored_messages/tests/urls.py
new file mode 100644
index 00000000..f626b2ae
--- /dev/null
+++ b/vendor/stored_messages/tests/urls.py
@@ -0,0 +1,14 @@
+from django.contrib import admin
+from django.urls import re_path as url, include
+
+from stored_messages.tests.views import message_view, message_create, message_create_mixed
+
+
+admin.autodiscover()
+
+urlpatterns = [
+ url(r'^consume$', message_view),
+ url(r'^create$', message_create),
+ url(r'^create_mixed$', message_create_mixed),
+ url(r'^messages', include('stored_messages.urls', namespace='stored_messages'))
+]
diff --git a/vendor/stored_messages/tests/views.py b/vendor/stored_messages/tests/views.py
new file mode 100644
index 00000000..35726b63
--- /dev/null
+++ b/vendor/stored_messages/tests/views.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+from django.http import HttpResponse
+
+import stored_messages
+
+
+def message_view(request):
+ keep_storage = request.GET.get('keep_storage', False)
+ storage = stored_messages.get_messages(request)
+ for _ in storage:
+ pass
+ if keep_storage:
+ storage.used = False
+ return HttpResponse()
+
+
+def message_create(request):
+ stored_messages.add_message(request, stored_messages.STORED_ERROR, "an error ☢")
+ stored_messages.add_message(request, stored_messages.STORED_DEBUG, "a debug message ☢")
+ return HttpResponse()
+
+
+def message_create_mixed(request):
+ stored_messages.add_message(request, stored_messages.STORED_ERROR, "an error ☢")
+ stored_messages.add_message(request, stored_messages.ERROR, "an error not persisted ☢")
+ return HttpResponse()
diff --git a/vendor/stored_messages/urls.py b/vendor/stored_messages/urls.py
new file mode 100644
index 00000000..632ae91f
--- /dev/null
+++ b/vendor/stored_messages/urls.py
@@ -0,0 +1,17 @@
+"""
+At the moment this module does something only when restframework is available
+"""
+from django.conf import settings
+
+if 'rest_framework' in settings.INSTALLED_APPS:
+ from rest_framework.routers import DefaultRouter
+ from django.urls import re_path as url, include
+ from . import views
+
+ router = DefaultRouter()
+ router.register(r'inbox', views.InboxViewSet, base_name='inbox')
+
+ urlpatterns = [
+ url(r'^', include(router.urls)),
+ url(r'^mark_all_read/$', views.mark_all_read, name='mark_all_read'),
+ ]
diff --git a/vendor/stored_messages/views.py b/vendor/stored_messages/views.py
new file mode 100644
index 00000000..c9979327
--- /dev/null
+++ b/vendor/stored_messages/views.py
@@ -0,0 +1,60 @@
+from rest_framework import viewsets
+from rest_framework.decorators import api_view, detail_route
+from rest_framework.response import Response
+
+from django.contrib.auth.decorators import login_required
+
+from .serializers import InboxSerializer
+from .backends.exceptions import MessageDoesNotExist
+
+
+class InboxViewSet(viewsets.ViewSet):
+ """
+ Provides `list` and `detail` actions, plus a `read` POST endpoint for
+ marking inbox messages as read.
+ """
+ def list(self, request):
+ from .settings import stored_messages_settings
+ backend = stored_messages_settings.STORAGE_BACKEND()
+ messages = backend.inbox_list(request.user)
+ serializer = InboxSerializer(messages, many=True)
+ return Response(serializer.data)
+
+ def retrieve(self, request, pk=None):
+ from .settings import stored_messages_settings
+ backend = stored_messages_settings.STORAGE_BACKEND()
+
+ try:
+ msg = backend.inbox_get(request.user, pk)
+ except MessageDoesNotExist as e:
+ return Response(e.message, status='404')
+
+ serializer = InboxSerializer(msg, many=False)
+ return Response(serializer.data)
+
+ @detail_route(methods=['POST'])
+ def read(self, request, pk=None):
+ """
+ Mark the message as read (i.e. delete from inbox)
+ """
+ from .settings import stored_messages_settings
+ backend = stored_messages_settings.STORAGE_BACKEND()
+
+ try:
+ backend.inbox_delete(request.user, pk)
+ except MessageDoesNotExist as e:
+ return Response(e.message, status='404')
+
+ return Response({'status': 'message marked as read'})
+
+
+@login_required
+@api_view(['POST'])
+def mark_all_read(request):
+ """
+ Mark all messages as read (i.e. delete from inbox) for current logged in user
+ """
+ from .settings import stored_messages_settings
+ backend = stored_messages_settings.STORAGE_BACKEND()
+ backend.inbox_purge(request.user)
+ return Response({"message": "All messages read"})
diff --git a/vendor/treebeard/__init__.py b/vendor/treebeard/__init__.py
new file mode 100644
index 00000000..c1f48015
--- /dev/null
+++ b/vendor/treebeard/__init__.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+"""
+See PEP 386 (https://www.python.org/dev/peps/pep-0386/)
+
+Release logic:
+ 1. Remove ".devX" from __version__ (below)
+ 2. git add treebeard/__init__.py
+ 3. git commit -m 'Bump to '
+ 4. git tag
+ 5. git push
+ 6. assure that all tests pass on https://travis-ci.org/django-treebeard/django-treebeard/builds/
+ 7. git push --tags
+ 8. python setup.py sdist upload
+ 9. bump the version, append ".dev0" to __version__
+10. git add treebeard/__init__.py
+11. git commit -m 'Start with '
+12. git push
+"""
+__version__ = '4.3.1'
diff --git a/vendor/treebeard/admin.py b/vendor/treebeard/admin.py
new file mode 100644
index 00000000..a878725b
--- /dev/null
+++ b/vendor/treebeard/admin.py
@@ -0,0 +1,147 @@
+"""Django admin support for treebeard"""
+
+import sys
+
+import django
+
+from django.conf import settings
+from django.urls import re_path as url
+
+from django.contrib import admin, messages
+from django.http import HttpResponse, HttpResponseBadRequest
+from django.utils.translation import gettext_lazy as _
+if sys.version_info >= (3, 0):
+ from django.utils.encoding import force_str
+else:
+ from django.utils.encoding import force_unicode as force_str
+
+from treebeard.exceptions import (InvalidPosition, MissingNodeOrderBy,
+ InvalidMoveToDescendant, PathOverflow)
+from treebeard.al_tree import AL_Node
+
+
+try:
+ from django.contrib.admin.options import TO_FIELD_VAR
+except ImportError:
+ from django.contrib.admin.views.main import TO_FIELD_VAR
+
+
+class TreeAdmin(admin.ModelAdmin):
+ """Django Admin class for treebeard."""
+
+ change_list_template = 'admin/tree_change_list.html'
+
+ def get_queryset(self, request):
+ if issubclass(self.model, AL_Node):
+ # AL Trees return a list instead of a QuerySet for .get_tree()
+ # So we're returning the regular .get_queryset cause we will use
+ # the old admin
+ return super(TreeAdmin, self).get_queryset(request)
+ else:
+ return self.model.get_tree()
+
+ def changelist_view(self, request, extra_context=None):
+ if issubclass(self.model, AL_Node):
+ # For AL trees, use the old admin display
+ self.change_list_template = 'admin/tree_list.html'
+ if extra_context is None:
+ extra_context = {}
+ if django.VERSION < (1, 10):
+ request_context = 'django.core.context_processors.request' in settings.TEMPLATE_CONTEXT_PROCESSORS
+ else:
+ request_context = any(
+ map(
+ lambda tmpl:
+ tmpl.get('BACKEND', None) == 'django.template.backends.django.DjangoTemplates' and
+ tmpl.get('APP_DIRS', False) and
+ 'django.template.context_processors.request' in tmpl.get('OPTIONS', {}).get('context_processors', []),
+ settings.TEMPLATES
+ )
+ )
+ lacks_request = ('request' not in extra_context and not request_context)
+ if lacks_request:
+ extra_context['request'] = request
+ return super(TreeAdmin, self).changelist_view(request, extra_context)
+
+ def get_urls(self):
+ """
+ Adds a url to move nodes to this admin
+ """
+ urls = super(TreeAdmin, self).get_urls()
+
+ if django.VERSION < (1, 10):
+ from django.views.i18n import javascript_catalog
+ jsi18n_url = url(r'^jsi18n/$', javascript_catalog, {'packages': ('treebeard',)})
+ else:
+ from django.views.i18n import JavaScriptCatalog
+
+ jsi18n_url = url(r'^jsi18n/$',
+ JavaScriptCatalog.as_view(packages=['treebeard']),
+ name='javascript-catalog'
+ )
+
+ new_urls = [
+ url('^move/$', self.admin_site.admin_view(self.move_node), ),
+ jsi18n_url,
+ ]
+ return new_urls + urls
+
+ def get_node(self, node_id):
+ return self.model.objects.get(pk=node_id)
+
+ def try_to_move_node(self, as_child, node, pos, request, target):
+ try:
+ node.move(target, pos=pos)
+ # Call the save method on the (reloaded) node in order to trigger
+ # possible signal handlers etc.
+ node = self.get_node(node.pk)
+ node.save()
+ except (MissingNodeOrderBy, PathOverflow, InvalidMoveToDescendant,
+ InvalidPosition):
+ e = sys.exc_info()[1]
+ # An error was raised while trying to move the node, then set an
+ # error message and return 400, this will cause a reload on the
+ # client to show the message
+ messages.error(request,
+ _('Exception raised while moving node: %s') % _(
+ force_str(e)))
+ return HttpResponseBadRequest('Exception raised during move')
+ if as_child:
+ msg = _('Moved node "%(node)s" as child of "%(other)s"')
+ else:
+ msg = _('Moved node "%(node)s" as sibling of "%(other)s"')
+ messages.info(request, msg % {'node': node, 'other': target})
+ return HttpResponse('OK')
+
+ def move_node(self, request):
+ try:
+ node_id = request.POST['node_id']
+ target_id = request.POST['sibling_id']
+ as_child = bool(int(request.POST.get('as_child', 0)))
+ except (KeyError, ValueError):
+ # Some parameters were missing return a BadRequest
+ return HttpResponseBadRequest('Malformed POST params')
+
+ node = self.get_node(node_id)
+ target = self.get_node(target_id)
+ is_sorted = True if node.node_order_by else False
+
+ pos = {
+ (True, True): 'sorted-child',
+ (True, False): 'last-child',
+ (False, True): 'sorted-sibling',
+ (False, False): 'left',
+ }[as_child, is_sorted]
+ return self.try_to_move_node(as_child, node, pos, request, target)
+
+
+def admin_factory(form_class):
+ """Dynamically build a TreeAdmin subclass for the given form class.
+
+ :param form_class:
+ :return: A TreeAdmin subclass.
+ """
+ return type(
+ form_class.__name__ + 'Admin',
+ (TreeAdmin,),
+ dict(form=form_class))
diff --git a/vendor/treebeard/al_tree.py b/vendor/treebeard/al_tree.py
new file mode 100644
index 00000000..8ac408ac
--- /dev/null
+++ b/vendor/treebeard/al_tree.py
@@ -0,0 +1,404 @@
+"""Adjacency List"""
+
+from django.core import serializers
+from django.db import models, transaction
+from django.utils.translation import gettext_noop as _
+from treebeard.exceptions import InvalidMoveToDescendant, NodeAlreadySaved
+from treebeard.models import Node
+
+
+def get_result_class(cls):
+ """
+ For the given model class, determine what class we should use for the
+ nodes returned by its tree methods (such as get_children).
+
+ Usually this will be trivially the same as the initial model class,
+ but there are special cases when model inheritance is in use:
+
+ * If the model extends another via multi-table inheritance, we need to
+ use whichever ancestor originally implemented the tree behaviour (i.e.
+ the one which defines the 'parent' field). We can't use the
+ subclass, because it's not guaranteed that the other nodes reachable
+ from the current one will be instances of the same subclass.
+
+ * If the model is a proxy model, the returned nodes should also use
+ the proxy class.
+ """
+ base_class = cls._meta.get_field('parent').model
+ if cls._meta.proxy_for_model == base_class:
+ return cls
+ else:
+ return base_class
+
+
+class AL_NodeManager(models.Manager):
+ """Custom manager for nodes in an Adjacency List tree."""
+ def get_queryset(self):
+ """Sets the custom queryset as the default."""
+ if self.model.node_order_by:
+ order_by = ['parent'] + list(self.model.node_order_by)
+ else:
+ order_by = ['parent', 'sib_order']
+ return super(AL_NodeManager, self).get_queryset().order_by(*order_by)
+
+
+class AL_Node(Node):
+ """Abstract model to create your own Adjacency List Trees."""
+
+ objects = AL_NodeManager()
+ node_order_by = None
+
+ @classmethod
+ def add_root(cls, **kwargs):
+ """Adds a root node to the tree."""
+
+ if len(kwargs) == 1 and 'instance' in kwargs:
+ # adding the passed (unsaved) instance to the tree
+ newobj = kwargs['instance']
+ if newobj.pk:
+ raise NodeAlreadySaved("Attempted to add a tree node that is "\
+ "already in the database")
+ else:
+ newobj = cls(**kwargs)
+
+ newobj._cached_depth = 1
+ if not cls.node_order_by:
+ try:
+ max = get_result_class(cls).objects.filter(
+ parent__isnull=True).order_by(
+ 'sib_order').reverse()[0].sib_order
+ except IndexError:
+ max = 0
+ newobj.sib_order = max + 1
+ newobj.save()
+ return newobj
+
+ @classmethod
+ def get_root_nodes(cls):
+ """:returns: A queryset containing the root nodes in the tree."""
+ return get_result_class(cls).objects.filter(parent__isnull=True)
+
+ def get_depth(self, update=False):
+ """
+ :returns: the depth (level) of the node
+ Caches the result in the object itself to help in loops.
+
+ :param update: Updates the cached value.
+ """
+
+ if self.parent_id is None:
+ return 1
+
+ try:
+ if update:
+ del self._cached_depth
+ else:
+ return self._cached_depth
+ except AttributeError:
+ pass
+
+ depth = 0
+ node = self
+ while node:
+ node = node.parent
+ depth += 1
+ self._cached_depth = depth
+ return depth
+
+ def get_children(self):
+ """:returns: A queryset of all the node's children"""
+ return get_result_class(self.__class__).objects.filter(parent=self)
+
+ def get_parent(self, update=False):
+ """:returns: the parent node of the current node object."""
+ if self._meta.proxy_for_model:
+ # the current node is a proxy model; the returned parent
+ # should be the same proxy model, so we need to explicitly
+ # fetch it as an instance of that model rather than simply
+ # following the 'parent' relation
+ if self.parent_id is None:
+ return None
+ else:
+ return self.__class__.objects.get(pk=self.parent_id)
+ else:
+ return self.parent
+
+ def get_ancestors(self):
+ """
+ :returns: A *list* containing the current node object's ancestors,
+ starting by the root node and descending to the parent.
+ """
+ ancestors = []
+ if self._meta.proxy_for_model:
+ # the current node is a proxy model; our result set
+ # should use the same proxy model, so we need to
+ # explicitly fetch instances of that model
+ # when following the 'parent' relation
+ cls = self.__class__
+ node = self
+ while node.parent_id:
+ node = cls.objects.get(pk=node.parent_id)
+ ancestors.insert(0, node)
+ else:
+ node = self.parent
+ while node:
+ ancestors.insert(0, node)
+ node = node.parent
+ return ancestors
+
+ def get_root(self):
+ """:returns: the root node for the current node object."""
+ ancestors = self.get_ancestors()
+ if ancestors:
+ return ancestors[0]
+ return self
+
+ def is_descendant_of(self, node):
+ """
+ :returns: ``True`` if the node if a descendant of another node given
+ as an argument, else, returns ``False``
+ """
+ return self.pk in [obj.pk for obj in node.get_descendants()]
+
+ @classmethod
+ def dump_bulk(cls, parent=None, keep_ids=True):
+ """Dumps a tree branch to a python data structure."""
+
+ serializable_cls = cls._get_serializable_model()
+ if (
+ parent and serializable_cls != cls and
+ parent.__class__ != serializable_cls
+ ):
+ parent = serializable_cls.objects.get(pk=parent.pk)
+
+ # a list of nodes: not really a queryset, but it works
+ objs = serializable_cls.get_tree(parent)
+
+ ret, lnk = [], {}
+ for node, pyobj in zip(objs, serializers.serialize('python', objs)):
+ depth = node.get_depth()
+ # django's serializer stores the attributes in 'fields'
+ fields = pyobj['fields']
+ del fields['parent']
+
+ # non-sorted trees have this
+ if 'sib_order' in fields:
+ del fields['sib_order']
+
+ if 'id' in fields:
+ del fields['id']
+
+ newobj = {'data': fields}
+ if keep_ids:
+ newobj['id'] = pyobj['pk']
+
+ if (not parent and depth == 1) or\
+ (parent and depth == parent.get_depth()):
+ ret.append(newobj)
+ else:
+ parentobj = lnk[node.parent_id]
+ if 'children' not in parentobj:
+ parentobj['children'] = []
+ parentobj['children'].append(newobj)
+ lnk[node.pk] = newobj
+ return ret
+
+ def add_child(self, **kwargs):
+ """Adds a child to the node."""
+ cls = get_result_class(self.__class__)
+
+ if len(kwargs) == 1 and 'instance' in kwargs:
+ # adding the passed (unsaved) instance to the tree
+ newobj = kwargs['instance']
+ if newobj.pk:
+ raise NodeAlreadySaved("Attempted to add a tree node that is "\
+ "already in the database")
+ else:
+ newobj = cls(**kwargs)
+
+ try:
+ newobj._cached_depth = self._cached_depth + 1
+ except AttributeError:
+ pass
+ if not cls.node_order_by:
+ try:
+ max = cls.objects.filter(parent=self).reverse(
+ )[0].sib_order
+ except IndexError:
+ max = 0
+ newobj.sib_order = max + 1
+ newobj.parent = self
+ newobj.save()
+ return newobj
+
+ @classmethod
+ def _get_tree_recursively(cls, results, parent, depth):
+ if parent:
+ nodes = parent.get_children()
+ else:
+ nodes = cls.get_root_nodes()
+ for node in nodes:
+ node._cached_depth = depth
+ results.append(node)
+ cls._get_tree_recursively(results, node, depth + 1)
+
+ @classmethod
+ def get_tree(cls, parent=None):
+ """
+ :returns: A list of nodes ordered as DFS, including the parent. If
+ no parent is given, the entire tree is returned.
+ """
+ if parent:
+ depth = parent.get_depth() + 1
+ results = [parent]
+ else:
+ depth = 1
+ results = []
+ cls._get_tree_recursively(results, parent, depth)
+ return results
+
+ def get_descendants(self):
+ """
+ :returns: A *list* of all the node's descendants, doesn't
+ include the node itself
+ """
+ return self.__class__.get_tree(parent=self)[1:]
+
+ def get_descendant_count(self):
+ """:returns: the number of descendants of a nodee"""
+ return len(self.get_descendants())
+
+ def get_siblings(self):
+ """
+ :returns: A queryset of all the node's siblings, including the node
+ itself.
+ """
+ if self.parent:
+ return get_result_class(self.__class__).objects.filter(
+ parent=self.parent)
+ return self.__class__.get_root_nodes()
+
+ def add_sibling(self, pos=None, **kwargs):
+ """Adds a new node as a sibling to the current node object."""
+ pos = self._prepare_pos_var_for_add_sibling(pos)
+
+ if len(kwargs) == 1 and 'instance' in kwargs:
+ # adding the passed (unsaved) instance to the tree
+ newobj = kwargs['instance']
+ if newobj.pk:
+ raise NodeAlreadySaved("Attempted to add a tree node that is "\
+ "already in the database")
+ else:
+ # creating a new object
+ newobj = get_result_class(self.__class__)(**kwargs)
+
+ if not self.node_order_by:
+ newobj.sib_order = self.__class__._get_new_sibling_order(pos,
+ self)
+ newobj.parent_id = self.parent_id
+ newobj.save()
+ return newobj
+
+ @classmethod
+ def _is_target_pos_the_last_sibling(cls, pos, target):
+ return pos == 'last-sibling' or (
+ pos == 'right' and target == target.get_last_sibling())
+
+ @classmethod
+ def _make_hole_in_db(cls, min, target_node):
+ qset = get_result_class(cls).objects.filter(sib_order__gte=min)
+ if target_node.is_root():
+ qset = qset.filter(parent__isnull=True)
+ else:
+ qset = qset.filter(parent=target_node.parent)
+ qset.update(sib_order=models.F('sib_order') + 1)
+
+ @classmethod
+ def _make_hole_and_get_sibling_order(cls, pos, target_node):
+ siblings = target_node.get_siblings()
+ siblings = {
+ 'left': siblings.filter(sib_order__gte=target_node.sib_order),
+ 'right': siblings.filter(sib_order__gt=target_node.sib_order),
+ 'first-sibling': siblings
+ }[pos]
+ sib_order = {
+ 'left': target_node.sib_order,
+ 'right': target_node.sib_order + 1,
+ 'first-sibling': 1
+ }[pos]
+ try:
+ min = siblings.order_by('sib_order')[0].sib_order
+ except IndexError:
+ min = 0
+ if min:
+ cls._make_hole_in_db(min, target_node)
+ return sib_order
+
+ @classmethod
+ def _get_new_sibling_order(cls, pos, target_node):
+ if cls._is_target_pos_the_last_sibling(pos, target_node):
+ sib_order = target_node.get_last_sibling().sib_order + 1
+ else:
+ sib_order = cls._make_hole_and_get_sibling_order(pos, target_node)
+ return sib_order
+
+ def move(self, target, pos=None):
+ """
+ Moves the current node and all it's descendants to a new position
+ relative to another node.
+ """
+
+ pos = self._prepare_pos_var_for_move(pos)
+
+ sib_order = None
+ parent = None
+
+ if pos in ('first-child', 'last-child', 'sorted-child'):
+ # moving to a child
+ if not target.is_leaf():
+ target = target.get_last_child()
+ pos = {'first-child': 'first-sibling',
+ 'last-child': 'last-sibling',
+ 'sorted-child': 'sorted-sibling'}[pos]
+ else:
+ parent = target
+ if pos == 'sorted-child':
+ pos = 'sorted-sibling'
+ else:
+ pos = 'first-sibling'
+ sib_order = 1
+
+ if target.is_descendant_of(self):
+ raise InvalidMoveToDescendant(
+ _("Can't move node to a descendant."))
+
+ if self == target and (
+ (pos == 'left') or
+ (pos in ('right', 'last-sibling') and
+ target == target.get_last_sibling()) or
+ (pos == 'first-sibling' and
+ target == target.get_first_sibling())):
+ # special cases, not actually moving the node so no need to UPDATE
+ return
+
+ if pos == 'sorted-sibling':
+ if parent:
+ self.parent = parent
+ else:
+ self.parent = target.parent
+ else:
+ if sib_order:
+ self.sib_order = sib_order
+ else:
+ self.sib_order = self.__class__._get_new_sibling_order(pos,
+ target)
+ if parent:
+ self.parent = parent
+ else:
+ self.parent = target.parent
+
+ self.save()
+
+ class Meta:
+ """Abstract model."""
+ abstract = True
diff --git a/vendor/treebeard/exceptions.py b/vendor/treebeard/exceptions.py
new file mode 100644
index 00000000..96364185
--- /dev/null
+++ b/vendor/treebeard/exceptions.py
@@ -0,0 +1,31 @@
+"""Treebeard exceptions"""
+
+
+class InvalidPosition(Exception):
+ """Raised when passing an invalid pos value"""
+
+
+class InvalidMoveToDescendant(Exception):
+ """Raised when attemping to move a node to one of it's descendants."""
+
+
+class NodeAlreadySaved(Exception):
+ """
+ Raised when attempting to add a node which is already saved to the
+ database.
+ """
+
+
+class MissingNodeOrderBy(Exception):
+ """
+ Raised when an operation needs a missing
+ :attr:`~treebeard.MP_Node.node_order_by` attribute
+ """
+
+
+class PathOverflow(Exception):
+ """
+ Raised when trying to add or move a node to a position where no more nodes
+ can be added (see :attr:`~treebeard.MP_Node.path` and
+ :attr:`~treebeard.MP_Node.alphabet` for more info)
+ """
diff --git a/vendor/treebeard/forms.py b/vendor/treebeard/forms.py
new file mode 100644
index 00000000..98c72895
--- /dev/null
+++ b/vendor/treebeard/forms.py
@@ -0,0 +1,229 @@
+"""Forms for treebeard."""
+
+from django import forms
+from django.db.models.query import QuerySet
+from django.forms.models import BaseModelForm, ErrorList, model_to_dict
+from django.forms.models import modelform_factory as django_modelform_factory
+from django.utils.html import escape
+from django.utils.safestring import mark_safe
+from django.utils.translation import gettext_lazy as _
+
+from treebeard.al_tree import AL_Node
+from treebeard.mp_tree import MP_Node
+from treebeard.ns_tree import NS_Node
+
+
+class MoveNodeForm(forms.ModelForm):
+ """
+ Form to handle moving a node in a tree.
+
+ Handles sorted/unsorted trees.
+
+ It adds two fields to the form:
+
+ - Relative to: The target node where the current node will
+ be moved to.
+ - Position: The position relative to the target node that
+ will be used to move the node. These can be:
+
+ - For sorted trees: ``Child of`` and ``Sibling of``
+ - For unsorted trees: ``First child of``, ``Before`` and
+ ``After``
+
+ .. warning::
+
+ Subclassing :py:class:`MoveNodeForm` directly is
+ discouraged, since special care is needed to handle
+ excluded fields, and these change depending on the
+ tree type.
+
+ It is recommended that the :py:func:`movenodeform_factory`
+ function is used instead.
+
+ """
+
+ __position_choices_sorted = (
+ ('sorted-child', _('Child of')),
+ ('sorted-sibling', _('Sibling of')),
+ )
+
+ __position_choices_unsorted = (
+ ('first-child', _('First child of')),
+ ('left', _('Before')),
+ ('right', _('After')),
+ )
+
+ _position = forms.ChoiceField(required=True, label=_("Position"))
+
+ _ref_node_id = forms.TypedChoiceField(required=False,
+ coerce=int,
+ label=_("Relative to"))
+
+ def _get_position_ref_node(self, instance):
+ if self.is_sorted:
+ position = 'sorted-child'
+ node_parent = instance.get_parent()
+ if node_parent:
+ ref_node_id = node_parent.pk
+ else:
+ ref_node_id = ''
+ else:
+ prev_sibling = instance.get_prev_sibling()
+ if prev_sibling:
+ position = 'right'
+ ref_node_id = prev_sibling.pk
+ else:
+ position = 'first-child'
+ if instance.is_root():
+ ref_node_id = ''
+ else:
+ ref_node_id = instance.get_parent().pk
+ return {'_ref_node_id': ref_node_id,
+ '_position': position}
+
+ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
+ initial=None, error_class=ErrorList, label_suffix=':',
+ empty_permitted=False, instance=None, **kwargs):
+ opts = self._meta
+ if opts.model is None:
+ raise ValueError('ModelForm has no model class specified')
+
+ # update the '_position' field choices
+ self.is_sorted = getattr(opts.model, 'node_order_by', False)
+ if self.is_sorted:
+ choices_sort_mode = self.__class__.__position_choices_sorted
+ else:
+ choices_sort_mode = self.__class__.__position_choices_unsorted
+ self.declared_fields['_position'].choices = choices_sort_mode
+
+ # update the '_ref_node_id' choices
+ choices = self.mk_dropdown_tree(opts.model, for_node=instance)
+ self.declared_fields['_ref_node_id'].choices = choices
+
+ # put initial data for these fields into a map, update the map with
+ # initial data, and pass this new map to the parent constructor as
+ # initial data
+ if instance is None:
+ initial_ = {}
+ else:
+ initial_ = self._get_position_ref_node(instance)
+
+ if initial is not None:
+ initial_.update(initial)
+
+ super(MoveNodeForm, self).__init__(
+ data=data, files=files, auto_id=auto_id, prefix=prefix,
+ initial=initial_, error_class=error_class,
+ label_suffix=label_suffix, empty_permitted=empty_permitted,
+ instance=instance, **kwargs)
+
+ def _clean_cleaned_data(self):
+ """ delete auxilary fields not belonging to node model """
+ reference_node_id = 0
+
+ if '_ref_node_id' in self.cleaned_data:
+ reference_node_id = self.cleaned_data['_ref_node_id']
+ del self.cleaned_data['_ref_node_id']
+
+ position_type = self.cleaned_data['_position']
+ del self.cleaned_data['_position']
+
+ return position_type, reference_node_id
+
+ def save(self, commit=True):
+ position_type, reference_node_id = self._clean_cleaned_data()
+
+ if self.instance.pk is None:
+ cl_data = {}
+ for field in self.cleaned_data:
+ if not isinstance(self.cleaned_data[field], (list, QuerySet)):
+ cl_data[field] = self.cleaned_data[field]
+ if reference_node_id:
+ reference_node = self._meta.model.objects.get(
+ pk=reference_node_id)
+ self.instance = reference_node.add_child(**cl_data)
+ self.instance.move(reference_node, pos=position_type)
+ else:
+ self.instance = self._meta.model.add_root(**cl_data)
+ else:
+ self.instance.save()
+ if reference_node_id:
+ reference_node = self._meta.model.objects.get(
+ pk=reference_node_id)
+ self.instance.move(reference_node, pos=position_type)
+ else:
+ if self.is_sorted:
+ pos = 'sorted-sibling'
+ else:
+ pos = 'first-sibling'
+ self.instance.move(self._meta.model.get_first_root_node(), pos)
+ # Reload the instance
+ self.instance = self._meta.model.objects.get(pk=self.instance.pk)
+ super(MoveNodeForm, self).save(commit=commit)
+ return self.instance
+
+ @staticmethod
+ def is_loop_safe(for_node, possible_parent):
+ if for_node is not None:
+ return not (
+ possible_parent == for_node
+ ) or (possible_parent.is_descendant_of(for_node))
+ return True
+
+ @staticmethod
+ def mk_indent(level):
+ return ' ' * (level - 1)
+
+ @classmethod
+ def add_subtree(cls, for_node, node, options):
+ """ Recursively build options tree. """
+ if cls.is_loop_safe(for_node, node):
+ options.append(
+ (node.pk,
+ mark_safe(cls.mk_indent(node.get_depth()) + escape(node))))
+ for subnode in node.get_children():
+ cls.add_subtree(for_node, subnode, options)
+
+ @classmethod
+ def mk_dropdown_tree(cls, model, for_node=None):
+ """ Creates a tree-like list of choices """
+
+ options = [(0, _('-- root --'))]
+ for node in model.get_root_nodes():
+ cls.add_subtree(for_node, node, options)
+ return options
+
+
+def movenodeform_factory(model, form=MoveNodeForm, fields=None, exclude=None,
+ formfield_callback=None, widgets=None):
+ """Dynamically build a MoveNodeForm subclass with the proper Meta.
+
+ :param Node model:
+
+ The subclass of :py:class:`Node` that will be handled
+ by the form.
+
+ :param form:
+
+ The form class that will be used as a base. By
+ default, :py:class:`MoveNodeForm` will be used.
+
+ :return: A :py:class:`MoveNodeForm` subclass
+ """
+ _exclude = _get_exclude_for_model(model, exclude)
+ return django_modelform_factory(
+ model, form, fields, _exclude, formfield_callback, widgets)
+
+
+def _get_exclude_for_model(model, exclude):
+ if exclude:
+ _exclude = tuple(exclude)
+ else:
+ _exclude = ()
+ if issubclass(model, AL_Node):
+ _exclude += ('sib_order', 'parent')
+ elif issubclass(model, MP_Node):
+ _exclude += ('depth', 'numchild', 'path')
+ elif issubclass(model, NS_Node):
+ _exclude += ('depth', 'lft', 'rgt', 'tree_id')
+ return _exclude
diff --git a/vendor/treebeard/locale/de/LC_MESSAGES/django.po b/vendor/treebeard/locale/de/LC_MESSAGES/django.po
new file mode 100644
index 00000000..2e668fe3
--- /dev/null
+++ b/vendor/treebeard/locale/de/LC_MESSAGES/django.po
@@ -0,0 +1,86 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-06-15 20:56+0000\n"
+"PO-Revision-Date: 2018-06-15 23:09+0100\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Poedit 1.5.7\n"
+
+#: treebeard/admin.py:106
+#, python-format
+msgid "Exception raised while moving node: %s"
+msgstr "Ausnahmefehler in folgendem Element: %s"
+
+#: treebeard/admin.py:110
+#, python-format
+msgid "Moved node \"%(node)s\" as child of \"%(other)s\""
+msgstr "Element \"%(node)s\" positioniert unterhalb von \"%(other)s\""
+
+#: treebeard/admin.py:112
+#, python-format
+msgid "Moved node \"%(node)s\" as sibling of \"%(other)s\""
+msgstr "Element \"%(node)s\" positioniert gleichauf mit \"%(other)s\""
+
+#: treebeard/al_tree.py:373 treebeard/mp_tree.py:474 treebeard/ns_tree.py:363
+msgid "Can't move node to a descendant."
+msgstr "Kann Element nicht in ein eigenes Unter-Element verschieben"
+
+#: treebeard/forms.py:46
+msgid "Child of"
+msgstr "als Unterkategorie von"
+
+#: treebeard/forms.py:47
+msgid "Sibling of"
+msgstr "auf gleicher Ebene wie"
+
+#: treebeard/forms.py:51
+msgid "First child of"
+msgstr "Als erste Unterkategorie von"
+
+#: treebeard/forms.py:52
+msgid "Before"
+msgstr "Vor"
+
+#: treebeard/forms.py:53
+msgid "After"
+msgstr "Nach"
+
+#: treebeard/forms.py:56
+msgid "Position"
+msgstr "Position"
+
+#: treebeard/forms.py:60
+msgid "Relative to"
+msgstr "relativ zu"
+
+#: treebeard/forms.py:189
+msgid "-- root --"
+msgstr "-- Basiskategorie --"
+
+#: treebeard/mp_tree.py:382
+msgid ""
+"The new node is too deep in the tree, try increasing the path.max_length "
+"property and UPDATE your database"
+msgstr ""
+"Das neue Element ist zu tief positioniert. Versuche path.max_length zu "
+"erhöhen und aktualisiere die Datenbank"
+
+#: treebeard/mp_tree.py:1114
+#, python-format
+msgid "Path Overflow from: '%s'"
+msgstr "Pfad Überlauf von: '%s'"
+
+#: treebeard/templatetags/admin_tree.py:249
+msgid "Return to ordered tree"
+msgstr "Zurück zur geordneten Baumansicht"
diff --git a/vendor/treebeard/locale/de/LC_MESSAGES/djangojs.po b/vendor/treebeard/locale/de/LC_MESSAGES/djangojs.po
new file mode 100644
index 00000000..1400631c
--- /dev/null
+++ b/vendor/treebeard/locale/de/LC_MESSAGES/djangojs.po
@@ -0,0 +1,30 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-06-15 20:56+0000\n"
+"PO-Revision-Date: 2018-06-15 23:10+0100\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Poedit 1.5.7\n"
+
+#: treebeard/static/treebeard/treebeard-admin.js:158
+msgid "Abort"
+msgstr "Abbruch"
+
+#: treebeard/static/treebeard/treebeard-admin.js:180
+msgid "As Sibling"
+msgstr "Als Geschwister-Element"
+
+#: treebeard/static/treebeard/treebeard-admin.js:198
+msgid "As child"
+msgstr "Als Kind-Element"
diff --git a/vendor/treebeard/locale/es/LC_MESSAGES/django.po b/vendor/treebeard/locale/es/LC_MESSAGES/django.po
new file mode 100644
index 00000000..c950ba58
--- /dev/null
+++ b/vendor/treebeard/locale/es/LC_MESSAGES/django.po
@@ -0,0 +1,78 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Django-treebeard\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2011-07-18 17:36+0200\n"
+"PO-Revision-Date: 2010-05-03 23:40-0500\n"
+"Last-Translator: Gustavo Picon \n"
+"Language-Team: Spanish\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:113
+#, python-format
+msgid "Moved node \"%(node)s\" as child of \"%(other)s\""
+msgstr ""
+
+#: admin.py:119
+#, python-format
+msgid "Moved node \"%(node)s\" as sibling of \"%(other)s\""
+msgstr ""
+
+#: admin.py:129
+#, python-format
+msgid "Exception raised while moving node: %s"
+msgstr ""
+
+#: al_tree.py:319 mp_tree.py:641 ns_tree.py:308
+msgid "Can't move node to a descendant."
+msgstr ""
+
+#: forms.py:17
+msgid "Child of"
+msgstr "Hijo de"
+
+#: forms.py:18
+msgid "Sibling of"
+msgstr "Hermano de"
+
+#: forms.py:22
+msgid "First child of"
+msgstr "Primer hijo de"
+
+#: forms.py:23
+msgid "Before"
+msgstr "Antes"
+
+#: forms.py:24
+msgid "After"
+msgstr "Después"
+
+#: forms.py:27
+msgid "Position"
+msgstr "Posición"
+
+#: forms.py:31
+msgid "Relative to"
+msgstr "Relativo a"
+
+#: forms.py:81
+msgid "-- root --"
+msgstr "-- raíz --"
+
+#: mp_tree.py:521
+msgid ""
+"The new node is too deep in the tree, try increasing the path.max_length "
+"property and UPDATE your database"
+msgstr ""
+
+#: mp_tree.py:702
+#, python-format
+msgid "Path Overflow from: '%s'"
+msgstr ""
+
+#: templatetags/admin_tree.py:148
+msgid "Return to ordered tree"
+msgstr ""
diff --git a/vendor/treebeard/locale/es/LC_MESSAGES/djangojs.po b/vendor/treebeard/locale/es/LC_MESSAGES/djangojs.po
new file mode 100644
index 00000000..bf30b865
--- /dev/null
+++ b/vendor/treebeard/locale/es/LC_MESSAGES/djangojs.po
@@ -0,0 +1,24 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Django-treebeard\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2011-07-18 14:12+0200\n"
+"PO-Revision-Date: 2011-07-18 14:12+0200\n"
+"Last-Translator: \n"
+"Language-Team: Spanish\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: static/treebeard/treebeard-admin.js:157
+msgid "Abort"
+msgstr ""
+
+#: static/treebeard/treebeard-admin.js:172
+msgid "As Sibling"
+msgstr ""
+
+#: static/treebeard/treebeard-admin.js:190
+msgid "As child"
+msgstr ""
diff --git a/vendor/treebeard/locale/nl/LC_MESSAGES/django.po b/vendor/treebeard/locale/nl/LC_MESSAGES/django.po
new file mode 100644
index 00000000..6cea35f1
--- /dev/null
+++ b/vendor/treebeard/locale/nl/LC_MESSAGES/django.po
@@ -0,0 +1,80 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Django-treebeard\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2011-07-18 17:36+0200\n"
+"PO-Revision-Date: 2011-07-18 14:11+0200\n"
+"Last-Translator: Jaap Roes \n"
+"Language-Team: Dutch\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:113
+#, python-format
+msgid "Moved node \"%(node)s\" as child of \"%(other)s\""
+msgstr "\"%(node)s\" is nu onderdeel van \"%(other)s\""
+
+#: admin.py:119
+#, python-format
+msgid "Moved node \"%(node)s\" as sibling of \"%(other)s\""
+msgstr "\"%(node)s\" staat nu voor \"%(other)s\""
+
+#: admin.py:129
+#, python-format
+msgid "Exception raised while moving node: %s"
+msgstr "Fatale fout tijdens het verplaatsen: %s"
+
+#: al_tree.py:319 mp_tree.py:641 ns_tree.py:308
+msgid "Can't move node to a descendant."
+msgstr "Kan node niet naar eigen subnode verplaatsen"
+
+#: forms.py:17
+msgid "Child of"
+msgstr "Onderdeel"
+
+#: forms.py:18
+msgid "Sibling of"
+msgstr "Naast"
+
+#: forms.py:22
+msgid "First child of"
+msgstr "1e onderdeel"
+
+#: forms.py:23
+msgid "Before"
+msgstr "Voor"
+
+#: forms.py:24
+msgid "After"
+msgstr "Na"
+
+#: forms.py:27
+msgid "Position"
+msgstr "Positie"
+
+#: forms.py:31
+msgid "Relative to"
+msgstr "Ten opzichte van"
+
+#: forms.py:81
+msgid "-- root --"
+msgstr "-- hoofdniveau --"
+
+#: mp_tree.py:521
+msgid ""
+"The new node is too deep in the tree, try increasing the path.max_length "
+"property and UPDATE your database"
+msgstr ""
+"De nieuwe node bevindt zich te diep in de boom. Verhoog de path.max_lenght "
+"waarde en UPDATE de database."
+
+#: mp_tree.py:702
+#, python-format
+msgid "Path Overflow from: '%s'"
+msgstr "Path overflow van: '%s'"
+
+#: templatetags/admin_tree.py:148
+msgid "Return to ordered tree"
+msgstr "Als gesorteerde boom"
diff --git a/vendor/treebeard/locale/nl/LC_MESSAGES/djangojs.po b/vendor/treebeard/locale/nl/LC_MESSAGES/djangojs.po
new file mode 100644
index 00000000..77393634
--- /dev/null
+++ b/vendor/treebeard/locale/nl/LC_MESSAGES/djangojs.po
@@ -0,0 +1,24 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Django-treebeard\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2011-07-18 14:12+0200\n"
+"PO-Revision-Date: 2011-07-18 14:12+0200\n"
+"Last-Translator: Jaap Roes \n"
+"Language-Team: Dutch\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: static/treebeard/treebeard-admin.js:157
+msgid "Abort"
+msgstr "Annuleren"
+
+#: static/treebeard/treebeard-admin.js:172
+msgid "As Sibling"
+msgstr "Als naastliggend onderdeel"
+
+#: static/treebeard/treebeard-admin.js:190
+msgid "As child"
+msgstr "Als subonderdeel"
diff --git a/vendor/treebeard/locale/pl/LC_MESSAGES/django.po b/vendor/treebeard/locale/pl/LC_MESSAGES/django.po
new file mode 100644
index 00000000..be741ab7
--- /dev/null
+++ b/vendor/treebeard/locale/pl/LC_MESSAGES/django.po
@@ -0,0 +1,44 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Django-treebeard\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-05-03 23:53-0500\n"
+"PO-Revision-Date: 2010-05-03 23:40-0500\n"
+"Last-Translator: Bartosz Turkot \n"
+"Language-Team: Polish\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: forms.py:16
+msgid "Child of"
+msgstr "Dziecko kategorii"
+
+#: forms.py:17
+msgid "Sibling of"
+msgstr "Sąsiad kategorii"
+
+#: forms.py:21
+msgid "First child of"
+msgstr "Pierwsze dziecko kategorii"
+
+#: forms.py:22
+msgid "Before"
+msgstr "Przed"
+
+#: forms.py:23
+msgid "After"
+msgstr "Za"
+
+#: forms.py:26
+msgid "Position"
+msgstr "Pozycja"
+
+#: forms.py:30
+msgid "Relative to"
+msgstr "Względem"
+
+#: forms.py:80
+msgid "-- root --"
+msgstr "-- kategoria główna --"
diff --git a/vendor/treebeard/locale/ru/LC_MESSAGES/django.po b/vendor/treebeard/locale/ru/LC_MESSAGES/django.po
new file mode 100644
index 00000000..f335c16e
--- /dev/null
+++ b/vendor/treebeard/locale/ru/LC_MESSAGES/django.po
@@ -0,0 +1,79 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Django-treebeard\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2011-07-18 17:36+0200\n"
+"PO-Revision-Date: 2009-04-10 18:37+0400\n"
+"Last-Translator: chembervint \n"
+"Language-Team: Russian\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"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"
+
+#: admin.py:113
+#, python-format
+msgid "Moved node \"%(node)s\" as child of \"%(other)s\""
+msgstr ""
+
+#: admin.py:119
+#, python-format
+msgid "Moved node \"%(node)s\" as sibling of \"%(other)s\""
+msgstr ""
+
+#: admin.py:129
+#, python-format
+msgid "Exception raised while moving node: %s"
+msgstr ""
+
+#: al_tree.py:319 mp_tree.py:641 ns_tree.py:308
+msgid "Can't move node to a descendant."
+msgstr ""
+
+#: forms.py:17
+msgid "Child of"
+msgstr "Вложенный"
+
+#: forms.py:18
+msgid "Sibling of"
+msgstr "Соседний к"
+
+#: forms.py:22
+msgid "First child of"
+msgstr "Первый вложенный"
+
+#: forms.py:23
+msgid "Before"
+msgstr "До"
+
+#: forms.py:24
+msgid "After"
+msgstr "После"
+
+#: forms.py:27
+msgid "Position"
+msgstr "Позиция"
+
+#: forms.py:31
+msgid "Relative to"
+msgstr "Относительно"
+
+#: forms.py:81
+msgid "-- root --"
+msgstr "-- корень --"
+
+#: mp_tree.py:521
+msgid ""
+"The new node is too deep in the tree, try increasing the path.max_length "
+"property and UPDATE your database"
+msgstr ""
+
+#: mp_tree.py:702
+#, python-format
+msgid "Path Overflow from: '%s'"
+msgstr ""
+
+#: templatetags/admin_tree.py:148
+msgid "Return to ordered tree"
+msgstr ""
diff --git a/vendor/treebeard/locale/ru/LC_MESSAGES/djangojs.po b/vendor/treebeard/locale/ru/LC_MESSAGES/djangojs.po
new file mode 100644
index 00000000..b02a2a14
--- /dev/null
+++ b/vendor/treebeard/locale/ru/LC_MESSAGES/djangojs.po
@@ -0,0 +1,25 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Django-treebeard\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2011-07-18 14:12+0200\n"
+"PO-Revision-Date: 2011-07-18 14:12+0200\n"
+"Last-Translator: \n"
+"Language-Team: Russian\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"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"
+
+#: static/treebeard/treebeard-admin.js:157
+msgid "Abort"
+msgstr ""
+
+#: static/treebeard/treebeard-admin.js:172
+msgid "As Sibling"
+msgstr ""
+
+#: static/treebeard/treebeard-admin.js:190
+msgid "As child"
+msgstr ""
diff --git a/vendor/treebeard/models.py b/vendor/treebeard/models.py
new file mode 100644
index 00000000..8db6211d
--- /dev/null
+++ b/vendor/treebeard/models.py
@@ -0,0 +1,658 @@
+"""Models and base API"""
+
+import sys
+import operator
+
+if sys.version_info >= (3, 0):
+ from functools import reduce
+
+import django
+from django.db.models import Q
+from django.db import models, transaction, router, connections
+
+from treebeard.exceptions import InvalidPosition, MissingNodeOrderBy
+
+
+class Node(models.Model):
+ """Node class"""
+
+ _db_connection = None
+
+ @classmethod
+ def add_root(cls, **kwargs): # pragma: no cover
+ """
+ Adds a root node to the tree. The new root node will be the new
+ rightmost root node. If you want to insert a root node at a specific
+ position, use :meth:`add_sibling` in an already existing root node
+ instead.
+
+ :param \*\*kwargs: object creation data that will be passed to the
+ inherited Node model
+ :param instance: Instead of passing object creation data, you can
+ pass an already-constructed (but not yet saved) model instance to
+ be inserted into the tree.
+
+ :returns: the created node object. It will be save()d by this method.
+
+ :raise NodeAlreadySaved: when the passed ``instance`` already exists
+ in the database
+ """
+ raise NotImplementedError
+
+ @classmethod
+ def get_foreign_keys(cls):
+ """Get foreign keys and models they refer to, so we can pre-process
+ the data for load_bulk
+ """
+ foreign_keys = {}
+ for field in cls._meta.fields:
+ if (
+ field.get_internal_type() == 'ForeignKey' and
+ field.name != 'parent'
+ ):
+ if django.VERSION >= (1, 9):
+ foreign_keys[field.name] = field.remote_field.model
+ else:
+ foreign_keys[field.name] = field.rel.to
+ return foreign_keys
+
+ @classmethod
+ def _process_foreign_keys(cls, foreign_keys, node_data):
+ """For each foreign key try to load the actual object so load_bulk
+ doesn't fail trying to load an int where django expects a
+ model instance
+ """
+ for key in foreign_keys.keys():
+ if key in node_data:
+ node_data[key] = foreign_keys[key].objects.get(
+ pk=node_data[key])
+
+ @classmethod
+ def load_bulk(cls, bulk_data, parent=None, keep_ids=False):
+ """
+ Loads a list/dictionary structure to the tree.
+
+
+ :param bulk_data:
+
+ The data that will be loaded, the structure is a list of
+ dictionaries with 2 keys:
+
+ - ``data``: will store arguments that will be passed for object
+ creation, and
+
+ - ``children``: a list of dictionaries, each one has it's own
+ ``data`` and ``children`` keys (a recursive structure)
+
+
+ :param parent:
+
+ The node that will receive the structure as children, if not
+ specified the first level of the structure will be loaded as root
+ nodes
+
+
+ :param keep_ids:
+
+ If enabled, loads the nodes with the same id that are given in the
+ structure. Will error if there are nodes without id info or if the
+ ids are already used.
+
+
+ :returns: A list of the added node ids.
+ """
+
+ # tree, iterative preorder
+ added = []
+ # stack of nodes to analyze
+ stack = [(parent, node) for node in bulk_data[::-1]]
+ foreign_keys = cls.get_foreign_keys()
+
+ while stack:
+ parent, node_struct = stack.pop()
+ # shallow copy of the data structure so it doesn't persist...
+ node_data = node_struct['data'].copy()
+ cls._process_foreign_keys(foreign_keys, node_data)
+ if keep_ids:
+ node_data['id'] = node_struct['id']
+ if parent:
+ node_obj = parent.add_child(**node_data)
+ else:
+ node_obj = cls.add_root(**node_data)
+ added.append(node_obj.pk)
+ if 'children' in node_struct:
+ # extending the stack with the current node as the parent of
+ # the new nodes
+ stack.extend([
+ (node_obj, node)
+ for node in node_struct['children'][::-1]
+ ])
+ return added
+
+ @classmethod
+ def dump_bulk(cls, parent=None, keep_ids=True): # pragma: no cover
+ """
+ Dumps a tree branch to a python data structure.
+
+ :param parent:
+
+ The node whose descendants will be dumped. The node itself will be
+ included in the dump. If not given, the entire tree will be dumped.
+
+ :param keep_ids:
+
+ Stores the id value (primary key) of every node. Enabled by
+ default.
+
+ :returns: A python data structure, described with detail in
+ :meth:`load_bulk`
+ """
+ raise NotImplementedError
+
+ @classmethod
+ def get_root_nodes(cls): # pragma: no cover
+ """:returns: A queryset containing the root nodes in the tree."""
+ raise NotImplementedError
+
+ @classmethod
+ def get_first_root_node(cls):
+ """
+ :returns:
+
+ The first root node in the tree or ``None`` if it is empty.
+ """
+ try:
+ return cls.get_root_nodes()[0]
+ except IndexError:
+ return None
+
+ @classmethod
+ def get_last_root_node(cls):
+ """
+ :returns:
+
+ The last root node in the tree or ``None`` if it is empty.
+ """
+ try:
+ return cls.get_root_nodes().reverse()[0]
+ except IndexError:
+ return None
+
+ @classmethod
+ def find_problems(cls): # pragma: no cover
+ """Checks for problems in the tree structure."""
+ raise NotImplementedError
+
+ @classmethod
+ def fix_tree(cls): # pragma: no cover
+ """
+ Solves problems that can appear when transactions are not used and
+ a piece of code breaks, leaving the tree in an inconsistent state.
+ """
+ raise NotImplementedError
+
+ @classmethod
+ def get_tree(cls, parent=None):
+ """
+ :returns:
+
+ A list of nodes ordered as DFS, including the parent. If
+ no parent is given, the entire tree is returned.
+ """
+ raise NotImplementedError
+
+ @classmethod
+ def get_descendants_group_count(cls, parent=None):
+ """
+ Helper for a very common case: get a group of siblings and the number
+ of *descendants* (not only children) in every sibling.
+
+ :param parent:
+
+ The parent of the siblings to return. If no parent is given, the
+ root nodes will be returned.
+
+ :returns:
+
+ A `list` (**NOT** a Queryset) of node objects with an extra
+ attribute: `descendants_count`.
+ """
+ if parent is None:
+ qset = cls.get_root_nodes()
+ else:
+ qset = parent.get_children()
+ nodes = list(qset)
+ for node in nodes:
+ node.descendants_count = node.get_descendant_count()
+ return nodes
+
+ def get_depth(self): # pragma: no cover
+ """:returns: the depth (level) of the node"""
+ raise NotImplementedError
+
+ def get_siblings(self): # pragma: no cover
+ """
+ :returns:
+
+ A queryset of all the node's siblings, including the node
+ itself.
+ """
+ raise NotImplementedError
+
+ def get_children(self): # pragma: no cover
+ """:returns: A queryset of all the node's children"""
+ raise NotImplementedError
+
+ def get_children_count(self):
+ """:returns: The number of the node's children"""
+ return self.get_children().count()
+
+ def get_descendants(self):
+ """
+ :returns:
+
+ A queryset of all the node's descendants, doesn't
+ include the node itself (some subclasses may return a list).
+ """
+ raise NotImplementedError
+
+ def get_descendant_count(self):
+ """:returns: the number of descendants of a node."""
+ return self.get_descendants().count()
+
+ def get_first_child(self):
+ """
+ :returns:
+
+ The leftmost node's child, or None if it has no children.
+ """
+ try:
+ return self.get_children()[0]
+ except IndexError:
+ return None
+
+ def get_last_child(self):
+ """
+ :returns:
+
+ The rightmost node's child, or None if it has no children.
+ """
+ try:
+ return self.get_children().reverse()[0]
+ except IndexError:
+ return None
+
+ def get_first_sibling(self):
+ """
+ :returns:
+
+ The leftmost node's sibling, can return the node itself if
+ it was the leftmost sibling.
+ """
+ return self.get_siblings()[0]
+
+ def get_last_sibling(self):
+ """
+ :returns:
+
+ The rightmost node's sibling, can return the node itself if
+ it was the rightmost sibling.
+ """
+ return self.get_siblings().reverse()[0]
+
+ def get_prev_sibling(self):
+ """
+ :returns:
+
+ The previous node's sibling, or None if it was the leftmost
+ sibling.
+ """
+ siblings = self.get_siblings()
+ ids = [obj.pk for obj in siblings]
+ if self.pk in ids:
+ idx = ids.index(self.pk)
+ if idx > 0:
+ return siblings[idx - 1]
+
+ def get_next_sibling(self):
+ """
+ :returns:
+
+ The next node's sibling, or None if it was the rightmost
+ sibling.
+ """
+ siblings = self.get_siblings()
+ ids = [obj.pk for obj in siblings]
+ if self.pk in ids:
+ idx = ids.index(self.pk)
+ if idx < len(siblings) - 1:
+ return siblings[idx + 1]
+
+ def is_sibling_of(self, node):
+ """
+ :returns: ``True`` if the node is a sibling of another node given as an
+ argument, else, returns ``False``
+
+ :param node:
+
+ The node that will be checked as a sibling
+ """
+ return self.get_siblings().filter(pk=node.pk).exists()
+
+ def is_child_of(self, node):
+ """
+ :returns: ``True`` if the node is a child of another node given as an
+ argument, else, returns ``False``
+
+ :param node:
+
+ The node that will be checked as a parent
+ """
+ return node.get_children().filter(pk=self.pk).exists()
+
+ def is_descendant_of(self, node): # pragma: no cover
+ """
+ :returns: ``True`` if the node is a descendant of another node given
+ as an argument, else, returns ``False``
+
+ :param node:
+
+ The node that will be checked as an ancestor
+ """
+ raise NotImplementedError
+
+ def add_child(self, **kwargs): # pragma: no cover
+ """
+ Adds a child to the node. The new node will be the new rightmost
+ child. If you want to insert a node at a specific position,
+ use the :meth:`add_sibling` method of an already existing
+ child node instead.
+
+ :param \*\*kwargs:
+
+ Object creation data that will be passed to the inherited Node
+ model
+ :param instance: Instead of passing object creation data, you can
+ pass an already-constructed (but not yet saved) model instance to
+ be inserted into the tree.
+
+ :returns: The created node object. It will be save()d by this method.
+
+ :raise NodeAlreadySaved: when the passed ``instance`` already exists
+ in the database
+ """
+ raise NotImplementedError
+
+ def add_sibling(self, pos=None, **kwargs): # pragma: no cover
+ """
+ Adds a new node as a sibling to the current node object.
+
+
+ :param pos:
+ The position, relative to the current node object, where the
+ new node will be inserted, can be one of:
+
+ - ``first-sibling``: the new node will be the new leftmost sibling
+ - ``left``: the new node will take the node's place, which will be
+ moved to the right 1 position
+ - ``right``: the new node will be inserted at the right of the node
+ - ``last-sibling``: the new node will be the new rightmost sibling
+ - ``sorted-sibling``: the new node will be at the right position
+ according to the value of node_order_by
+
+ :param \*\*kwargs:
+
+ Object creation data that will be passed to the inherited
+ Node model
+ :param instance: Instead of passing object creation data, you can
+ pass an already-constructed (but not yet saved) model instance to
+ be inserted into the tree.
+
+ :returns:
+
+ The created node object. It will be saved by this method.
+
+ :raise InvalidPosition: when passing an invalid ``pos`` parm
+ :raise InvalidPosition: when :attr:`node_order_by` is enabled and the
+ ``pos`` parm wasn't ``sorted-sibling``
+ :raise MissingNodeOrderBy: when passing ``sorted-sibling`` as ``pos``
+ and the :attr:`node_order_by` attribute is missing
+ :raise NodeAlreadySaved: when the passed ``instance`` already exists
+ in the database
+ """
+ raise NotImplementedError
+
+ def get_root(self): # pragma: no cover
+ """:returns: the root node for the current node object."""
+ raise NotImplementedError
+
+ def is_root(self):
+ """:returns: True if the node is a root node (else, returns False)"""
+ return self.get_root().pk == self.pk
+
+ def is_leaf(self):
+ """:returns: True if the node is a leaf node (else, returns False)"""
+ return not self.get_children().exists()
+
+ def get_ancestors(self): # pragma: no cover
+ """
+ :returns:
+
+ A queryset containing the current node object's ancestors,
+ starting by the root node and descending to the parent.
+ (some subclasses may return a list)
+ """
+ raise NotImplementedError
+
+ def get_parent(self, update=False): # pragma: no cover
+ """
+ :returns: the parent node of the current node object.
+ Caches the result in the object itself to help in loops.
+
+ :param update: Updates the cached value.
+ """
+ raise NotImplementedError
+
+ def move(self, target, pos=None): # pragma: no cover
+ """
+ Moves the current node and all it's descendants to a new position
+ relative to another node.
+
+ :param target:
+
+ The node that will be used as a relative child/sibling when moving
+
+ :param pos:
+
+ The position, relative to the target node, where the
+ current node object will be moved to, can be one of:
+
+ - ``first-child``: the node will be the new leftmost child of the
+ ``target`` node
+ - ``last-child``: the node will be the new rightmost child of the
+ ``target`` node
+ - ``sorted-child``: the new node will be moved as a child of the
+ ``target`` node according to the value of :attr:`node_order_by`
+ - ``first-sibling``: the node will be the new leftmost sibling of
+ the ``target`` node
+ - ``left``: the node will take the ``target`` node's place, which
+ will be moved to the right 1 position
+ - ``right``: the node will be moved to the right of the ``target``
+ node
+ - ``last-sibling``: the node will be the new rightmost sibling of
+ the ``target`` node
+ - ``sorted-sibling``: the new node will be moved as a sibling of
+ the ``target`` node according to the value of
+ :attr:`node_order_by`
+
+ .. note::
+
+ If no ``pos`` is given the library will use ``last-sibling``,
+ or ``sorted-sibling`` if :attr:`node_order_by` is enabled.
+
+ :returns: None
+
+ :raise InvalidPosition: when passing an invalid ``pos`` parm
+ :raise InvalidPosition: when :attr:`node_order_by` is enabled and the
+ ``pos`` parm wasn't ``sorted-sibling`` or ``sorted-child``
+ :raise InvalidMoveToDescendant: when trying to move a node to one of
+ it's own descendants
+ :raise PathOverflow: when the library can't make room for the
+ node's new position
+ :raise MissingNodeOrderBy: when passing ``sorted-sibling`` or
+ ``sorted-child`` as ``pos`` and the :attr:`node_order_by`
+ attribute is missing
+ """
+ raise NotImplementedError
+
+ def delete(self):
+ """Removes a node and all it's descendants."""
+ self.__class__.objects.filter(pk=self.pk).delete()
+
+ def _prepare_pos_var(self, pos, method_name, valid_pos, valid_sorted_pos):
+ if pos is None:
+ if self.node_order_by:
+ pos = 'sorted-sibling'
+ else:
+ pos = 'last-sibling'
+ if pos not in valid_pos:
+ raise InvalidPosition('Invalid relative position: %s' % (pos, ))
+ if self.node_order_by and pos not in valid_sorted_pos:
+ raise InvalidPosition(
+ 'Must use %s in %s when node_order_by is enabled' % (
+ ' or '.join(valid_sorted_pos), method_name))
+ if pos in valid_sorted_pos and not self.node_order_by:
+ raise MissingNodeOrderBy('Missing node_order_by attribute.')
+ return pos
+
+ _valid_pos_for_add_sibling = ('first-sibling', 'left', 'right',
+ 'last-sibling', 'sorted-sibling')
+ _valid_pos_for_sorted_add_sibling = ('sorted-sibling',)
+
+ def _prepare_pos_var_for_add_sibling(self, pos):
+ return self._prepare_pos_var(
+ pos,
+ 'add_sibling',
+ self._valid_pos_for_add_sibling,
+ self._valid_pos_for_sorted_add_sibling)
+
+ _valid_pos_for_move = _valid_pos_for_add_sibling + (
+ 'first-child', 'last-child', 'sorted-child')
+ _valid_pos_for_sorted_move = _valid_pos_for_sorted_add_sibling + (
+ 'sorted-child',)
+
+ def _prepare_pos_var_for_move(self, pos):
+ return self._prepare_pos_var(
+ pos,
+ 'move',
+ self._valid_pos_for_move,
+ self._valid_pos_for_sorted_move)
+
+ def get_sorted_pos_queryset(self, siblings, newobj):
+ """
+ :returns: A queryset of the nodes that must be moved
+ to the right. Called only for Node models with :attr:`node_order_by`
+
+ This function is based on _insertion_target_filters from django-mptt
+ (BSD licensed) by Jonathan Buchanan:
+ https://github.com/django-mptt/django-mptt/blob/0.3.0/mptt/signals.py
+ """
+
+ fields, filters = [], []
+ for field in self.node_order_by:
+ value = getattr(newobj, field)
+ filters.append(
+ Q(
+ *[Q(**{f: v}) for f, v in fields] +
+ [Q(**{'%s__gt' % field: value})]
+ )
+ )
+ fields.append((field, value))
+ return siblings.filter(reduce(operator.or_, filters))
+
+ @classmethod
+ def get_annotated_list_qs(cls, qs):
+ """
+ Gets an annotated list from a queryset.
+ """
+ result, info = [], {}
+ start_depth, prev_depth = (None, None)
+ for node in qs:
+ depth = node.get_depth()
+ if start_depth is None:
+ start_depth = depth
+ open = (depth and (prev_depth is None or depth > prev_depth))
+ if prev_depth is not None and depth < prev_depth:
+ info['close'] = list(range(0, prev_depth - depth))
+ info = {'open': open, 'close': [], 'level': depth - start_depth}
+ result.append((node, info,))
+ prev_depth = depth
+ if start_depth and start_depth > 0:
+ info['close'] = list(range(0, prev_depth - start_depth + 1))
+ return result
+
+ @classmethod
+ def get_annotated_list(cls, parent=None, max_depth=None):
+ """
+ Gets an annotated list from a tree branch.
+
+ :param parent:
+
+ The node whose descendants will be annotated. The node itself
+ will be included in the list. If not given, the entire tree
+ will be annotated.
+
+ :param max_depth:
+
+ Optionally limit to specified depth
+ """
+
+ result, info = [], {}
+ start_depth, prev_depth = (None, None)
+ qs = cls.get_tree(parent)
+ if max_depth:
+ qs = qs.filter(depth__lte=max_depth)
+ return cls.get_annotated_list_qs(qs)
+
+ @classmethod
+ def _get_serializable_model(cls):
+ """
+ Returns a model with a valid _meta.local_fields (serializable).
+
+ Basically, this means the original model, not a proxied model.
+
+ (this is a workaround for a bug in django)
+ """
+ current_class = cls
+ while current_class._meta.proxy:
+ current_class = current_class._meta.proxy_for_model
+ return current_class
+
+ @classmethod
+ def _get_database_connection(cls, action):
+ return {
+ 'read': connections[router.db_for_read(cls)],
+ 'write': connections[router.db_for_write(cls)]
+ }[action]
+
+ @classmethod
+ def get_database_vendor(cls, action):
+ """
+ returns the supported database vendor used by a treebeard model when
+ performing read (select) or write (update, insert, delete) operations.
+
+ :param action:
+
+ `read` or `write`
+
+ :returns: postgresql, mysql or sqlite
+ """
+ return cls._get_database_connection(action).vendor
+
+ @classmethod
+ def _get_database_cursor(cls, action):
+ return cls._get_database_connection(action).cursor()
+
+ class Meta:
+ """Abstract model."""
+ abstract = True
diff --git a/vendor/treebeard/mp_tree.py b/vendor/treebeard/mp_tree.py
new file mode 100644
index 00000000..d3342557
--- /dev/null
+++ b/vendor/treebeard/mp_tree.py
@@ -0,0 +1,1152 @@
+"""Materialized Path Trees"""
+
+import sys
+import operator
+
+if sys.version_info >= (3, 0):
+ from functools import reduce
+
+from django.core import serializers
+from django.db import models, transaction, connection
+from django.db.models import F, Q
+from django.utils.translation import gettext_noop as _
+
+from treebeard.numconv import NumConv
+from treebeard.models import Node
+from treebeard.exceptions import InvalidMoveToDescendant, PathOverflow,\
+ NodeAlreadySaved
+
+
+# The following functions generate vendor-specific SQL functions
+def sql_concat(*args, **kwargs):
+ vendor = kwargs.pop('vendor', None)
+ if vendor == 'mysql':
+ return 'CONCAT({})'.format(', '.join(args))
+ if vendor == 'microsoft':
+ return ' + '.join(args)
+ return '||'.join(args)
+
+
+def sql_length(field, vendor=None):
+ if vendor == 'microsoft':
+ return 'LEN({})'.format(field)
+ return 'LENGTH({})'.format(field)
+
+
+def sql_substr(field, pos, length=None, **kwargs):
+ vendor = kwargs.pop('vendor', None)
+ function = 'SUBSTR({field}, {pos})'
+ if length:
+ function = 'SUBSTR({field}, {pos}, {length})'
+ if vendor == 'microsoft':
+ if not length:
+ length = 'LEN({})'.format(field)
+ function = 'SUBSTRING({field}, {pos}, {length})'
+ return function.format(field=field, pos=pos, length=length)
+
+
+def get_result_class(cls):
+ """
+ For the given model class, determine what class we should use for the
+ nodes returned by its tree methods (such as get_children).
+
+ Usually this will be trivially the same as the initial model class,
+ but there are special cases when model inheritance is in use:
+
+ * If the model extends another via multi-table inheritance, we need to
+ use whichever ancestor originally implemented the tree behaviour (i.e.
+ the one which defines the 'path' field). We can't use the
+ subclass, because it's not guaranteed that the other nodes reachable
+ from the current one will be instances of the same subclass.
+
+ * If the model is a proxy model, the returned nodes should also use
+ the proxy class.
+ """
+ base_class = cls._meta.get_field('path').model
+ if cls._meta.proxy_for_model == base_class:
+ return cls
+ else:
+ return base_class
+
+
+class MP_NodeQuerySet(models.query.QuerySet):
+ """
+ Custom queryset for the tree node manager.
+
+ Needed only for the custom delete method.
+ """
+
+ def delete(self):
+ """
+ Custom delete method, will remove all descendant nodes to ensure a
+ consistent tree (no orphans)
+
+ :returns: ``None``
+ """
+ # we'll have to manually run through all the nodes that are going
+ # to be deleted and remove nodes from the list if an ancestor is
+ # already getting removed, since that would be redundant
+ removed = {}
+ for node in self.order_by('depth', 'path'):
+ found = False
+ for depth in range(1, int(len(node.path) / node.steplen)):
+ path = node._get_basepath(node.path, depth)
+ if path in removed:
+ # we are already removing a parent of this node
+ # skip
+ found = True
+ break
+ if not found:
+ removed[node.path] = node
+
+ # ok, got the minimal list of nodes to remove...
+ # we must also remove their children
+ # and update every parent node's numchild attribute
+ # LOTS OF FUN HERE!
+ parents = {}
+ toremove = []
+ for path, node in removed.items():
+ parentpath = node._get_basepath(node.path, node.depth - 1)
+ if parentpath:
+ if parentpath not in parents:
+ parents[parentpath] = node.get_parent(True)
+ parent = parents[parentpath]
+ if parent and parent.numchild > 0:
+ parent.numchild -= 1
+ parent.save()
+ if node.is_leaf():
+ toremove.append(Q(path=node.path))
+ else:
+ toremove.append(Q(path__startswith=node.path))
+
+ # Django will handle this as a SELECT and then a DELETE of
+ # ids, and will deal with removing related objects
+ if toremove:
+ qset = get_result_class(self.model).objects.filter(reduce(operator.or_, toremove))
+ super(MP_NodeQuerySet, qset).delete()
+
+
+class MP_NodeManager(models.Manager):
+ """Custom manager for nodes in a Materialized Path tree."""
+
+ def get_queryset(self):
+ """Sets the custom queryset as the default."""
+ return MP_NodeQuerySet(self.model).order_by('path')
+
+
+class MP_AddHandler(object):
+ def __init__(self):
+ self.stmts = []
+
+
+class MP_ComplexAddMoveHandler(MP_AddHandler):
+
+ def run_sql_stmts(self):
+ cursor = self.node_cls._get_database_cursor('write')
+ for sql, vals in self.stmts:
+ cursor.execute(sql, vals)
+
+ def get_sql_update_numchild(self, path, incdec='inc'):
+ """:returns: The sql needed the numchild value of a node"""
+ sql = "UPDATE %s SET numchild=numchild%s1"\
+ " WHERE path=%%s" % (
+ connection.ops.quote_name(
+ get_result_class(self.node_cls)._meta.db_table),
+ {'inc': '+', 'dec': '-'}[incdec])
+ vals = [path]
+ return sql, vals
+
+ def reorder_nodes_before_add_or_move(self, pos, newpos, newdepth, target,
+ siblings, oldpath=None,
+ movebranch=False):
+ """
+ Handles the reordering of nodes and branches when adding/moving
+ nodes.
+
+ :returns: A tuple containing the old path and the new path.
+ """
+ if (
+ (pos == 'last-sibling') or
+ (pos == 'right' and target == target.get_last_sibling())
+ ):
+ # easy, the last node
+ last = target.get_last_sibling()
+ newpath = last._inc_path()
+ if movebranch:
+ self.stmts.append(
+ self.get_sql_newpath_in_branches(oldpath, newpath))
+ else:
+ # do the UPDATE dance
+
+ if newpos is None:
+ siblings = target.get_siblings()
+ siblings = {'left': siblings.filter(path__gte=target.path),
+ 'right': siblings.filter(path__gt=target.path),
+ 'first-sibling': siblings}[pos]
+ basenum = target._get_lastpos_in_path()
+ newpos = {'first-sibling': 1,
+ 'left': basenum,
+ 'right': basenum + 1}[pos]
+
+ newpath = self.node_cls._get_path(target.path, newdepth, newpos)
+
+ # If the move is amongst siblings and is to the left and there
+ # are siblings to the right of its new position then to be on
+ # the safe side we temporarily dump it on the end of the list
+ tempnewpath = None
+ if movebranch and len(oldpath) == len(newpath):
+ parentoldpath = self.node_cls._get_basepath(
+ oldpath,
+ int(len(oldpath) / self.node_cls.steplen) - 1
+ )
+ parentnewpath = self.node_cls._get_basepath(
+ newpath, newdepth - 1)
+ if (
+ parentoldpath == parentnewpath and
+ siblings and
+ newpath < oldpath
+ ):
+ last = target.get_last_sibling()
+ basenum = last._get_lastpos_in_path()
+ tempnewpath = self.node_cls._get_path(
+ newpath, newdepth, basenum + 2)
+ self.stmts.append(
+ self.get_sql_newpath_in_branches(
+ oldpath, tempnewpath))
+
+ # Optimisation to only move siblings which need moving
+ # (i.e. if we've got holes, allow them to compress)
+ movesiblings = []
+ priorpath = newpath
+ for node in siblings:
+ # If the path of the node is already greater than the path
+ # of the previous node it doesn't need shifting
+ if node.path > priorpath:
+ break
+ # It does need shifting, so add to the list
+ movesiblings.append(node)
+ # Calculate the path that it would be moved to, as that's
+ # the next "priorpath"
+ priorpath = node._inc_path()
+ movesiblings.reverse()
+
+ for node in movesiblings:
+ # moving the siblings (and their branches) at the right of the
+ # related position one step to the right
+ sql, vals = self.get_sql_newpath_in_branches(
+ node.path, node._inc_path())
+ self.stmts.append((sql, vals))
+
+ if movebranch:
+ if oldpath.startswith(node.path):
+ # if moving to a parent, update oldpath since we just
+ # increased the path of the entire branch
+ oldpath = vals[0] + oldpath[len(vals[0]):]
+ if target.path.startswith(node.path):
+ # and if we moved the target, update the object
+ # django made for us, since the update won't do it
+ # maybe useful in loops
+ target.path = vals[0] + target.path[len(vals[0]):]
+ if movebranch:
+ # node to move
+ if tempnewpath:
+ self.stmts.append(
+ self.get_sql_newpath_in_branches(
+ tempnewpath, newpath))
+ else:
+ self.stmts.append(
+ self.get_sql_newpath_in_branches(
+ oldpath, newpath))
+ return oldpath, newpath
+
+ def get_sql_newpath_in_branches(self, oldpath, newpath):
+ """
+ :returns: The sql needed to move a branch to another position.
+
+ .. note::
+
+ The generated sql will only update the depth values if needed.
+
+ """
+
+ vendor = self.node_cls.get_database_vendor('write')
+ sql1 = "UPDATE %s SET" % (
+ connection.ops.quote_name(
+ get_result_class(self.node_cls)._meta.db_table), )
+
+ # <3 "standard" sql
+ if vendor == 'sqlite':
+ # I know that the third argument in SUBSTR (LENGTH(path)) is
+ # awful, but sqlite fails without it:
+ # OperationalError: wrong number of arguments to function substr()
+ # even when the documentation says that 2 arguments are valid:
+ # http://www.sqlite.org/lang_corefunc.html
+ sqlpath = "%s||SUBSTR(path, %s, LENGTH(path))"
+ elif vendor == 'mysql':
+ # hooray for mysql ignoring standards in their default
+ # configuration!
+ # to make || work as it should, enable ansi mode
+ # http://dev.mysql.com/doc/refman/5.0/en/ansi-mode.html
+ sqlpath = "CONCAT(%s, SUBSTR(path, %s))"
+ else:
+ sqlpath = sql_concat("%s", sql_substr("path", "%s", vendor=vendor), vendor=vendor)
+
+ sql2 = ["path=%s" % (sqlpath, )]
+ vals = [newpath, len(oldpath) + 1]
+ if len(oldpath) != len(newpath) and vendor != 'mysql':
+ # when using mysql, this won't update the depth and it has to be
+ # done in another query
+ # doesn't even work with sql_mode='ANSI,TRADITIONAL'
+ # TODO: FIND OUT WHY?!?? right now I'm just blaming mysql
+ sql2.append(("depth=" + sql_length("%s", vendor=vendor) + "/%%s") % (sqlpath, ))
+ vals.extend([newpath, len(oldpath) + 1, self.node_cls.steplen])
+ sql3 = "WHERE path LIKE %s"
+ vals.extend([oldpath + '%'])
+ sql = '%s %s %s' % (sql1, ', '.join(sql2), sql3)
+ return sql, vals
+
+
+class MP_AddRootHandler(MP_AddHandler):
+ def __init__(self, cls, **kwargs):
+ super(MP_AddRootHandler, self).__init__()
+ self.cls = cls
+ self.kwargs = kwargs
+
+ def process(self):
+
+ # do we have a root node already?
+ last_root = self.cls.get_last_root_node()
+
+ if last_root and last_root.node_order_by:
+ # there are root nodes and node_order_by has been set
+ # delegate sorted insertion to add_sibling
+ return last_root.add_sibling('sorted-sibling', **self.kwargs)
+
+ if last_root:
+ # adding the new root node as the last one
+ newpath = last_root._inc_path()
+ else:
+ # adding the first root node
+ newpath = self.cls._get_path(None, 1, 1)
+
+ if len(self.kwargs) == 1 and 'instance' in self.kwargs:
+ # adding the passed (unsaved) instance to the tree
+ newobj = self.kwargs['instance']
+ if newobj.pk:
+ raise NodeAlreadySaved("Attempted to add a tree node that is "\
+ "already in the database")
+ else:
+ # creating the new object
+ newobj = self.cls(**self.kwargs)
+
+ newobj.depth = 1
+ newobj.path = newpath
+ # saving the instance before returning it
+ newobj.save()
+ return newobj
+
+
+class MP_AddChildHandler(MP_AddHandler):
+ def __init__(self, node, **kwargs):
+ super(MP_AddChildHandler, self).__init__()
+ self.node = node
+ self.node_cls = node.__class__
+ self.kwargs = kwargs
+
+ def process(self):
+ if self.node_cls.node_order_by and not self.node.is_leaf():
+ # there are child nodes and node_order_by has been set
+ # delegate sorted insertion to add_sibling
+ self.node.numchild += 1
+ return self.node.get_last_child().add_sibling(
+ 'sorted-sibling', **self.kwargs)
+
+ if len(self.kwargs) == 1 and 'instance' in self.kwargs:
+ # adding the passed (unsaved) instance to the tree
+ newobj = self.kwargs['instance']
+ if newobj.pk:
+ raise NodeAlreadySaved("Attempted to add a tree node that is "\
+ "already in the database")
+ else:
+ # creating a new object
+ newobj = self.node_cls(**self.kwargs)
+
+ newobj.depth = self.node.depth + 1
+ if self.node.is_leaf():
+ # the node had no children, adding the first child
+ newobj.path = self.node_cls._get_path(
+ self.node.path, newobj.depth, 1)
+ max_length = self.node_cls._meta.get_field('path').max_length
+ if len(newobj.path) > max_length:
+ raise PathOverflow(
+ _('The new node is too deep in the tree, try'
+ ' increasing the path.max_length property'
+ ' and UPDATE your database'))
+ else:
+ # adding the new child as the last one
+ newobj.path = self.node.get_last_child()._inc_path()
+ # saving the instance before returning it
+ newobj.save()
+ newobj._cached_parent_obj = self.node
+
+ get_result_class(self.node_cls).objects.filter(
+ path=self.node.path).update(numchild=F('numchild')+1)
+
+ # we increase the numchild value of the object in memory
+ self.node.numchild += 1
+ return newobj
+
+
+class MP_AddSiblingHandler(MP_ComplexAddMoveHandler):
+ def __init__(self, node, pos, **kwargs):
+ super(MP_AddSiblingHandler, self).__init__()
+ self.node = node
+ self.node_cls = node.__class__
+ self.pos = pos
+ self.kwargs = kwargs
+
+ def process(self):
+ self.pos = self.node._prepare_pos_var_for_add_sibling(self.pos)
+
+ if len(self.kwargs) == 1 and 'instance' in self.kwargs:
+ # adding the passed (unsaved) instance to the tree
+ newobj = self.kwargs['instance']
+ if newobj.pk:
+ raise NodeAlreadySaved("Attempted to add a tree node that is "\
+ "already in the database")
+ else:
+ # creating a new object
+ newobj = self.node_cls(**self.kwargs)
+
+ newobj.depth = self.node.depth
+
+ if self.pos == 'sorted-sibling':
+ siblings = self.node.get_sorted_pos_queryset(
+ self.node.get_siblings(), newobj)
+ try:
+ newpos = siblings.all()[0]._get_lastpos_in_path()
+ except IndexError:
+ newpos = None
+ if newpos is None:
+ self.pos = 'last-sibling'
+ else:
+ newpos, siblings = None, []
+
+ _, newpath = self.reorder_nodes_before_add_or_move(
+ self.pos, newpos, self.node.depth, self.node, siblings, None,
+ False)
+
+ parentpath = self.node._get_basepath(newpath, self.node.depth - 1)
+ if parentpath:
+ self.stmts.append(
+ self.get_sql_update_numchild(parentpath, 'inc'))
+
+ self.run_sql_stmts()
+
+ # saving the instance before returning it
+ newobj.path = newpath
+ newobj.save()
+
+ return newobj
+
+
+class MP_MoveHandler(MP_ComplexAddMoveHandler):
+ def __init__(self, node, target, pos=None):
+ super(MP_MoveHandler, self).__init__()
+ self.node = node
+ self.node_cls = node.__class__
+ self.target = target
+ self.pos = pos
+
+ def process(self):
+
+ self.pos = self.node._prepare_pos_var_for_move(self.pos)
+
+ oldpath = self.node.path
+
+ # initialize variables and if moving to a child, updates "move to
+ # child" to become a "move to sibling" if possible (if it can't
+ # be done, it means that we are adding the first child)
+ newdepth, siblings, newpos = self.update_move_to_child_vars()
+
+ if self.target.is_descendant_of(self.node):
+ raise InvalidMoveToDescendant(
+ _("Can't move node to a descendant."))
+
+ if (
+ oldpath == self.target.path and
+ (
+ (self.pos == 'left') or
+ (
+ self.pos in ('right', 'last-sibling') and
+ self.target.path == self.target.get_last_sibling().path
+ ) or
+ (
+ self.pos == 'first-sibling' and
+ self.target.path == self.target.get_first_sibling().path
+ )
+ )
+ ):
+ # special cases, not actually moving the node so no need to UPDATE
+ return
+
+ if self.pos == 'sorted-sibling':
+ siblings = self.node.get_sorted_pos_queryset(
+ self.target.get_siblings(), self.node)
+ try:
+ newpos = siblings.all()[0]._get_lastpos_in_path()
+ except IndexError:
+ newpos = None
+ if newpos is None:
+ self.pos = 'last-sibling'
+
+ # generate the sql that will do the actual moving of nodes
+ oldpath, newpath = self.reorder_nodes_before_add_or_move(
+ self.pos, newpos, newdepth, self.target, siblings, oldpath, True)
+ # updates needed for mysql and children count in parents
+ self.sanity_updates_after_move(oldpath, newpath)
+
+ self.run_sql_stmts()
+
+ def sanity_updates_after_move(self, oldpath, newpath):
+ """
+ Updates the list of sql statements needed after moving nodes.
+
+ 1. :attr:`depth` updates *ONLY* needed by mysql databases (*sigh*)
+ 2. update the number of children of parent nodes
+ """
+ if (
+ self.node_cls.get_database_vendor('write') == 'mysql' and
+ len(oldpath) != len(newpath)
+ ):
+ # no words can describe how dumb mysql is
+ # we must update the depth of the branch in a different query
+ self.stmts.append(
+ self.get_mysql_update_depth_in_branch(newpath))
+
+ oldparentpath = self.node_cls._get_parent_path_from_path(oldpath)
+ newparentpath = self.node_cls._get_parent_path_from_path(newpath)
+ if (
+ (not oldparentpath and newparentpath) or
+ (oldparentpath and not newparentpath) or
+ (oldparentpath != newparentpath)
+ ):
+ # node changed parent, updating count
+ if oldparentpath:
+ self.stmts.append(
+ self.get_sql_update_numchild(oldparentpath, 'dec'))
+ if newparentpath:
+ self.stmts.append(
+ self.get_sql_update_numchild(newparentpath, 'inc'))
+
+ def update_move_to_child_vars(self):
+ """Update preliminar vars in :meth:`move` when moving to a child"""
+ newdepth = self.target.depth
+ newpos = None
+ siblings = []
+ if self.pos in ('first-child', 'last-child', 'sorted-child'):
+ # moving to a child
+ parent = self.target
+ newdepth += 1
+ if self.target.is_leaf():
+ # moving as a target's first child
+ newpos = 1
+ self.pos = 'first-sibling'
+ siblings = get_result_class(self.node_cls).objects.none()
+ else:
+ self.target = self.target.get_last_child()
+ self.pos = {
+ 'first-child': 'first-sibling',
+ 'last-child': 'last-sibling',
+ 'sorted-child': 'sorted-sibling'}[self.pos]
+
+ # this is not for save(), since if needed, will be handled with a
+ # custom UPDATE, this is only here to update django's object,
+ # should be useful in loops
+ parent.numchild += 1
+
+ return newdepth, siblings, newpos
+
+ def get_mysql_update_depth_in_branch(self, path):
+ """
+ :returns: The sql needed to update the depth of all the nodes in a
+ branch.
+ """
+ vendor = self.node_cls.get_database_vendor('write')
+ sql = ("UPDATE %s SET depth=" + sql_length("path", vendor=vendor) + "/%%s WHERE path LIKE %%s") % (
+ connection.ops.quote_name(
+ get_result_class(self.node_cls)._meta.db_table), )
+ vals = [self.node_cls.steplen, path + '%']
+ return sql, vals
+
+
+class MP_Node(Node):
+ """Abstract model to create your own Materialized Path Trees."""
+
+ steplen = 4
+ alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+ node_order_by = []
+ path = models.CharField(max_length=255, unique=True)
+ depth = models.PositiveIntegerField()
+ numchild = models.PositiveIntegerField(default=0)
+ gap = 1
+
+ objects = MP_NodeManager()
+
+ numconv_obj_ = None
+
+ @classmethod
+ def _int2str(cls, num):
+ return cls.numconv_obj().int2str(num)
+
+ @classmethod
+ def _str2int(cls, num):
+ return cls.numconv_obj().str2int(num)
+
+ @classmethod
+ def numconv_obj(cls):
+ if cls.numconv_obj_ is None:
+ cls.numconv_obj_ = NumConv(len(cls.alphabet), cls.alphabet)
+ return cls.numconv_obj_
+
+ @classmethod
+ def add_root(cls, **kwargs):
+ """
+ Adds a root node to the tree.
+
+ This method saves the node in database. The object is populated as if via:
+
+ ```
+ obj = cls(**kwargs)
+ ```
+
+ :raise PathOverflow: when no more root objects can be added
+ """
+ return MP_AddRootHandler(cls, **kwargs).process()
+
+ @classmethod
+ def dump_bulk(cls, parent=None, keep_ids=True):
+ """Dumps a tree branch to a python data structure."""
+
+ cls = get_result_class(cls)
+
+ # Because of fix_tree, this method assumes that the depth
+ # and numchild properties in the nodes can be incorrect,
+ # so no helper methods are used
+ qset = cls._get_serializable_model().objects.all()
+ if parent:
+ qset = qset.filter(path__startswith=parent.path)
+ ret, lnk = [], {}
+ for pyobj in serializers.serialize('python', qset):
+ # django's serializer stores the attributes in 'fields'
+ fields = pyobj['fields']
+ path = fields['path']
+ depth = int(len(path) / cls.steplen)
+ # this will be useless in load_bulk
+ del fields['depth']
+ del fields['path']
+ del fields['numchild']
+ if 'id' in fields:
+ # this happens immediately after a load_bulk
+ del fields['id']
+
+ newobj = {'data': fields}
+ if keep_ids:
+ newobj['id'] = pyobj['pk']
+
+ if (not parent and depth == 1) or\
+ (parent and len(path) == len(parent.path)):
+ ret.append(newobj)
+ else:
+ parentpath = cls._get_basepath(path, depth - 1)
+ parentobj = lnk[parentpath]
+ if 'children' not in parentobj:
+ parentobj['children'] = []
+ parentobj['children'].append(newobj)
+ lnk[path] = newobj
+ return ret
+
+ @classmethod
+ def find_problems(cls):
+ """
+ Checks for problems in the tree structure, problems can occur when:
+
+ 1. your code breaks and you get incomplete transactions (always
+ use transactions!)
+ 2. changing the ``steplen`` value in a model (you must
+ :meth:`dump_bulk` first, change ``steplen`` and then
+ :meth:`load_bulk`
+
+ :returns: A tuple of five lists:
+
+ 1. a list of ids of nodes with characters not found in the
+ ``alphabet``
+ 2. a list of ids of nodes when a wrong ``path`` length
+ according to ``steplen``
+ 3. a list of ids of orphaned nodes
+ 4. a list of ids of nodes with the wrong depth value for
+ their path
+ 5. a list of ids nodes that report a wrong number of children
+ """
+ cls = get_result_class(cls)
+ vendor = cls.get_database_vendor('write')
+
+ evil_chars, bad_steplen, orphans = [], [], []
+ wrong_depth, wrong_numchild = [], []
+ for node in cls.objects.all():
+ found_error = False
+ for char in node.path:
+ if char not in cls.alphabet:
+ evil_chars.append(node.pk)
+ found_error = True
+ break
+ if found_error:
+ continue
+ if len(node.path) % cls.steplen:
+ bad_steplen.append(node.pk)
+ continue
+ try:
+ node.get_parent(True)
+ except cls.DoesNotExist:
+ orphans.append(node.pk)
+ continue
+
+ if node.depth != int(len(node.path) / cls.steplen):
+ wrong_depth.append(node.pk)
+ continue
+
+ real_numchild = cls.objects.filter(
+ path__range=cls._get_children_path_interval(node.path)
+ ).extra(
+ where=[(sql_length("path", vendor=vendor) + '/%d=%d') % (cls.steplen, node.depth + 1)]
+ ).count()
+ if real_numchild != node.numchild:
+ wrong_numchild.append(node.pk)
+ continue
+
+ return evil_chars, bad_steplen, orphans, wrong_depth, wrong_numchild
+
+ @classmethod
+ def fix_tree(cls, destructive=False):
+ """
+ Solves some problems that can appear when transactions are not used and
+ a piece of code breaks, leaving the tree in an inconsistent state.
+
+ The problems this method solves are:
+
+ 1. Nodes with an incorrect ``depth`` or ``numchild`` values due to
+ incorrect code and lack of database transactions.
+ 2. "Holes" in the tree. This is normal if you move/delete nodes a
+ lot. Holes in a tree don't affect performance,
+ 3. Incorrect ordering of nodes when ``node_order_by`` is enabled.
+ Ordering is enforced on *node insertion*, so if an attribute in
+ ``node_order_by`` is modified after the node is inserted, the
+ tree ordering will be inconsistent.
+
+ :param destructive:
+
+ A boolean value. If True, a more agressive fix_tree method will be
+ attempted. If False (the default), it will use a safe (and fast!)
+ fix approach, but it will only solve the ``depth`` and
+ ``numchild`` nodes, it won't fix the tree holes or broken path
+ ordering.
+
+ .. warning::
+
+ Currently what the ``destructive`` method does is:
+
+ 1. Backup the tree with :meth:`dump_data`
+ 2. Remove all nodes in the tree.
+ 3. Restore the tree with :meth:`load_data`
+
+ So, even when the primary keys of your nodes will be preserved,
+ this method isn't foreign-key friendly. That needs complex
+ in-place tree reordering, not available at the moment (hint:
+ patches are welcome).
+ """
+ cls = get_result_class(cls)
+ vendor = cls.get_database_vendor('write')
+
+ if destructive:
+ dump = cls.dump_bulk(None, True)
+ cls.objects.all().delete()
+ cls.load_bulk(dump, None, True)
+ else:
+ cursor = cls._get_database_cursor('write')
+
+ # fix the depth field
+ # we need the WHERE to speed up postgres
+ sql = (
+ "UPDATE %s "
+ "SET depth=" + sql_length("path", vendor=vendor) + "/%%s "
+ "WHERE depth!=" + sql_length("path", vendor=vendor) + "/%%s"
+ ) % (connection.ops.quote_name(cls._meta.db_table), )
+ vals = [cls.steplen, cls.steplen]
+ cursor.execute(sql, vals)
+
+ # fix the numchild field
+ vals = ['_' * cls.steplen]
+ # the cake and sql portability are a lie
+ if cls.get_database_vendor('read') == 'mysql':
+ sql = (
+ "SELECT tbn1.path, tbn1.numchild, ("
+ "SELECT COUNT(1) "
+ "FROM %(table)s AS tbn2 "
+ "WHERE tbn2.path LIKE " +
+ sql_concat("tbn1.path", "%%s", vendor=vendor) + ") AS real_numchild "
+ "FROM %(table)s AS tbn1 "
+ "HAVING tbn1.numchild != real_numchild"
+ ) % {'table': connection.ops.quote_name(cls._meta.db_table)}
+ else:
+ subquery = "(SELECT COUNT(1) FROM %(table)s AS tbn2"\
+ " WHERE tbn2.path LIKE " + sql_concat("tbn1.path", "%%s", vendor=vendor) + ")"
+ sql = ("SELECT tbn1.path, tbn1.numchild, " + subquery +
+ " FROM %(table)s AS tbn1 WHERE tbn1.numchild != " +
+ subquery)
+ sql = sql % {
+ 'table': connection.ops.quote_name(cls._meta.db_table)}
+ # we include the subquery twice
+ vals *= 2
+ cursor.execute(sql, vals)
+ sql = "UPDATE %(table)s "\
+ "SET numchild=%%s "\
+ "WHERE path=%%s" % {
+ 'table': connection.ops.quote_name(cls._meta.db_table)}
+ for node_data in cursor.fetchall():
+ vals = [node_data[2], node_data[0]]
+ cursor.execute(sql, vals)
+
+
+ @classmethod
+ def get_tree(cls, parent=None):
+ """
+ :returns:
+
+ A *queryset* of nodes ordered as DFS, including the parent.
+ If no parent is given, the entire tree is returned.
+ """
+ cls = get_result_class(cls)
+
+ if parent is None:
+ # return the entire tree
+ return cls.objects.all()
+ if parent.is_leaf():
+ return cls.objects.filter(pk=parent.pk)
+ return cls.objects.filter(
+ path__startswith=parent.path,
+ depth__gte=parent.depth
+ ).order_by(
+ 'path'
+ )
+
+ @classmethod
+ def get_root_nodes(cls):
+ """:returns: A queryset containing the root nodes in the tree."""
+ return get_result_class(cls).objects.filter(depth=1).order_by('path')
+
+ @classmethod
+ def get_descendants_group_count(cls, parent=None):
+ """
+ Helper for a very common case: get a group of siblings and the number
+ of *descendants* in every sibling.
+ """
+
+ #~
+ # disclaimer: this is the FOURTH implementation I wrote for this
+ # function. I really tried to make it return a queryset, but doing so
+ # with a *single* query isn't trivial with Django's ORM.
+
+ # ok, I DID manage to make Django's ORM return a queryset here,
+ # defining two querysets, passing one subquery in the tables parameters
+ # of .extra() of the second queryset, using the undocumented order_by
+ # feature, and using a HORRIBLE hack to avoid django quoting the
+ # subquery as a table, BUT (and there is always a but) the hack didn't
+ # survive turning the QuerySet into a ValuesQuerySet, so I just used
+ # good old SQL.
+ # NOTE: in case there is interest, the hack to avoid django quoting the
+ # subquery as a table, was adding the subquery to the alias cache of
+ # the queryset's query object:
+ #
+ # qset.query.quote_cache[subquery] = subquery
+ #
+ # If there is a better way to do this in an UNMODIFIED django 1.0, let
+ # me know.
+ #~
+
+ cls = get_result_class(cls)
+ vendor = cls.get_database_vendor('write')
+
+ if parent:
+ depth = parent.depth + 1
+ params = cls._get_children_path_interval(parent.path)
+ extrand = 'AND path BETWEEN %s AND %s'
+ else:
+ depth = 1
+ params = []
+ extrand = ''
+
+ subpath = sql_substr("path", "1", "%(subpathlen)s", vendor=vendor)
+
+ sql = (
+ 'SELECT * FROM %(table)s AS t1 INNER JOIN '
+ ' (SELECT '
+ ' ' + subpath + ' AS subpath, '
+ ' COUNT(1)-1 AS count '
+ ' FROM %(table)s '
+ ' WHERE depth >= %(depth)s %(extrand)s'
+ ' GROUP BY '+ subpath + ') AS t2 '
+ ' ON t1.path=t2.subpath '
+ ' ORDER BY t1.path'
+ ) % {
+ 'table': connection.ops.quote_name(cls._meta.db_table),
+ 'subpathlen': depth * cls.steplen,
+ 'depth': depth,
+ 'extrand': extrand}
+ cursor = cls._get_database_cursor('write')
+ cursor.execute(sql, params)
+
+ ret = []
+ field_names = [field[0] for field in cursor.description]
+ for node_data in cursor.fetchall():
+ node = cls(**dict(zip(field_names, node_data[:-2])))
+ node.descendants_count = node_data[-1]
+ ret.append(node)
+ return ret
+
+ def get_depth(self):
+ """:returns: the depth (level) of the node"""
+ return self.depth
+
+ def get_siblings(self):
+ """
+ :returns: A queryset of all the node's siblings, including the node
+ itself.
+ """
+ qset = get_result_class(self.__class__).objects.filter(
+ depth=self.depth
+ ).order_by(
+ 'path'
+ )
+ if self.depth > 1:
+ # making sure the non-root nodes share a parent
+ parentpath = self._get_basepath(self.path, self.depth - 1)
+ qset = qset.filter(
+ path__range=self._get_children_path_interval(parentpath))
+ return qset
+
+ def get_children(self):
+ """:returns: A queryset of all the node's children"""
+ if self.is_leaf():
+ return get_result_class(self.__class__).objects.none()
+ return get_result_class(self.__class__).objects.filter(
+ depth=self.depth + 1,
+ path__range=self._get_children_path_interval(self.path)
+ ).order_by(
+ 'path'
+ )
+
+ def get_next_sibling(self):
+ """
+ :returns: The next node's sibling, or None if it was the rightmost
+ sibling.
+ """
+ try:
+ return self.get_siblings().filter(path__gt=self.path)[0]
+ except IndexError:
+ return None
+
+ def get_descendants(self):
+ """
+ :returns: A queryset of all the node's descendants as DFS, doesn't
+ include the node itself
+ """
+ return self.__class__.get_tree(self).exclude(pk=self.pk)
+
+ def get_prev_sibling(self):
+ """
+ :returns: The previous node's sibling, or None if it was the leftmost
+ sibling.
+ """
+ try:
+ return self.get_siblings().filter(path__lt=self.path).reverse()[0]
+ except IndexError:
+ return None
+
+ def get_children_count(self):
+ """
+ :returns: The number the node's children, calculated in the most
+ efficient possible way.
+ """
+ return self.numchild
+
+ def is_sibling_of(self, node):
+ """
+ :returns: ``True`` if the node is a sibling of another node given as an
+ argument, else, returns ``False``
+ """
+ aux = self.depth == node.depth
+ # Check non-root nodes share a parent only if they have the same depth
+ if aux and self.depth > 1:
+ # making sure the non-root nodes share a parent
+ parentpath = self._get_basepath(self.path, self.depth - 1)
+ return aux and node.path.startswith(parentpath)
+ return aux
+
+ def is_child_of(self, node):
+ """
+ :returns: ``True`` is the node if a child of another node given as an
+ argument, else, returns ``False``
+ """
+ return (self.path.startswith(node.path) and
+ self.depth == node.depth + 1)
+
+ def is_descendant_of(self, node):
+ """
+ :returns: ``True`` if the node is a descendant of another node given
+ as an argument, else, returns ``False``
+ """
+ return self.path.startswith(node.path) and self.depth > node.depth
+
+ def add_child(self, **kwargs):
+ """
+ Adds a child to the node.
+
+ This method saves the node in database. The object is populated as if via:
+
+ ```
+ obj = self.__class__(**kwargs)
+ ```
+
+ :raise PathOverflow: when no more child nodes can be added
+ """
+ return MP_AddChildHandler(self, **kwargs).process()
+
+ def add_sibling(self, pos=None, **kwargs):
+ """
+ Adds a new node as a sibling to the current node object.
+
+ This method saves the node in database. The object is populated as if via:
+
+ ```
+ obj = self.__class__(**kwargs)
+ ```
+
+ :raise PathOverflow: when the library can't make room for the
+ node's new position
+ """
+ return MP_AddSiblingHandler(self, pos, **kwargs).process()
+
+ def get_root(self):
+ """:returns: the root node for the current node object."""
+ return get_result_class(self.__class__).objects.get(
+ path=self.path[0:self.steplen])
+
+ def is_root(self):
+ """:returns: True if the node is a root node (else, returns False)"""
+ return self.depth == 1
+
+ def is_leaf(self):
+ """:returns: True if the node is a leaf node (else, returns False)"""
+ return self.numchild == 0
+
+ def get_ancestors(self):
+ """
+ :returns: A queryset containing the current node object's ancestors,
+ starting by the root node and descending to the parent.
+ """
+ if self.is_root():
+ return get_result_class(self.__class__).objects.none()
+
+ paths = [
+ self.path[0:pos]
+ for pos in range(0, len(self.path), self.steplen)[1:]
+ ]
+ return get_result_class(self.__class__).objects.filter(
+ path__in=paths).order_by('depth')
+
+ def get_parent(self, update=False):
+ """
+ :returns: the parent node of the current node object.
+ Caches the result in the object itself to help in loops.
+ """
+ depth = int(len(self.path) / self.steplen)
+ if depth <= 1:
+ return
+ try:
+ if update:
+ del self._cached_parent_obj
+ else:
+ return self._cached_parent_obj
+ except AttributeError:
+ pass
+ parentpath = self._get_basepath(self.path, depth - 1)
+ self._cached_parent_obj = get_result_class(
+ self.__class__).objects.get(path=parentpath)
+ return self._cached_parent_obj
+
+ def move(self, target, pos=None):
+ """
+ Moves the current node and all it's descendants to a new position
+ relative to another node.
+
+ :raise PathOverflow: when the library can't make room for the
+ node's new position
+ """
+ return MP_MoveHandler(self, target, pos).process()
+
+ @classmethod
+ def _get_basepath(cls, path, depth):
+ """:returns: The base path of another path up to a given depth"""
+ if path:
+ return path[0:depth * cls.steplen]
+ return ''
+
+ @classmethod
+ def _get_path(cls, path, depth, newstep):
+ """
+ Builds a path given some values
+
+ :param path: the base path
+ :param depth: the depth of the node
+ :param newstep: the value (integer) of the new step
+ """
+ parentpath = cls._get_basepath(path, depth - 1)
+ key = cls._int2str(newstep)
+ return '{0}{1}{2}'.format(
+ parentpath,
+ cls.alphabet[0] * (cls.steplen - len(key)),
+ key
+ )
+
+ def _inc_path(self):
+ """:returns: The path of the next sibling of a given node path."""
+ newpos = self._str2int(self.path[-self.steplen:]) + 1
+ key = self._int2str(newpos)
+ if len(key) > self.steplen:
+ raise PathOverflow(_("Path Overflow from: '%s'" % (self.path, )))
+ return '{0}{1}{2}'.format(
+ self.path[:-self.steplen],
+ self.alphabet[0] * (self.steplen - len(key)),
+ key
+ )
+
+ def _get_lastpos_in_path(self):
+ """:returns: The integer value of the last step in a path."""
+ return self._str2int(self.path[-self.steplen:])
+
+ @classmethod
+ def _get_parent_path_from_path(cls, path):
+ """:returns: The parent path for a given path"""
+ if path:
+ return path[0:len(path) - cls.steplen]
+ return ''
+
+ @classmethod
+ def _get_children_path_interval(cls, path):
+ """:returns: An interval of all possible children paths for a node."""
+ return (path + cls.alphabet[0] * cls.steplen,
+ path + cls.alphabet[-1] * cls.steplen)
+
+ class Meta:
+ """Abstract model."""
+ abstract = True
diff --git a/vendor/treebeard/ns_tree.py b/vendor/treebeard/ns_tree.py
new file mode 100644
index 00000000..170b9a80
--- /dev/null
+++ b/vendor/treebeard/ns_tree.py
@@ -0,0 +1,674 @@
+"""Nested Sets"""
+
+import sys
+import operator
+
+if sys.version_info >= (3, 0):
+ from functools import reduce
+
+from django.core import serializers
+from django.db import connection, models, transaction
+from django.db.models import Q
+from django.utils.translation import gettext_noop as _
+
+from treebeard.exceptions import InvalidMoveToDescendant, NodeAlreadySaved
+from treebeard.models import Node
+
+
+def get_result_class(cls):
+ """
+ For the given model class, determine what class we should use for the
+ nodes returned by its tree methods (such as get_children).
+
+ Usually this will be trivially the same as the initial model class,
+ but there are special cases when model inheritance is in use:
+
+ * If the model extends another via multi-table inheritance, we need to
+ use whichever ancestor originally implemented the tree behaviour (i.e.
+ the one which defines the 'lft'/'rgt' fields). We can't use the
+ subclass, because it's not guaranteed that the other nodes reachable
+ from the current one will be instances of the same subclass.
+
+ * If the model is a proxy model, the returned nodes should also use
+ the proxy class.
+ """
+ base_class = cls._meta.get_field('lft').model
+ if cls._meta.proxy_for_model == base_class:
+ return cls
+ else:
+ return base_class
+
+
+class NS_NodeQuerySet(models.query.QuerySet):
+ """
+ Custom queryset for the tree node manager.
+
+ Needed only for the customized delete method.
+ """
+
+ def delete(self, removed_ranges=None):
+ """
+ Custom delete method, will remove all descendant nodes to ensure a
+ consistent tree (no orphans)
+
+ :returns: ``None``
+ """
+ model = get_result_class(self.model)
+ if removed_ranges is not None:
+ # we already know the children, let's call the default django
+ # delete method and let it handle the removal of the user's
+ # foreign keys...
+ super(NS_NodeQuerySet, self).delete()
+ cursor = model._get_database_cursor('write')
+
+ # Now closing the gap (Celko's trees book, page 62)
+ # We do this for every gap that was left in the tree when the nodes
+ # were removed. If many nodes were removed, we're going to update
+ # the same nodes over and over again. This would be probably
+ # cheaper precalculating the gapsize per intervals, or just do a
+ # complete reordering of the tree (uses COUNT)...
+ for tree_id, drop_lft, drop_rgt in sorted(removed_ranges,
+ reverse=True):
+ sql, params = model._get_close_gap_sql(drop_lft, drop_rgt,
+ tree_id)
+ cursor.execute(sql, params)
+ else:
+ # we'll have to manually run through all the nodes that are going
+ # to be deleted and remove nodes from the list if an ancestor is
+ # already getting removed, since that would be redundant
+ removed = {}
+ for node in self.order_by('tree_id', 'lft'):
+ found = False
+ for rid, rnode in removed.items():
+ if node.is_descendant_of(rnode):
+ found = True
+ break
+ if not found:
+ removed[node.pk] = node
+
+ # ok, got the minimal list of nodes to remove...
+ # we must also remove their descendants
+ toremove = []
+ ranges = []
+ for id, node in removed.items():
+ toremove.append(Q(lft__range=(node.lft, node.rgt)) &
+ Q(tree_id=node.tree_id))
+ ranges.append((node.tree_id, node.lft, node.rgt))
+ if toremove:
+ model.objects.filter(
+ reduce(operator.or_,
+ toremove)
+ ).delete(removed_ranges=ranges)
+
+
+class NS_NodeManager(models.Manager):
+ """Custom manager for nodes in a Nested Sets tree."""
+
+ def get_queryset(self):
+ """Sets the custom queryset as the default."""
+ return NS_NodeQuerySet(self.model).order_by('tree_id', 'lft')
+
+
+class NS_Node(Node):
+ """Abstract model to create your own Nested Sets Trees."""
+ node_order_by = []
+
+ lft = models.PositiveIntegerField(db_index=True)
+ rgt = models.PositiveIntegerField(db_index=True)
+ tree_id = models.PositiveIntegerField(db_index=True)
+ depth = models.PositiveIntegerField(db_index=True)
+
+ objects = NS_NodeManager()
+
+ @classmethod
+ def add_root(cls, **kwargs):
+ """Adds a root node to the tree."""
+
+ # do we have a root node already?
+ last_root = cls.get_last_root_node()
+
+ if last_root and last_root.node_order_by:
+ # there are root nodes and node_order_by has been set
+ # delegate sorted insertion to add_sibling
+ return last_root.add_sibling('sorted-sibling', **kwargs)
+
+ if last_root:
+ # adding the new root node as the last one
+ newtree_id = last_root.tree_id + 1
+ else:
+ # adding the first root node
+ newtree_id = 1
+
+ if len(kwargs) == 1 and 'instance' in kwargs:
+ # adding the passed (unsaved) instance to the tree
+ newobj = kwargs['instance']
+ if newobj.pk:
+ raise NodeAlreadySaved("Attempted to add a tree node that is "\
+ "already in the database")
+ else:
+ # creating the new object
+ newobj = get_result_class(cls)(**kwargs)
+
+ newobj.depth = 1
+ newobj.tree_id = newtree_id
+ newobj.lft = 1
+ newobj.rgt = 2
+ # saving the instance before returning it
+ newobj.save()
+ return newobj
+
+ @classmethod
+ def _move_right(cls, tree_id, rgt, lftmove=False, incdec=2):
+ if lftmove:
+ lftop = '>='
+ else:
+ lftop = '>'
+ sql = 'UPDATE %(table)s '\
+ ' SET lft = CASE WHEN lft %(lftop)s %(parent_rgt)d '\
+ ' THEN lft %(incdec)+d '\
+ ' ELSE lft END, '\
+ ' rgt = CASE WHEN rgt >= %(parent_rgt)d '\
+ ' THEN rgt %(incdec)+d '\
+ ' ELSE rgt END '\
+ ' WHERE rgt >= %(parent_rgt)d AND '\
+ ' tree_id = %(tree_id)s' % {
+ 'table': connection.ops.quote_name(
+ get_result_class(cls)._meta.db_table),
+ 'parent_rgt': rgt,
+ 'tree_id': tree_id,
+ 'lftop': lftop,
+ 'incdec': incdec}
+ return sql, []
+
+ @classmethod
+ def _move_tree_right(cls, tree_id):
+ sql = 'UPDATE %(table)s '\
+ ' SET tree_id = tree_id+1 '\
+ ' WHERE tree_id >= %(tree_id)d' % {
+ 'table': connection.ops.quote_name(
+ get_result_class(cls)._meta.db_table),
+ 'tree_id': tree_id}
+ return sql, []
+
+ def add_child(self, **kwargs):
+ """Adds a child to the node."""
+ if not self.is_leaf():
+ # there are child nodes, delegate insertion to add_sibling
+ if self.node_order_by:
+ pos = 'sorted-sibling'
+ else:
+ pos = 'last-sibling'
+ last_child = self.get_last_child()
+ last_child._cached_parent_obj = self
+ return last_child.add_sibling(pos, **kwargs)
+
+ # we're adding the first child of this node
+ sql, params = self.__class__._move_right(self.tree_id,
+ self.rgt, False, 2)
+
+ if len(kwargs) == 1 and 'instance' in kwargs:
+ # adding the passed (unsaved) instance to the tree
+ newobj = kwargs['instance']
+ if newobj.pk:
+ raise NodeAlreadySaved("Attempted to add a tree node that is "\
+ "already in the database")
+ else:
+ # creating a new object
+ newobj = get_result_class(self.__class__)(**kwargs)
+
+ newobj.tree_id = self.tree_id
+ newobj.depth = self.depth + 1
+ newobj.lft = self.lft + 1
+ newobj.rgt = self.lft + 2
+
+ # this is just to update the cache
+ self.rgt += 2
+
+ newobj._cached_parent_obj = self
+
+ cursor = self._get_database_cursor('write')
+ cursor.execute(sql, params)
+
+ # saving the instance before returning it
+ newobj.save()
+
+ return newobj
+
+ def add_sibling(self, pos=None, **kwargs):
+ """Adds a new node as a sibling to the current node object."""
+
+ pos = self._prepare_pos_var_for_add_sibling(pos)
+
+ if len(kwargs) == 1 and 'instance' in kwargs:
+ # adding the passed (unsaved) instance to the tree
+ newobj = kwargs['instance']
+ if newobj.pk:
+ raise NodeAlreadySaved("Attempted to add a tree node that is "\
+ "already in the database")
+ else:
+ # creating a new object
+ newobj = get_result_class(self.__class__)(**kwargs)
+
+ newobj.depth = self.depth
+
+ sql = None
+ target = self
+
+ if target.is_root():
+ newobj.lft = 1
+ newobj.rgt = 2
+ if pos == 'sorted-sibling':
+ siblings = list(target.get_sorted_pos_queryset(
+ target.get_siblings(), newobj))
+ if siblings:
+ pos = 'left'
+ target = siblings[0]
+ else:
+ pos = 'last-sibling'
+
+ last_root = target.__class__.get_last_root_node()
+ if (
+ (pos == 'last-sibling') or
+ (pos == 'right' and target == last_root)
+ ):
+ newobj.tree_id = last_root.tree_id + 1
+ else:
+ newpos = {'first-sibling': 1,
+ 'left': target.tree_id,
+ 'right': target.tree_id + 1}[pos]
+ sql, params = target.__class__._move_tree_right(newpos)
+
+ newobj.tree_id = newpos
+ else:
+ newobj.tree_id = target.tree_id
+
+ if pos == 'sorted-sibling':
+ siblings = list(target.get_sorted_pos_queryset(
+ target.get_siblings(), newobj))
+ if siblings:
+ pos = 'left'
+ target = siblings[0]
+ else:
+ pos = 'last-sibling'
+
+ if pos in ('left', 'right', 'first-sibling'):
+ siblings = list(target.get_siblings())
+
+ if pos == 'right':
+ if target == siblings[-1]:
+ pos = 'last-sibling'
+ else:
+ pos = 'left'
+ found = False
+ for node in siblings:
+ if found:
+ target = node
+ break
+ elif node == target:
+ found = True
+ if pos == 'left':
+ if target == siblings[0]:
+ pos = 'first-sibling'
+ if pos == 'first-sibling':
+ target = siblings[0]
+
+ move_right = self.__class__._move_right
+
+ if pos == 'last-sibling':
+ newpos = target.get_parent().rgt
+ sql, params = move_right(target.tree_id, newpos, False, 2)
+ elif pos == 'first-sibling':
+ newpos = target.lft
+ sql, params = move_right(target.tree_id, newpos - 1, False, 2)
+ elif pos == 'left':
+ newpos = target.lft
+ sql, params = move_right(target.tree_id, newpos, True, 2)
+
+ newobj.lft = newpos
+ newobj.rgt = newpos + 1
+
+ # saving the instance before returning it
+ if sql:
+ cursor = self._get_database_cursor('write')
+ cursor.execute(sql, params)
+ newobj.save()
+
+
+ return newobj
+
+ def move(self, target, pos=None):
+ """
+ Moves the current node and all it's descendants to a new position
+ relative to another node.
+ """
+
+ pos = self._prepare_pos_var_for_move(pos)
+ cls = get_result_class(self.__class__)
+
+ parent = None
+
+ if pos in ('first-child', 'last-child', 'sorted-child'):
+ # moving to a child
+ if target.is_leaf():
+ parent = target
+ pos = 'last-child'
+ else:
+ target = target.get_last_child()
+ pos = {'first-child': 'first-sibling',
+ 'last-child': 'last-sibling',
+ 'sorted-child': 'sorted-sibling'}[pos]
+
+ if target.is_descendant_of(self):
+ raise InvalidMoveToDescendant(
+ _("Can't move node to a descendant."))
+
+ if self == target and (
+ (pos == 'left') or
+ (pos in ('right', 'last-sibling') and
+ target == target.get_last_sibling()) or
+ (pos == 'first-sibling' and
+ target == target.get_first_sibling())):
+ # special cases, not actually moving the node so no need to UPDATE
+ return
+
+ if pos == 'sorted-sibling':
+ siblings = list(target.get_sorted_pos_queryset(
+ target.get_siblings(), self))
+ if siblings:
+ pos = 'left'
+ target = siblings[0]
+ else:
+ pos = 'last-sibling'
+ if pos in ('left', 'right', 'first-sibling'):
+ siblings = list(target.get_siblings())
+
+ if pos == 'right':
+ if target == siblings[-1]:
+ pos = 'last-sibling'
+ else:
+ pos = 'left'
+ found = False
+ for node in siblings:
+ if found:
+ target = node
+ break
+ elif node == target:
+ found = True
+ if pos == 'left':
+ if target == siblings[0]:
+ pos = 'first-sibling'
+ if pos == 'first-sibling':
+ target = siblings[0]
+
+ # ok let's move this
+ cursor = self._get_database_cursor('write')
+ move_right = cls._move_right
+ gap = self.rgt - self.lft + 1
+ sql = None
+ target_tree = target.tree_id
+
+ # first make a hole
+ if pos == 'last-child':
+ newpos = parent.rgt
+ sql, params = move_right(target.tree_id, newpos, False, gap)
+ elif target.is_root():
+ newpos = 1
+ if pos == 'last-sibling':
+ target_tree = target.get_siblings().reverse()[0].tree_id + 1
+ elif pos == 'first-sibling':
+ target_tree = 1
+ sql, params = cls._move_tree_right(1)
+ elif pos == 'left':
+ sql, params = cls._move_tree_right(target.tree_id)
+ else:
+ if pos == 'last-sibling':
+ newpos = target.get_parent().rgt
+ sql, params = move_right(target.tree_id, newpos, False, gap)
+ elif pos == 'first-sibling':
+ newpos = target.lft
+ sql, params = move_right(target.tree_id,
+ newpos - 1, False, gap)
+ elif pos == 'left':
+ newpos = target.lft
+ sql, params = move_right(target.tree_id, newpos, True, gap)
+
+ if sql:
+ cursor.execute(sql, params)
+
+ # we reload 'self' because lft/rgt may have changed
+
+ fromobj = cls.objects.get(pk=self.pk)
+
+ depthdiff = target.depth - fromobj.depth
+ if parent:
+ depthdiff += 1
+
+ # move the tree to the hole
+ sql = "UPDATE %(table)s "\
+ " SET tree_id = %(target_tree)d, "\
+ " lft = lft + %(jump)d , "\
+ " rgt = rgt + %(jump)d , "\
+ " depth = depth + %(depthdiff)d "\
+ " WHERE tree_id = %(from_tree)d AND "\
+ " lft BETWEEN %(fromlft)d AND %(fromrgt)d" % {
+ 'table': connection.ops.quote_name(cls._meta.db_table),
+ 'from_tree': fromobj.tree_id,
+ 'target_tree': target_tree,
+ 'jump': newpos - fromobj.lft,
+ 'depthdiff': depthdiff,
+ 'fromlft': fromobj.lft,
+ 'fromrgt': fromobj.rgt}
+ cursor.execute(sql, [])
+
+ # close the gap
+ sql, params = cls._get_close_gap_sql(fromobj.lft,
+ fromobj.rgt, fromobj.tree_id)
+ cursor.execute(sql, params)
+
+
+ @classmethod
+ def _get_close_gap_sql(cls, drop_lft, drop_rgt, tree_id):
+ sql = 'UPDATE %(table)s '\
+ ' SET lft = CASE '\
+ ' WHEN lft > %(drop_lft)d '\
+ ' THEN lft - %(gapsize)d '\
+ ' ELSE lft END, '\
+ ' rgt = CASE '\
+ ' WHEN rgt > %(drop_lft)d '\
+ ' THEN rgt - %(gapsize)d '\
+ ' ELSE rgt END '\
+ ' WHERE (lft > %(drop_lft)d '\
+ ' OR rgt > %(drop_lft)d) AND '\
+ ' tree_id=%(tree_id)d' % {
+ 'table': connection.ops.quote_name(
+ get_result_class(cls)._meta.db_table),
+ 'gapsize': drop_rgt - drop_lft + 1,
+ 'drop_lft': drop_lft,
+ 'tree_id': tree_id}
+ return sql, []
+
+ @classmethod
+ def load_bulk(cls, bulk_data, parent=None, keep_ids=False):
+ """Loads a list/dictionary structure to the tree."""
+
+ cls = get_result_class(cls)
+
+ # tree, iterative preorder
+ added = []
+ if parent:
+ parent_id = parent.pk
+ else:
+ parent_id = None
+ # stack of nodes to analize
+ stack = [(parent_id, node) for node in bulk_data[::-1]]
+ foreign_keys = cls.get_foreign_keys()
+ while stack:
+ parent_id, node_struct = stack.pop()
+ # shallow copy of the data strucure so it doesn't persist...
+ node_data = node_struct['data'].copy()
+ cls._process_foreign_keys(foreign_keys, node_data)
+ if keep_ids:
+ node_data['id'] = node_struct['id']
+ if parent_id:
+ parent = cls.objects.get(pk=parent_id)
+ node_obj = parent.add_child(**node_data)
+ else:
+ node_obj = cls.add_root(**node_data)
+ added.append(node_obj.pk)
+ if 'children' in node_struct:
+ # extending the stack with the current node as the parent of
+ # the new nodes
+ stack.extend([
+ (node_obj.pk, node)
+ for node in node_struct['children'][::-1]
+ ])
+ return added
+
+ def get_children(self):
+ """:returns: A queryset of all the node's children"""
+ return self.get_descendants().filter(depth=self.depth + 1)
+
+ def get_depth(self):
+ """:returns: the depth (level) of the node"""
+ return self.depth
+
+ def is_leaf(self):
+ """:returns: True if the node is a leaf node (else, returns False)"""
+ return self.rgt - self.lft == 1
+
+ def get_root(self):
+ """:returns: the root node for the current node object."""
+ if self.lft == 1:
+ return self
+ return get_result_class(self.__class__).objects.get(
+ tree_id=self.tree_id, lft=1)
+
+ def is_root(self):
+ """:returns: True if the node is a root node (else, returns False)"""
+ return self.lft == 1
+
+ def get_siblings(self):
+ """
+ :returns: A queryset of all the node's siblings, including the node
+ itself.
+ """
+ if self.lft == 1:
+ return self.get_root_nodes()
+ return self.get_parent(True).get_children()
+
+ @classmethod
+ def dump_bulk(cls, parent=None, keep_ids=True):
+ """Dumps a tree branch to a python data structure."""
+ qset = cls._get_serializable_model().get_tree(parent)
+ ret, lnk = [], {}
+ for pyobj in qset:
+ serobj = serializers.serialize('python', [pyobj])[0]
+ # django's serializer stores the attributes in 'fields'
+ fields = serobj['fields']
+ depth = fields['depth']
+ # this will be useless in load_bulk
+ del fields['lft']
+ del fields['rgt']
+ del fields['depth']
+ del fields['tree_id']
+ if 'id' in fields:
+ # this happens immediately after a load_bulk
+ del fields['id']
+
+ newobj = {'data': fields}
+ if keep_ids:
+ newobj['id'] = serobj['pk']
+
+ if (not parent and depth == 1) or\
+ (parent and depth == parent.depth):
+ ret.append(newobj)
+ else:
+ parentobj = pyobj.get_parent()
+ parentser = lnk[parentobj.pk]
+ if 'children' not in parentser:
+ parentser['children'] = []
+ parentser['children'].append(newobj)
+ lnk[pyobj.pk] = newobj
+ return ret
+
+ @classmethod
+ def get_tree(cls, parent=None):
+ """
+ :returns:
+
+ A *queryset* of nodes ordered as DFS, including the parent.
+ If no parent is given, all trees are returned.
+ """
+ cls = get_result_class(cls)
+
+ if parent is None:
+ # return the entire tree
+ return cls.objects.all()
+ if parent.is_leaf():
+ return cls.objects.filter(pk=parent.pk)
+ return cls.objects.filter(
+ tree_id=parent.tree_id,
+ lft__range=(parent.lft, parent.rgt - 1))
+
+ def get_descendants(self):
+ """
+ :returns: A queryset of all the node's descendants as DFS, doesn't
+ include the node itself
+ """
+ if self.is_leaf():
+ return get_result_class(self.__class__).objects.none()
+ return self.__class__.get_tree(self).exclude(pk=self.pk)
+
+ def get_descendant_count(self):
+ """:returns: the number of descendants of a node."""
+ return (self.rgt - self.lft - 1) / 2
+
+ def get_ancestors(self):
+ """
+ :returns: A queryset containing the current node object's ancestors,
+ starting by the root node and descending to the parent.
+ """
+ if self.is_root():
+ return get_result_class(self.__class__).objects.none()
+ return get_result_class(self.__class__).objects.filter(
+ tree_id=self.tree_id,
+ lft__lt=self.lft,
+ rgt__gt=self.rgt)
+
+ def is_descendant_of(self, node):
+ """
+ :returns: ``True`` if the node if a descendant of another node given
+ as an argument, else, returns ``False``
+ """
+ return (
+ self.tree_id == node.tree_id and
+ self.lft > node.lft and
+ self.rgt < node.rgt
+ )
+
+ def get_parent(self, update=False):
+ """
+ :returns: the parent node of the current node object.
+ Caches the result in the object itself to help in loops.
+ """
+ if self.is_root():
+ return
+ try:
+ if update:
+ del self._cached_parent_obj
+ else:
+ return self._cached_parent_obj
+ except AttributeError:
+ pass
+ # parent = our most direct ancestor
+ self._cached_parent_obj = self.get_ancestors().reverse()[0]
+ return self._cached_parent_obj
+
+ @classmethod
+ def get_root_nodes(cls):
+ """:returns: A queryset containing the root nodes in the tree."""
+ return get_result_class(cls).objects.filter(lft=1)
+
+ class Meta:
+ """Abstract model."""
+ abstract = True
diff --git a/vendor/treebeard/numconv.py b/vendor/treebeard/numconv.py
new file mode 100644
index 00000000..4c8d81fa
--- /dev/null
+++ b/vendor/treebeard/numconv.py
@@ -0,0 +1,115 @@
+"""Convert strings to numbers and numbers to strings.
+
+Gustavo Picon
+https://tabo.pe/projects/numconv/
+
+"""
+
+
+__version__ = '2.1.1'
+
+# from april fool's rfc 1924
+BASE85 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' \
+ '!#$%&()*+-;<=>?@^_`{|}~'
+
+# rfc4648 alphabets
+BASE16 = BASE85[:16]
+BASE32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
+BASE32HEX = BASE85[:32]
+BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
+BASE64URL = BASE64[:62] + '-_'
+
+# http://en.wikipedia.org/wiki/Base_62 useful for url shorteners
+BASE62 = BASE85[:62]
+
+
+class NumConv(object):
+ """Class to create converter objects.
+
+ :param radix: The base that will be used in the conversions.
+ The default value is 10 for decimal conversions.
+ :param alphabet: A string that will be used as a encoding alphabet.
+ The length of the alphabet can be longer than the radix. In this
+ case the alphabet will be internally truncated.
+
+ The default value is :data:`numconv.BASE85`
+
+ :raise TypeError: when *radix* isn't an integer
+ :raise ValueError: when *radix* is invalid
+ :raise ValueError: when *alphabet* has duplicated characters
+ """
+
+ def __init__(self, radix=10, alphabet=BASE85):
+ """basic validation and cached_map storage"""
+ if int(radix) != radix:
+ raise TypeError('radix must be an integer')
+ if not 2 <= radix <= len(alphabet):
+ raise ValueError('radix must be >= 2 and <= %d' % (
+ len(alphabet), ))
+ self.radix = radix
+ self.alphabet = alphabet
+ self.cached_map = dict(zip(self.alphabet, range(len(self.alphabet))))
+ if len(self.cached_map) != len(self.alphabet):
+ raise ValueError("duplicate characters found in '%s'" % (
+ self.alphabet, ))
+
+ def int2str(self, num):
+ """Converts an integer into a string.
+
+ :param num: A numeric value to be converted to another base as a
+ string.
+
+ :rtype: string
+
+ :raise TypeError: when *num* isn't an integer
+ :raise ValueError: when *num* isn't positive
+ """
+ if int(num) != num:
+ raise TypeError('number must be an integer')
+ if num < 0:
+ raise ValueError('number must be positive')
+ radix, alphabet = self.radix, self.alphabet
+ if radix in (8, 10, 16) and \
+ alphabet[:radix].lower() == BASE85[:radix].lower():
+ return ({8: '%o', 10: '%d', 16: '%x'}[radix] % num).upper()
+ ret = ''
+ while True:
+ ret = alphabet[num % radix] + ret
+ if num < radix:
+ break
+ num //= radix
+ return ret
+
+ def str2int(self, num):
+ """Converts a string into an integer.
+
+ If possible, the built-in python conversion will be used for speed
+ purposes.
+
+ :param num: A string that will be converted to an integer.
+
+ :rtype: integer
+
+ :raise ValueError: when *num* is invalid
+ """
+ radix, alphabet = self.radix, self.alphabet
+ if radix <= 36 and alphabet[:radix].lower() == BASE85[:radix].lower():
+ return int(num, radix)
+ ret = 0
+ lalphabet = alphabet[:radix]
+ for char in num:
+ if char not in lalphabet:
+ raise ValueError("invalid literal for radix2int() with radix "
+ "%d: '%s'" % (radix, num))
+ ret = ret * radix + self.cached_map[char]
+ return ret
+
+
+def int2str(num, radix=10, alphabet=BASE85):
+ """helper function for quick base conversions from integers to strings"""
+ return NumConv(radix, alphabet).int2str(num)
+
+
+def str2int(num, radix=10, alphabet=BASE85):
+ """helper function for quick base conversions from strings to integers"""
+ return NumConv(radix, alphabet).str2int(num)
diff --git a/vendor/treebeard/static/treebeard/expand-collapse.png b/vendor/treebeard/static/treebeard/expand-collapse.png
new file mode 100644
index 00000000..2f9da7e1
Binary files /dev/null and b/vendor/treebeard/static/treebeard/expand-collapse.png differ
diff --git a/vendor/treebeard/static/treebeard/jquery-ui-1.8.5.custom.min.js b/vendor/treebeard/static/treebeard/jquery-ui-1.8.5.custom.min.js
new file mode 100644
index 00000000..1461652d
--- /dev/null
+++ b/vendor/treebeard/static/treebeard/jquery-ui-1.8.5.custom.min.js
@@ -0,0 +1,1816 @@
+/*!
+ * jQuery UI 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI
+ */
+(function (c, j) {
+ function k(a) {
+ return!c(a).parents().andSelf().filter(function () {
+ return c.curCSS(this, "visibility") === "hidden" || c.expr.filters.hidden(this)
+ }).length
+ }
+
+ c.ui = c.ui || {};
+ if (!c.ui.version) {
+ c.extend(c.ui, {version: "1.8.5", keyCode: {ALT: 18, BACKSPACE: 8, CAPS_LOCK: 20, COMMA: 188, COMMAND: 91, COMMAND_LEFT: 91, COMMAND_RIGHT: 93, CONTROL: 17, DELETE: 46, DOWN: 40, END: 35, ENTER: 13, ESCAPE: 27, HOME: 36, INSERT: 45, LEFT: 37, MENU: 93, NUMPAD_ADD: 107, NUMPAD_DECIMAL: 110, NUMPAD_DIVIDE: 111, NUMPAD_ENTER: 108, NUMPAD_MULTIPLY: 106,
+ NUMPAD_SUBTRACT: 109, PAGE_DOWN: 34, PAGE_UP: 33, PERIOD: 190, RIGHT: 39, SHIFT: 16, SPACE: 32, TAB: 9, UP: 38, WINDOWS: 91}});
+ c.fn.extend({_focus: c.fn.focus, focus: function (a, b) {
+ return typeof a === "number" ? this.each(function () {
+ var d = this;
+ setTimeout(function () {
+ c(d).focus();
+ b && b.call(d)
+ }, a)
+ }) : this._focus.apply(this, arguments)
+ }, scrollParent: function () {
+ var a;
+ a = c.browser.msie && /(static|relative)/.test(this.css("position")) || /absolute/.test(this.css("position")) ? this.parents().filter(function () {
+ return/(relative|absolute|fixed)/.test(c.curCSS(this,
+ "position", 1)) && /(auto|scroll)/.test(c.curCSS(this, "overflow", 1) + c.curCSS(this, "overflow-y", 1) + c.curCSS(this, "overflow-x", 1))
+ }).eq(0) : this.parents().filter(function () {
+ return/(auto|scroll)/.test(c.curCSS(this, "overflow", 1) + c.curCSS(this, "overflow-y", 1) + c.curCSS(this, "overflow-x", 1))
+ }).eq(0);
+ return/fixed/.test(this.css("position")) || !a.length ? c(document) : a
+ }, zIndex: function (a) {
+ if (a !== j)return this.css("zIndex", a);
+ if (this.length) {
+ a = c(this[0]);
+ for (var b; a.length && a[0] !== document;) {
+ b = a.css("position");
+ if (b === "absolute" || b === "relative" || b === "fixed") {
+ b = parseInt(a.css("zIndex"));
+ if (!isNaN(b) && b != 0)return b
+ }
+ a = a.parent()
+ }
+ }
+ return 0
+ }, disableSelection: function () {
+ return this.bind("mousedown.ui-disableSelection selectstart.ui-disableSelection", function (a) {
+ a.preventDefault()
+ })
+ }, enableSelection: function () {
+ return this.unbind(".ui-disableSelection")
+ }});
+ c.each(["Width", "Height"], function (a, b) {
+ function d(f, g, l, m) {
+ c.each(e, function () {
+ g -= parseFloat(c.curCSS(f, "padding" + this, true)) || 0;
+ if (l)g -= parseFloat(c.curCSS(f,
+ "border" + this + "Width", true)) || 0;
+ if (m)g -= parseFloat(c.curCSS(f, "margin" + this, true)) || 0
+ });
+ return g
+ }
+
+ var e = b === "Width" ? ["Left", "Right"] : ["Top", "Bottom"], h = b.toLowerCase(), i = {innerWidth: c.fn.innerWidth, innerHeight: c.fn.innerHeight, outerWidth: c.fn.outerWidth, outerHeight: c.fn.outerHeight};
+ c.fn["inner" + b] = function (f) {
+ if (f === j)return i["inner" + b].call(this);
+ return this.each(function () {
+ c.style(this, h, d(this, f) + "px")
+ })
+ };
+ c.fn["outer" + b] = function (f, g) {
+ if (typeof f !== "number")return i["outer" + b].call(this, f);
+ return this.each(function () {
+ c.style(this,
+ h, d(this, f, true, g) + "px")
+ })
+ }
+ });
+ c.extend(c.expr[":"], {data: function (a, b, d) {
+ return!!c.data(a, d[3])
+ }, focusable: function (a) {
+ var b = a.nodeName.toLowerCase(), d = c.attr(a, "tabindex");
+ if ("area" === b) {
+ b = a.parentNode;
+ d = b.name;
+ if (!a.href || !d || b.nodeName.toLowerCase() !== "map")return false;
+ a = c("img[usemap=#" + d + "]")[0];
+ return!!a && k(a)
+ }
+ return(/input|select|textarea|button|object/.test(b) ? !a.disabled : "a" == b ? a.href || !isNaN(d) : !isNaN(d)) && k(a)
+ }, tabbable: function (a) {
+ var b = c.attr(a, "tabindex");
+ return(isNaN(b) || b >= 0) && c(a).is(":focusable")
+ }});
+ c(function () {
+ var a = document.createElement("div"), b = document.body;
+ c.extend(a.style, {minHeight: "100px", height: "auto", padding: 0, borderWidth: 0});
+ c.support.minHeight = b.appendChild(a).offsetHeight === 100;
+ b.removeChild(a).style.display = "none"
+ });
+ c.extend(c.ui, {plugin: {add: function (a, b, d) {
+ a = c.ui[a].prototype;
+ for (var e in d) {
+ a.plugins[e] = a.plugins[e] || [];
+ a.plugins[e].push([b, d[e]])
+ }
+ }, call: function (a, b, d) {
+ if ((b = a.plugins[b]) && a.element[0].parentNode)for (var e = 0; e < b.length; e++)a.options[b[e][0]] && b[e][1].apply(a.element,
+ d)
+ }}, contains: function (a, b) {
+ return document.compareDocumentPosition ? a.compareDocumentPosition(b) & 16 : a !== b && a.contains(b)
+ }, hasScroll: function (a, b) {
+ if (c(a).css("overflow") === "hidden")return false;
+ b = b && b === "left" ? "scrollLeft" : "scrollTop";
+ var d = false;
+ if (a[b] > 0)return true;
+ a[b] = 1;
+ d = a[b] > 0;
+ a[b] = 0;
+ return d
+ }, isOverAxis: function (a, b, d) {
+ return a > b && a < b + d
+ }, isOver: function (a, b, d, e, h, i) {
+ return c.ui.isOverAxis(a, d, h) && c.ui.isOverAxis(b, e, i)
+ }})
+ }
+})(jQuery);
+;
+/*!
+ * jQuery UI Widget 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Widget
+ */
+(function (b, j) {
+ if (b.cleanData) {
+ var k = b.cleanData;
+ b.cleanData = function (a) {
+ for (var c = 0, d; (d = a[c]) != null; c++)b(d).triggerHandler("remove");
+ k(a)
+ }
+ } else {
+ var l = b.fn.remove;
+ b.fn.remove = function (a, c) {
+ return this.each(function () {
+ if (!c)if (!a || b.filter(a, [this]).length)b("*", this).add([this]).each(function () {
+ b(this).triggerHandler("remove")
+ });
+ return l.call(b(this), a, c)
+ })
+ }
+ }
+ b.widget = function (a, c, d) {
+ var e = a.split(".")[0], f;
+ a = a.split(".")[1];
+ f = e + "-" + a;
+ if (!d) {
+ d = c;
+ c = b.Widget
+ }
+ b.expr[":"][f] = function (h) {
+ return!!b.data(h,
+ a)
+ };
+ b[e] = b[e] || {};
+ b[e][a] = function (h, g) {
+ arguments.length && this._createWidget(h, g)
+ };
+ c = new c;
+ c.options = b.extend(true, {}, c.options);
+ b[e][a].prototype = b.extend(true, c, {namespace: e, widgetName: a, widgetEventPrefix: b[e][a].prototype.widgetEventPrefix || a, widgetBaseClass: f}, d);
+ b.widget.bridge(a, b[e][a])
+ };
+ b.widget.bridge = function (a, c) {
+ b.fn[a] = function (d) {
+ var e = typeof d === "string", f = Array.prototype.slice.call(arguments, 1), h = this;
+ d = !e && f.length ? b.extend.apply(null, [true, d].concat(f)) : d;
+ if (e && d.substring(0, 1) ===
+ "_")return h;
+ e ? this.each(function () {
+ var g = b.data(this, a);
+ if (!g)throw"cannot call methods on " + a + " prior to initialization; attempted to call method '" + d + "'";
+ if (!b.isFunction(g[d]))throw"no such method '" + d + "' for " + a + " widget instance";
+ var i = g[d].apply(g, f);
+ if (i !== g && i !== j) {
+ h = i;
+ return false
+ }
+ }) : this.each(function () {
+ var g = b.data(this, a);
+ g ? g.option(d || {})._init() : b.data(this, a, new c(d, this))
+ });
+ return h
+ }
+ };
+ b.Widget = function (a, c) {
+ arguments.length && this._createWidget(a, c)
+ };
+ b.Widget.prototype = {widgetName: "widget",
+ widgetEventPrefix: "", options: {disabled: false}, _createWidget: function (a, c) {
+ b.data(c, this.widgetName, this);
+ this.element = b(c);
+ this.options = b.extend(true, {}, this.options, b.metadata && b.metadata.get(c)[this.widgetName], a);
+ var d = this;
+ this.element.bind("remove." + this.widgetName, function () {
+ d.destroy()
+ });
+ this._create();
+ this._init()
+ }, _create: function () {
+ }, _init: function () {
+ }, destroy: function () {
+ this.element.unbind("." + this.widgetName).removeData(this.widgetName);
+ this.widget().unbind("." + this.widgetName).removeAttr("aria-disabled").removeClass(this.widgetBaseClass +
+ "-disabled ui-state-disabled")
+ }, widget: function () {
+ return this.element
+ }, option: function (a, c) {
+ var d = a, e = this;
+ if (arguments.length === 0)return b.extend({}, e.options);
+ if (typeof a === "string") {
+ if (c === j)return this.options[a];
+ d = {};
+ d[a] = c
+ }
+ b.each(d, function (f, h) {
+ e._setOption(f, h)
+ });
+ return e
+ }, _setOption: function (a, c) {
+ this.options[a] = c;
+ if (a === "disabled")this.widget()[c ? "addClass" : "removeClass"](this.widgetBaseClass + "-disabled ui-state-disabled").attr("aria-disabled", c);
+ return this
+ }, enable: function () {
+ return this._setOption("disabled",
+ false)
+ }, disable: function () {
+ return this._setOption("disabled", true)
+ }, _trigger: function (a, c, d) {
+ var e = this.options[a];
+ c = b.Event(c);
+ c.type = (a === this.widgetEventPrefix ? a : this.widgetEventPrefix + a).toLowerCase();
+ d = d || {};
+ if (c.originalEvent) {
+ a = b.event.props.length;
+ for (var f; a;) {
+ f = b.event.props[--a];
+ c[f] = c.originalEvent[f]
+ }
+ }
+ this.element.trigger(c, d);
+ return!(b.isFunction(e) && e.call(this.element[0], c, d) === false || c.isDefaultPrevented())
+ }}
+})(jQuery);
+;
+/*!
+ * jQuery UI Mouse 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Mouse
+ *
+ * Depends:
+ * jquery.ui.widget.js
+ */
+(function (c) {
+ c.widget("ui.mouse", {options: {cancel: ":input,option", distance: 1, delay: 0}, _mouseInit: function () {
+ var a = this;
+ this.element.bind("mousedown." + this.widgetName,function (b) {
+ return a._mouseDown(b)
+ }).bind("click." + this.widgetName, function (b) {
+ if (a._preventClickEvent) {
+ a._preventClickEvent = false;
+ b.stopImmediatePropagation();
+ return false
+ }
+ });
+ this.started = false
+ }, _mouseDestroy: function () {
+ this.element.unbind("." + this.widgetName)
+ }, _mouseDown: function (a) {
+ a.originalEvent = a.originalEvent || {};
+ if (!a.originalEvent.mouseHandled) {
+ this._mouseStarted &&
+ this._mouseUp(a);
+ this._mouseDownEvent = a;
+ var b = this, e = a.which == 1, f = typeof this.options.cancel == "string" ? c(a.target).parents().add(a.target).filter(this.options.cancel).length : false;
+ if (!e || f || !this._mouseCapture(a))return true;
+ this.mouseDelayMet = !this.options.delay;
+ if (!this.mouseDelayMet)this._mouseDelayTimer = setTimeout(function () {
+ b.mouseDelayMet = true
+ }, this.options.delay);
+ if (this._mouseDistanceMet(a) && this._mouseDelayMet(a)) {
+ this._mouseStarted = this._mouseStart(a) !== false;
+ if (!this._mouseStarted) {
+ a.preventDefault();
+ return true
+ }
+ }
+ this._mouseMoveDelegate = function (d) {
+ return b._mouseMove(d)
+ };
+ this._mouseUpDelegate = function (d) {
+ return b._mouseUp(d)
+ };
+ c(document).bind("mousemove." + this.widgetName, this._mouseMoveDelegate).bind("mouseup." + this.widgetName, this._mouseUpDelegate);
+ c.browser.safari || a.preventDefault();
+ return a.originalEvent.mouseHandled = true
+ }
+ }, _mouseMove: function (a) {
+ if (c.browser.msie && !a.button)return this._mouseUp(a);
+ if (this._mouseStarted) {
+ this._mouseDrag(a);
+ return a.preventDefault()
+ }
+ if (this._mouseDistanceMet(a) &&
+ this._mouseDelayMet(a))(this._mouseStarted = this._mouseStart(this._mouseDownEvent, a) !== false) ? this._mouseDrag(a) : this._mouseUp(a);
+ return!this._mouseStarted
+ }, _mouseUp: function (a) {
+ c(document).unbind("mousemove." + this.widgetName, this._mouseMoveDelegate).unbind("mouseup." + this.widgetName, this._mouseUpDelegate);
+ if (this._mouseStarted) {
+ this._mouseStarted = false;
+ this._preventClickEvent = a.target == this._mouseDownEvent.target;
+ this._mouseStop(a)
+ }
+ return false
+ }, _mouseDistanceMet: function (a) {
+ return Math.max(Math.abs(this._mouseDownEvent.pageX -
+ a.pageX), Math.abs(this._mouseDownEvent.pageY - a.pageY)) >= this.options.distance
+ }, _mouseDelayMet: function () {
+ return this.mouseDelayMet
+ }, _mouseStart: function () {
+ }, _mouseDrag: function () {
+ }, _mouseStop: function () {
+ }, _mouseCapture: function () {
+ return true
+ }})
+})(jQuery);
+;
+/*
+ * jQuery UI Position 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Position
+ */
+(function (c) {
+ c.ui = c.ui || {};
+ var n = /left|center|right/, o = /top|center|bottom/, t = c.fn.position, u = c.fn.offset;
+ c.fn.position = function (b) {
+ if (!b || !b.of)return t.apply(this, arguments);
+ b = c.extend({}, b);
+ var a = c(b.of), d = a[0], g = (b.collision || "flip").split(" "), e = b.offset ? b.offset.split(" ") : [0, 0], h, k, j;
+ if (d.nodeType === 9) {
+ h = a.width();
+ k = a.height();
+ j = {top: 0, left: 0}
+ } else if (d.scrollTo && d.document) {
+ h = a.width();
+ k = a.height();
+ j = {top: a.scrollTop(), left: a.scrollLeft()}
+ } else if (d.preventDefault) {
+ b.at = "left top";
+ h = k = 0;
+ j =
+ {top: b.of.pageY, left: b.of.pageX}
+ } else {
+ h = a.outerWidth();
+ k = a.outerHeight();
+ j = a.offset()
+ }
+ c.each(["my", "at"], function () {
+ var f = (b[this] || "").split(" ");
+ if (f.length === 1)f = n.test(f[0]) ? f.concat(["center"]) : o.test(f[0]) ? ["center"].concat(f) : ["center", "center"];
+ f[0] = n.test(f[0]) ? f[0] : "center";
+ f[1] = o.test(f[1]) ? f[1] : "center";
+ b[this] = f
+ });
+ if (g.length === 1)g[1] = g[0];
+ e[0] = parseInt(e[0], 10) || 0;
+ if (e.length === 1)e[1] = e[0];
+ e[1] = parseInt(e[1], 10) || 0;
+ if (b.at[0] === "right")j.left += h; else if (b.at[0] === "center")j.left += h /
+ 2;
+ if (b.at[1] === "bottom")j.top += k; else if (b.at[1] === "center")j.top += k / 2;
+ j.left += e[0];
+ j.top += e[1];
+ return this.each(function () {
+ var f = c(this), l = f.outerWidth(), m = f.outerHeight(), p = parseInt(c.curCSS(this, "marginLeft", true)) || 0, q = parseInt(c.curCSS(this, "marginTop", true)) || 0, v = l + p + parseInt(c.curCSS(this, "marginRight", true)) || 0, w = m + q + parseInt(c.curCSS(this, "marginBottom", true)) || 0, i = c.extend({}, j), r;
+ if (b.my[0] === "right")i.left -= l; else if (b.my[0] === "center")i.left -= l / 2;
+ if (b.my[1] === "bottom")i.top -= m; else if (b.my[1] ===
+ "center")i.top -= m / 2;
+ i.left = parseInt(i.left);
+ i.top = parseInt(i.top);
+ r = {left: i.left - p, top: i.top - q};
+ c.each(["left", "top"], function (s, x) {
+ c.ui.position[g[s]] && c.ui.position[g[s]][x](i, {targetWidth: h, targetHeight: k, elemWidth: l, elemHeight: m, collisionPosition: r, collisionWidth: v, collisionHeight: w, offset: e, my: b.my, at: b.at})
+ });
+ c.fn.bgiframe && f.bgiframe();
+ f.offset(c.extend(i, {using: b.using}))
+ })
+ };
+ c.ui.position = {fit: {left: function (b, a) {
+ var d = c(window);
+ d = a.collisionPosition.left + a.collisionWidth - d.width() - d.scrollLeft();
+ b.left = d > 0 ? b.left - d : Math.max(b.left - a.collisionPosition.left, b.left)
+ }, top: function (b, a) {
+ var d = c(window);
+ d = a.collisionPosition.top + a.collisionHeight - d.height() - d.scrollTop();
+ b.top = d > 0 ? b.top - d : Math.max(b.top - a.collisionPosition.top, b.top)
+ }}, flip: {left: function (b, a) {
+ if (a.at[0] !== "center") {
+ var d = c(window);
+ d = a.collisionPosition.left + a.collisionWidth - d.width() - d.scrollLeft();
+ var g = a.my[0] === "left" ? -a.elemWidth : a.my[0] === "right" ? a.elemWidth : 0, e = a.at[0] === "left" ? a.targetWidth : -a.targetWidth, h = -2 * a.offset[0];
+ b.left += a.collisionPosition.left < 0 ? g + e + h : d > 0 ? g + e + h : 0
+ }
+ }, top: function (b, a) {
+ if (a.at[1] !== "center") {
+ var d = c(window);
+ d = a.collisionPosition.top + a.collisionHeight - d.height() - d.scrollTop();
+ var g = a.my[1] === "top" ? -a.elemHeight : a.my[1] === "bottom" ? a.elemHeight : 0, e = a.at[1] === "top" ? a.targetHeight : -a.targetHeight, h = -2 * a.offset[1];
+ b.top += a.collisionPosition.top < 0 ? g + e + h : d > 0 ? g + e + h : 0
+ }
+ }}};
+ if (!c.offset.setOffset) {
+ c.offset.setOffset = function (b, a) {
+ if (/static/.test(c.curCSS(b, "position")))b.style.position = "relative";
+ var d =
+ c(b), g = d.offset(), e = parseInt(c.curCSS(b, "top", true), 10) || 0, h = parseInt(c.curCSS(b, "left", true), 10) || 0;
+ g = {top: a.top - g.top + e, left: a.left - g.left + h};
+ "using"in a ? a.using.call(b, g) : d.css(g)
+ };
+ c.fn.offset = function (b) {
+ var a = this[0];
+ if (!a || !a.ownerDocument)return null;
+ if (b)return this.each(function () {
+ c.offset.setOffset(this, b)
+ });
+ return u.call(this)
+ }
+ }
+})(jQuery);
+;
+/*
+ * jQuery UI Slider 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Slider
+ *
+ * Depends:
+ * jquery.ui.core.js
+ * jquery.ui.mouse.js
+ * jquery.ui.widget.js
+ */
+(function (d) {
+ d.widget("ui.slider", d.ui.mouse, {widgetEventPrefix: "slide", options: {animate: false, distance: 0, max: 100, min: 0, orientation: "horizontal", range: false, step: 1, value: 0, values: null}, _create: function () {
+ var a = this, b = this.options;
+ this._mouseSliding = this._keySliding = false;
+ this._animateOff = true;
+ this._handleIndex = null;
+ this._detectOrientation();
+ this._mouseInit();
+ this.element.addClass("ui-slider ui-slider-" + this.orientation + " ui-widget ui-widget-content ui-corner-all");
+ b.disabled && this.element.addClass("ui-slider-disabled ui-disabled");
+ this.range = d([]);
+ if (b.range) {
+ if (b.range === true) {
+ this.range = d("
");
+ if (!b.values)b.values = [this._valueMin(), this._valueMin()];
+ if (b.values.length && b.values.length !== 2)b.values = [b.values[0], b.values[0]]
+ } else this.range = d("
");
+ this.range.appendTo(this.element).addClass("ui-slider-range");
+ if (b.range === "min" || b.range === "max")this.range.addClass("ui-slider-range-" + b.range);
+ this.range.addClass("ui-widget-header")
+ }
+ d(".ui-slider-handle", this.element).length === 0 && d(" ").appendTo(this.element).addClass("ui-slider-handle");
+ if (b.values && b.values.length)for (; d(".ui-slider-handle", this.element).length < b.values.length;)d(" ").appendTo(this.element).addClass("ui-slider-handle");
+ this.handles = d(".ui-slider-handle", this.element).addClass("ui-state-default ui-corner-all");
+ this.handle = this.handles.eq(0);
+ this.handles.add(this.range).filter("a").click(function (c) {
+ c.preventDefault()
+ }).hover(function () {
+ b.disabled || d(this).addClass("ui-state-hover")
+ },function () {
+ d(this).removeClass("ui-state-hover")
+ }).focus(function () {
+ if (b.disabled)d(this).blur();
+ else {
+ d(".ui-slider .ui-state-focus").removeClass("ui-state-focus");
+ d(this).addClass("ui-state-focus")
+ }
+ }).blur(function () {
+ d(this).removeClass("ui-state-focus")
+ });
+ this.handles.each(function (c) {
+ d(this).data("index.ui-slider-handle", c)
+ });
+ this.handles.keydown(function (c) {
+ var e = true, f = d(this).data("index.ui-slider-handle"), h, g, i;
+ if (!a.options.disabled) {
+ switch (c.keyCode) {
+ case d.ui.keyCode.HOME:
+ case d.ui.keyCode.END:
+ case d.ui.keyCode.PAGE_UP:
+ case d.ui.keyCode.PAGE_DOWN:
+ case d.ui.keyCode.UP:
+ case d.ui.keyCode.RIGHT:
+ case d.ui.keyCode.DOWN:
+ case d.ui.keyCode.LEFT:
+ e =
+ false;
+ if (!a._keySliding) {
+ a._keySliding = true;
+ d(this).addClass("ui-state-active");
+ h = a._start(c, f);
+ if (h === false)return
+ }
+ break
+ }
+ i = a.options.step;
+ h = a.options.values && a.options.values.length ? (g = a.values(f)) : (g = a.value());
+ switch (c.keyCode) {
+ case d.ui.keyCode.HOME:
+ g = a._valueMin();
+ break;
+ case d.ui.keyCode.END:
+ g = a._valueMax();
+ break;
+ case d.ui.keyCode.PAGE_UP:
+ g = a._trimAlignValue(h + (a._valueMax() - a._valueMin()) / 5);
+ break;
+ case d.ui.keyCode.PAGE_DOWN:
+ g = a._trimAlignValue(h - (a._valueMax() - a._valueMin()) / 5);
+ break;
+ case d.ui.keyCode.UP:
+ case d.ui.keyCode.RIGHT:
+ if (h ===
+ a._valueMax())return;
+ g = a._trimAlignValue(h + i);
+ break;
+ case d.ui.keyCode.DOWN:
+ case d.ui.keyCode.LEFT:
+ if (h === a._valueMin())return;
+ g = a._trimAlignValue(h - i);
+ break
+ }
+ a._slide(c, f, g);
+ return e
+ }
+ }).keyup(function (c) {
+ var e = d(this).data("index.ui-slider-handle");
+ if (a._keySliding) {
+ a._keySliding = false;
+ a._stop(c, e);
+ a._change(c, e);
+ d(this).removeClass("ui-state-active")
+ }
+ });
+ this._refreshValue();
+ this._animateOff = false
+ }, destroy: function () {
+ this.handles.remove();
+ this.range.remove();
+ this.element.removeClass("ui-slider ui-slider-horizontal ui-slider-vertical ui-slider-disabled ui-widget ui-widget-content ui-corner-all").removeData("slider").unbind(".slider");
+ this._mouseDestroy();
+ return this
+ }, _mouseCapture: function (a) {
+ var b = this.options, c, e, f, h, g;
+ if (b.disabled)return false;
+ this.elementSize = {width: this.element.outerWidth(), height: this.element.outerHeight()};
+ this.elementOffset = this.element.offset();
+ c = this._normValueFromMouse({x: a.pageX, y: a.pageY});
+ e = this._valueMax() - this._valueMin() + 1;
+ h = this;
+ this.handles.each(function (i) {
+ var j = Math.abs(c - h.values(i));
+ if (e > j) {
+ e = j;
+ f = d(this);
+ g = i
+ }
+ });
+ if (b.range === true && this.values(1) === b.min) {
+ g += 1;
+ f = d(this.handles[g])
+ }
+ if (this._start(a,
+ g) === false)return false;
+ this._mouseSliding = true;
+ h._handleIndex = g;
+ f.addClass("ui-state-active").focus();
+ b = f.offset();
+ this._clickOffset = !d(a.target).parents().andSelf().is(".ui-slider-handle") ? {left: 0, top: 0} : {left: a.pageX - b.left - f.width() / 2, top: a.pageY - b.top - f.height() / 2 - (parseInt(f.css("borderTopWidth"), 10) || 0) - (parseInt(f.css("borderBottomWidth"), 10) || 0) + (parseInt(f.css("marginTop"), 10) || 0)};
+ this._slide(a, g, c);
+ return this._animateOff = true
+ }, _mouseStart: function () {
+ return true
+ }, _mouseDrag: function (a) {
+ var b =
+ this._normValueFromMouse({x: a.pageX, y: a.pageY});
+ this._slide(a, this._handleIndex, b);
+ return false
+ }, _mouseStop: function (a) {
+ this.handles.removeClass("ui-state-active");
+ this._mouseSliding = false;
+ this._stop(a, this._handleIndex);
+ this._change(a, this._handleIndex);
+ this._clickOffset = this._handleIndex = null;
+ return this._animateOff = false
+ }, _detectOrientation: function () {
+ this.orientation = this.options.orientation === "vertical" ? "vertical" : "horizontal"
+ }, _normValueFromMouse: function (a) {
+ var b;
+ if (this.orientation === "horizontal") {
+ b =
+ this.elementSize.width;
+ a = a.x - this.elementOffset.left - (this._clickOffset ? this._clickOffset.left : 0)
+ } else {
+ b = this.elementSize.height;
+ a = a.y - this.elementOffset.top - (this._clickOffset ? this._clickOffset.top : 0)
+ }
+ b = a / b;
+ if (b > 1)b = 1;
+ if (b < 0)b = 0;
+ if (this.orientation === "vertical")b = 1 - b;
+ a = this._valueMax() - this._valueMin();
+ return this._trimAlignValue(this._valueMin() + b * a)
+ }, _start: function (a, b) {
+ var c = {handle: this.handles[b], value: this.value()};
+ if (this.options.values && this.options.values.length) {
+ c.value = this.values(b);
+ c.values = this.values()
+ }
+ return this._trigger("start", a, c)
+ }, _slide: function (a, b, c) {
+ var e;
+ if (this.options.values && this.options.values.length) {
+ e = this.values(b ? 0 : 1);
+ if (this.options.values.length === 2 && this.options.range === true && (b === 0 && c > e || b === 1 && c < e))c = e;
+ if (c !== this.values(b)) {
+ e = this.values();
+ e[b] = c;
+ a = this._trigger("slide", a, {handle: this.handles[b], value: c, values: e});
+ this.values(b ? 0 : 1);
+ a !== false && this.values(b, c, true)
+ }
+ } else if (c !== this.value()) {
+ a = this._trigger("slide", a, {handle: this.handles[b], value: c});
+ a !== false && this.value(c)
+ }
+ }, _stop: function (a, b) {
+ var c = {handle: this.handles[b], value: this.value()};
+ if (this.options.values && this.options.values.length) {
+ c.value = this.values(b);
+ c.values = this.values()
+ }
+ this._trigger("stop", a, c)
+ }, _change: function (a, b) {
+ if (!this._keySliding && !this._mouseSliding) {
+ var c = {handle: this.handles[b], value: this.value()};
+ if (this.options.values && this.options.values.length) {
+ c.value = this.values(b);
+ c.values = this.values()
+ }
+ this._trigger("change", a, c)
+ }
+ }, value: function (a) {
+ if (arguments.length) {
+ this.options.value =
+ this._trimAlignValue(a);
+ this._refreshValue();
+ this._change(null, 0)
+ }
+ return this._value()
+ }, values: function (a, b) {
+ var c, e, f;
+ if (arguments.length > 1) {
+ this.options.values[a] = this._trimAlignValue(b);
+ this._refreshValue();
+ this._change(null, a)
+ }
+ if (arguments.length)if (d.isArray(arguments[0])) {
+ c = this.options.values;
+ e = arguments[0];
+ for (f = 0; f < c.length; f += 1) {
+ c[f] = this._trimAlignValue(e[f]);
+ this._change(null, f)
+ }
+ this._refreshValue()
+ } else return this.options.values && this.options.values.length ? this._values(a) : this.value();
+ else return this._values()
+ }, _setOption: function (a, b) {
+ var c, e = 0;
+ if (d.isArray(this.options.values))e = this.options.values.length;
+ d.Widget.prototype._setOption.apply(this, arguments);
+ switch (a) {
+ case "disabled":
+ if (b) {
+ this.handles.filter(".ui-state-focus").blur();
+ this.handles.removeClass("ui-state-hover");
+ this.handles.attr("disabled", "disabled");
+ this.element.addClass("ui-disabled")
+ } else {
+ this.handles.removeAttr("disabled");
+ this.element.removeClass("ui-disabled")
+ }
+ break;
+ case "orientation":
+ this._detectOrientation();
+ this.element.removeClass("ui-slider-horizontal ui-slider-vertical").addClass("ui-slider-" + this.orientation);
+ this._refreshValue();
+ break;
+ case "value":
+ this._animateOff = true;
+ this._refreshValue();
+ this._change(null, 0);
+ this._animateOff = false;
+ break;
+ case "values":
+ this._animateOff = true;
+ this._refreshValue();
+ for (c = 0; c < e; c += 1)this._change(null, c);
+ this._animateOff = false;
+ break
+ }
+ }, _value: function () {
+ var a = this.options.value;
+ return a = this._trimAlignValue(a)
+ }, _values: function (a) {
+ var b, c;
+ if (arguments.length) {
+ b = this.options.values[a];
+ return b = this._trimAlignValue(b)
+ } else {
+ b = this.options.values.slice();
+ for (c = 0; c < b.length; c += 1)b[c] = this._trimAlignValue(b[c]);
+ return b
+ }
+ }, _trimAlignValue: function (a) {
+ if (a < this._valueMin())return this._valueMin();
+ if (a > this._valueMax())return this._valueMax();
+ var b = this.options.step > 0 ? this.options.step : 1, c = a % b;
+ a = a - c;
+ if (Math.abs(c) * 2 >= b)a += c > 0 ? b : -b;
+ return parseFloat(a.toFixed(5))
+ }, _valueMin: function () {
+ return this.options.min
+ }, _valueMax: function () {
+ return this.options.max
+ }, _refreshValue: function () {
+ var a =
+ this.options.range, b = this.options, c = this, e = !this._animateOff ? b.animate : false, f, h = {}, g, i, j, l;
+ if (this.options.values && this.options.values.length)this.handles.each(function (k) {
+ f = (c.values(k) - c._valueMin()) / (c._valueMax() - c._valueMin()) * 100;
+ h[c.orientation === "horizontal" ? "left" : "bottom"] = f + "%";
+ d(this).stop(1, 1)[e ? "animate" : "css"](h, b.animate);
+ if (c.options.range === true)if (c.orientation === "horizontal") {
+ if (k === 0)c.range.stop(1, 1)[e ? "animate" : "css"]({left: f + "%"}, b.animate);
+ if (k === 1)c.range[e ? "animate" : "css"]({width: f -
+ g + "%"}, {queue: false, duration: b.animate})
+ } else {
+ if (k === 0)c.range.stop(1, 1)[e ? "animate" : "css"]({bottom: f + "%"}, b.animate);
+ if (k === 1)c.range[e ? "animate" : "css"]({height: f - g + "%"}, {queue: false, duration: b.animate})
+ }
+ g = f
+ }); else {
+ i = this.value();
+ j = this._valueMin();
+ l = this._valueMax();
+ f = l !== j ? (i - j) / (l - j) * 100 : 0;
+ h[c.orientation === "horizontal" ? "left" : "bottom"] = f + "%";
+ this.handle.stop(1, 1)[e ? "animate" : "css"](h, b.animate);
+ if (a === "min" && this.orientation === "horizontal")this.range.stop(1, 1)[e ? "animate" : "css"]({width: f + "%"},
+ b.animate);
+ if (a === "max" && this.orientation === "horizontal")this.range[e ? "animate" : "css"]({width: 100 - f + "%"}, {queue: false, duration: b.animate});
+ if (a === "min" && this.orientation === "vertical")this.range.stop(1, 1)[e ? "animate" : "css"]({height: f + "%"}, b.animate);
+ if (a === "max" && this.orientation === "vertical")this.range[e ? "animate" : "css"]({height: 100 - f + "%"}, {queue: false, duration: b.animate})
+ }
+ }});
+ d.extend(d.ui.slider, {version: "1.8.5"})
+})(jQuery);
+;
+/*
+ * jQuery UI Datepicker 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Datepicker
+ *
+ * Depends:
+ * jquery.ui.core.js
+ */
+(function (d, G) {
+ function L() {
+ this.debug = false;
+ this._curInst = null;
+ this._keyEvent = false;
+ this._disabledInputs = [];
+ this._inDialog = this._datepickerShowing = false;
+ this._mainDivId = "ui-datepicker-div";
+ this._inlineClass = "ui-datepicker-inline";
+ this._appendClass = "ui-datepicker-append";
+ this._triggerClass = "ui-datepicker-trigger";
+ this._dialogClass = "ui-datepicker-dialog";
+ this._disableClass = "ui-datepicker-disabled";
+ this._unselectableClass = "ui-datepicker-unselectable";
+ this._currentClass = "ui-datepicker-current-day";
+ this._dayOverClass =
+ "ui-datepicker-days-cell-over";
+ this.regional = [];
+ this.regional[""] = {closeText: "Done", prevText: "Prev", nextText: "Next", currentText: "Today", monthNames: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], monthNamesShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], dayNames: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], dayNamesShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], dayNamesMin: ["Su",
+ "Mo", "Tu", "We", "Th", "Fr", "Sa"], weekHeader: "Wk", dateFormat: "mm/dd/yy", firstDay: 0, isRTL: false, showMonthAfterYear: false, yearSuffix: ""};
+ this._defaults = {showOn: "focus", showAnim: "fadeIn", showOptions: {}, defaultDate: null, appendText: "", buttonText: "...", buttonImage: "", buttonImageOnly: false, hideIfNoPrevNext: false, navigationAsDateFormat: false, gotoCurrent: false, changeMonth: false, changeYear: false, yearRange: "c-10:c+10", showOtherMonths: false, selectOtherMonths: false, showWeek: false, calculateWeek: this.iso8601Week, shortYearCutoff: "+10",
+ minDate: null, maxDate: null, duration: "fast", beforeShowDay: null, beforeShow: null, onSelect: null, onChangeMonthYear: null, onClose: null, numberOfMonths: 1, showCurrentAtPos: 0, stepMonths: 1, stepBigMonths: 12, altField: "", altFormat: "", constrainInput: true, showButtonPanel: false, autoSize: false};
+ d.extend(this._defaults, this.regional[""]);
+ this.dpDiv = d('
')
+ }
+
+ function E(a, b) {
+ d.extend(a,
+ b);
+ for (var c in b)if (b[c] == null || b[c] == G)a[c] = b[c];
+ return a
+ }
+
+ d.extend(d.ui, {datepicker: {version: "1.8.5"}});
+ var y = (new Date).getTime();
+ d.extend(L.prototype, {markerClassName: "hasDatepicker", log: function () {
+ this.debug && console.log.apply("", arguments)
+ }, _widgetDatepicker: function () {
+ return this.dpDiv
+ }, setDefaults: function (a) {
+ E(this._defaults, a || {});
+ return this
+ }, _attachDatepicker: function (a, b) {
+ var c = null;
+ for (var e in this._defaults) {
+ var f = a.getAttribute("date:" + e);
+ if (f) {
+ c = c || {};
+ try {
+ c[e] = eval(f)
+ } catch (h) {
+ c[e] =
+ f
+ }
+ }
+ }
+ e = a.nodeName.toLowerCase();
+ f = e == "div" || e == "span";
+ if (!a.id) {
+ this.uuid += 1;
+ a.id = "dp" + this.uuid
+ }
+ var i = this._newInst(d(a), f);
+ i.settings = d.extend({}, b || {}, c || {});
+ if (e == "input")this._connectDatepicker(a, i); else f && this._inlineDatepicker(a, i)
+ }, _newInst: function (a, b) {
+ return{id: a[0].id.replace(/([^A-Za-z0-9_])/g, "\\\\$1"), input: a, selectedDay: 0, selectedMonth: 0, selectedYear: 0, drawMonth: 0, drawYear: 0, inline: b, dpDiv: !b ? this.dpDiv : d('
')}
+ },
+ _connectDatepicker: function (a, b) {
+ var c = d(a);
+ b.append = d([]);
+ b.trigger = d([]);
+ if (!c.hasClass(this.markerClassName)) {
+ this._attachments(c, b);
+ c.addClass(this.markerClassName).keydown(this._doKeyDown).keypress(this._doKeyPress).keyup(this._doKeyUp).bind("setData.datepicker",function (e, f, h) {
+ b.settings[f] = h
+ }).bind("getData.datepicker", function (e, f) {
+ return this._get(b, f)
+ });
+ this._autoSize(b);
+ d.data(a, "datepicker", b)
+ }
+ }, _attachments: function (a, b) {
+ var c = this._get(b, "appendText"), e = this._get(b, "isRTL");
+ b.append &&
+ b.append.remove();
+ if (c) {
+ b.append = d('' + c + " ");
+ a[e ? "before" : "after"](b.append)
+ }
+ a.unbind("focus", this._showDatepicker);
+ b.trigger && b.trigger.remove();
+ c = this._get(b, "showOn");
+ if (c == "focus" || c == "both")a.focus(this._showDatepicker);
+ if (c == "button" || c == "both") {
+ c = this._get(b, "buttonText");
+ var f = this._get(b, "buttonImage");
+ b.trigger = d(this._get(b, "buttonImageOnly") ? d(" ").addClass(this._triggerClass).attr({src: f, alt: c, title: c}) : d(' ').addClass(this._triggerClass).html(f ==
+ "" ? c : d(" ").attr({src: f, alt: c, title: c})));
+ a[e ? "before" : "after"](b.trigger);
+ b.trigger.click(function () {
+ d.datepicker._datepickerShowing && d.datepicker._lastInput == a[0] ? d.datepicker._hideDatepicker() : d.datepicker._showDatepicker(a[0]);
+ return false
+ })
+ }
+ }, _autoSize: function (a) {
+ if (this._get(a, "autoSize") && !a.inline) {
+ var b = new Date(2009, 11, 20), c = this._get(a, "dateFormat");
+ if (c.match(/[DM]/)) {
+ var e = function (f) {
+ for (var h = 0, i = 0, g = 0; g < f.length; g++)if (f[g].length > h) {
+ h = f[g].length;
+ i = g
+ }
+ return i
+ };
+ b.setMonth(e(this._get(a,
+ c.match(/MM/) ? "monthNames" : "monthNamesShort")));
+ b.setDate(e(this._get(a, c.match(/DD/) ? "dayNames" : "dayNamesShort")) + 20 - b.getDay())
+ }
+ a.input.attr("size", this._formatDate(a, b).length)
+ }
+ }, _inlineDatepicker: function (a, b) {
+ var c = d(a);
+ if (!c.hasClass(this.markerClassName)) {
+ c.addClass(this.markerClassName).append(b.dpDiv).bind("setData.datepicker",function (e, f, h) {
+ b.settings[f] = h
+ }).bind("getData.datepicker", function (e, f) {
+ return this._get(b, f)
+ });
+ d.data(a, "datepicker", b);
+ this._setDate(b, this._getDefaultDate(b),
+ true);
+ this._updateDatepicker(b);
+ this._updateAlternate(b)
+ }
+ }, _dialogDatepicker: function (a, b, c, e, f) {
+ a = this._dialogInst;
+ if (!a) {
+ this.uuid += 1;
+ this._dialogInput = d(' ');
+ this._dialogInput.keydown(this._doKeyDown);
+ d("body").append(this._dialogInput);
+ a = this._dialogInst = this._newInst(this._dialogInput, false);
+ a.settings = {};
+ d.data(this._dialogInput[0], "datepicker", a)
+ }
+ E(a.settings, e || {});
+ b = b && b.constructor ==
+ Date ? this._formatDate(a, b) : b;
+ this._dialogInput.val(b);
+ this._pos = f ? f.length ? f : [f.pageX, f.pageY] : null;
+ if (!this._pos)this._pos = [document.documentElement.clientWidth / 2 - 100 + (document.documentElement.scrollLeft || document.body.scrollLeft), document.documentElement.clientHeight / 2 - 150 + (document.documentElement.scrollTop || document.body.scrollTop)];
+ this._dialogInput.css("left", this._pos[0] + 20 + "px").css("top", this._pos[1] + "px");
+ a.settings.onSelect = c;
+ this._inDialog = true;
+ this.dpDiv.addClass(this._dialogClass);
+ this._showDatepicker(this._dialogInput[0]);
+ d.blockUI && d.blockUI(this.dpDiv);
+ d.data(this._dialogInput[0], "datepicker", a);
+ return this
+ }, _destroyDatepicker: function (a) {
+ var b = d(a), c = d.data(a, "datepicker");
+ if (b.hasClass(this.markerClassName)) {
+ var e = a.nodeName.toLowerCase();
+ d.removeData(a, "datepicker");
+ if (e == "input") {
+ c.append.remove();
+ c.trigger.remove();
+ b.removeClass(this.markerClassName).unbind("focus", this._showDatepicker).unbind("keydown", this._doKeyDown).unbind("keypress", this._doKeyPress).unbind("keyup", this._doKeyUp)
+ } else if (e == "div" || e == "span")b.removeClass(this.markerClassName).empty()
+ }
+ },
+ _enableDatepicker: function (a) {
+ var b = d(a), c = d.data(a, "datepicker");
+ if (b.hasClass(this.markerClassName)) {
+ var e = a.nodeName.toLowerCase();
+ if (e == "input") {
+ a.disabled = false;
+ c.trigger.filter("button").each(function () {
+ this.disabled = false
+ }).end().filter("img").css({opacity: "1.0", cursor: ""})
+ } else if (e == "div" || e == "span")b.children("." + this._inlineClass).children().removeClass("ui-state-disabled");
+ this._disabledInputs = d.map(this._disabledInputs, function (f) {
+ return f == a ? null : f
+ })
+ }
+ }, _disableDatepicker: function (a) {
+ var b =
+ d(a), c = d.data(a, "datepicker");
+ if (b.hasClass(this.markerClassName)) {
+ var e = a.nodeName.toLowerCase();
+ if (e == "input") {
+ a.disabled = true;
+ c.trigger.filter("button").each(function () {
+ this.disabled = true
+ }).end().filter("img").css({opacity: "0.5", cursor: "default"})
+ } else if (e == "div" || e == "span")b.children("." + this._inlineClass).children().addClass("ui-state-disabled");
+ this._disabledInputs = d.map(this._disabledInputs, function (f) {
+ return f == a ? null : f
+ });
+ this._disabledInputs[this._disabledInputs.length] = a
+ }
+ }, _isDisabledDatepicker: function (a) {
+ if (!a)return false;
+ for (var b = 0; b < this._disabledInputs.length; b++)if (this._disabledInputs[b] == a)return true;
+ return false
+ }, _getInst: function (a) {
+ try {
+ return d.data(a, "datepicker")
+ } catch (b) {
+ throw"Missing instance data for this datepicker";
+ }
+ }, _optionDatepicker: function (a, b, c) {
+ var e = this._getInst(a);
+ if (arguments.length == 2 && typeof b == "string")return b == "defaults" ? d.extend({}, d.datepicker._defaults) : e ? b == "all" ? d.extend({}, e.settings) : this._get(e, b) : null;
+ var f = b || {};
+ if (typeof b == "string") {
+ f = {};
+ f[b] = c
+ }
+ if (e) {
+ this._curInst == e &&
+ this._hideDatepicker();
+ var h = this._getDateDatepicker(a, true);
+ E(e.settings, f);
+ this._attachments(d(a), e);
+ this._autoSize(e);
+ this._setDateDatepicker(a, h);
+ this._updateDatepicker(e)
+ }
+ }, _changeDatepicker: function (a, b, c) {
+ this._optionDatepicker(a, b, c)
+ }, _refreshDatepicker: function (a) {
+ (a = this._getInst(a)) && this._updateDatepicker(a)
+ }, _setDateDatepicker: function (a, b) {
+ if (a = this._getInst(a)) {
+ this._setDate(a, b);
+ this._updateDatepicker(a);
+ this._updateAlternate(a)
+ }
+ }, _getDateDatepicker: function (a, b) {
+ (a = this._getInst(a)) && !a.inline && this._setDateFromField(a, b);
+ return a ? this._getDate(a) : null
+ }, _doKeyDown: function (a) {
+ var b = d.datepicker._getInst(a.target), c = true, e = b.dpDiv.is(".ui-datepicker-rtl");
+ b._keyEvent = true;
+ if (d.datepicker._datepickerShowing)switch (a.keyCode) {
+ case 9:
+ d.datepicker._hideDatepicker();
+ c = false;
+ break;
+ case 13:
+ c = d("td." + d.datepicker._dayOverClass, b.dpDiv).add(d("td." + d.datepicker._currentClass, b.dpDiv));
+ c[0] ? d.datepicker._selectDay(a.target, b.selectedMonth, b.selectedYear, c[0]) : d.datepicker._hideDatepicker();
+ return false;
+ case 27:
+ d.datepicker._hideDatepicker();
+ break;
+ case 33:
+ d.datepicker._adjustDate(a.target, a.ctrlKey ? -d.datepicker._get(b, "stepBigMonths") : -d.datepicker._get(b, "stepMonths"), "M");
+ break;
+ case 34:
+ d.datepicker._adjustDate(a.target, a.ctrlKey ? +d.datepicker._get(b, "stepBigMonths") : +d.datepicker._get(b, "stepMonths"), "M");
+ break;
+ case 35:
+ if (a.ctrlKey || a.metaKey)d.datepicker._clearDate(a.target);
+ c = a.ctrlKey || a.metaKey;
+ break;
+ case 36:
+ if (a.ctrlKey || a.metaKey)d.datepicker._gotoToday(a.target);
+ c = a.ctrlKey ||
+ a.metaKey;
+ break;
+ case 37:
+ if (a.ctrlKey || a.metaKey)d.datepicker._adjustDate(a.target, e ? +1 : -1, "D");
+ c = a.ctrlKey || a.metaKey;
+ if (a.originalEvent.altKey)d.datepicker._adjustDate(a.target, a.ctrlKey ? -d.datepicker._get(b, "stepBigMonths") : -d.datepicker._get(b, "stepMonths"), "M");
+ break;
+ case 38:
+ if (a.ctrlKey || a.metaKey)d.datepicker._adjustDate(a.target, -7, "D");
+ c = a.ctrlKey || a.metaKey;
+ break;
+ case 39:
+ if (a.ctrlKey || a.metaKey)d.datepicker._adjustDate(a.target, e ? -1 : +1, "D");
+ c = a.ctrlKey || a.metaKey;
+ if (a.originalEvent.altKey)d.datepicker._adjustDate(a.target,
+ a.ctrlKey ? +d.datepicker._get(b, "stepBigMonths") : +d.datepicker._get(b, "stepMonths"), "M");
+ break;
+ case 40:
+ if (a.ctrlKey || a.metaKey)d.datepicker._adjustDate(a.target, +7, "D");
+ c = a.ctrlKey || a.metaKey;
+ break;
+ default:
+ c = false
+ } else if (a.keyCode == 36 && a.ctrlKey)d.datepicker._showDatepicker(this); else c = false;
+ if (c) {
+ a.preventDefault();
+ a.stopPropagation()
+ }
+ }, _doKeyPress: function (a) {
+ var b = d.datepicker._getInst(a.target);
+ if (d.datepicker._get(b, "constrainInput")) {
+ b = d.datepicker._possibleChars(d.datepicker._get(b, "dateFormat"));
+ var c = String.fromCharCode(a.charCode == G ? a.keyCode : a.charCode);
+ return a.ctrlKey || c < " " || !b || b.indexOf(c) > -1
+ }
+ }, _doKeyUp: function (a) {
+ a = d.datepicker._getInst(a.target);
+ if (a.input.val() != a.lastVal)try {
+ if (d.datepicker.parseDate(d.datepicker._get(a, "dateFormat"), a.input ? a.input.val() : null, d.datepicker._getFormatConfig(a))) {
+ d.datepicker._setDateFromField(a);
+ d.datepicker._updateAlternate(a);
+ d.datepicker._updateDatepicker(a)
+ }
+ } catch (b) {
+ d.datepicker.log(b)
+ }
+ return true
+ }, _showDatepicker: function (a) {
+ a = a.target ||
+ a;
+ if (a.nodeName.toLowerCase() != "input")a = d("input", a.parentNode)[0];
+ if (!(d.datepicker._isDisabledDatepicker(a) || d.datepicker._lastInput == a)) {
+ var b = d.datepicker._getInst(a);
+ d.datepicker._curInst && d.datepicker._curInst != b && d.datepicker._curInst.dpDiv.stop(true, true);
+ var c = d.datepicker._get(b, "beforeShow");
+ E(b.settings, c ? c.apply(a, [a, b]) : {});
+ b.lastVal = null;
+ d.datepicker._lastInput = a;
+ d.datepicker._setDateFromField(b);
+ if (d.datepicker._inDialog)a.value = "";
+ if (!d.datepicker._pos) {
+ d.datepicker._pos = d.datepicker._findPos(a);
+ d.datepicker._pos[1] += a.offsetHeight
+ }
+ var e = false;
+ d(a).parents().each(function () {
+ e |= d(this).css("position") == "fixed";
+ return!e
+ });
+ if (e && d.browser.opera) {
+ d.datepicker._pos[0] -= document.documentElement.scrollLeft;
+ d.datepicker._pos[1] -= document.documentElement.scrollTop
+ }
+ c = {left: d.datepicker._pos[0], top: d.datepicker._pos[1]};
+ d.datepicker._pos = null;
+ b.dpDiv.css({position: "absolute", display: "block", top: "-1000px"});
+ d.datepicker._updateDatepicker(b);
+ c = d.datepicker._checkOffset(b, c, e);
+ b.dpDiv.css({position: d.datepicker._inDialog &&
+ d.blockUI ? "static" : e ? "fixed" : "absolute", display: "none", left: c.left + "px", top: c.top + "px"});
+ if (!b.inline) {
+ c = d.datepicker._get(b, "showAnim");
+ var f = d.datepicker._get(b, "duration"), h = function () {
+ d.datepicker._datepickerShowing = true;
+ var i = d.datepicker._getBorders(b.dpDiv);
+ b.dpDiv.find("iframe.ui-datepicker-cover").css({left: -i[0], top: -i[1], width: b.dpDiv.outerWidth(), height: b.dpDiv.outerHeight()})
+ };
+ b.dpDiv.zIndex(d(a).zIndex() + 1);
+ d.effects && d.effects[c] ? b.dpDiv.show(c, d.datepicker._get(b, "showOptions"), f,
+ h) : b.dpDiv[c || "show"](c ? f : null, h);
+ if (!c || !f)h();
+ b.input.is(":visible") && !b.input.is(":disabled") && b.input.focus();
+ d.datepicker._curInst = b
+ }
+ }
+ }, _updateDatepicker: function (a) {
+ var b = this, c = d.datepicker._getBorders(a.dpDiv);
+ a.dpDiv.empty().append(this._generateHTML(a)).find("iframe.ui-datepicker-cover").css({left: -c[0], top: -c[1], width: a.dpDiv.outerWidth(), height: a.dpDiv.outerHeight()}).end().find("button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a").bind("mouseout",function () {
+ d(this).removeClass("ui-state-hover");
+ this.className.indexOf("ui-datepicker-prev") != -1 && d(this).removeClass("ui-datepicker-prev-hover");
+ this.className.indexOf("ui-datepicker-next") != -1 && d(this).removeClass("ui-datepicker-next-hover")
+ }).bind("mouseover",function () {
+ if (!b._isDisabledDatepicker(a.inline ? a.dpDiv.parent()[0] : a.input[0])) {
+ d(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover");
+ d(this).addClass("ui-state-hover");
+ this.className.indexOf("ui-datepicker-prev") != -1 && d(this).addClass("ui-datepicker-prev-hover");
+ this.className.indexOf("ui-datepicker-next") != -1 && d(this).addClass("ui-datepicker-next-hover")
+ }
+ }).end().find("." + this._dayOverClass + " a").trigger("mouseover").end();
+ c = this._getNumberOfMonths(a);
+ var e = c[1];
+ e > 1 ? a.dpDiv.addClass("ui-datepicker-multi-" + e).css("width", 17 * e + "em") : a.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width("");
+ a.dpDiv[(c[0] != 1 || c[1] != 1 ? "add" : "remove") + "Class"]("ui-datepicker-multi");
+ a.dpDiv[(this._get(a, "isRTL") ? "add" : "remove") + "Class"]("ui-datepicker-rtl");
+ a == d.datepicker._curInst && d.datepicker._datepickerShowing && a.input && a.input.is(":visible") && !a.input.is(":disabled") && a.input.focus()
+ }, _getBorders: function (a) {
+ var b = function (c) {
+ return{thin: 1, medium: 2, thick: 3}[c] || c
+ };
+ return[parseFloat(b(a.css("border-left-width"))), parseFloat(b(a.css("border-top-width")))]
+ }, _checkOffset: function (a, b, c) {
+ var e = a.dpDiv.outerWidth(), f = a.dpDiv.outerHeight(), h = a.input ? a.input.outerWidth() : 0, i = a.input ? a.input.outerHeight() : 0, g = document.documentElement.clientWidth + d(document).scrollLeft(),
+ k = document.documentElement.clientHeight + d(document).scrollTop();
+ b.left -= this._get(a, "isRTL") ? e - h : 0;
+ b.left -= c && b.left == a.input.offset().left ? d(document).scrollLeft() : 0;
+ b.top -= c && b.top == a.input.offset().top + i ? d(document).scrollTop() : 0;
+ b.left -= Math.min(b.left, b.left + e > g && g > e ? Math.abs(b.left + e - g) : 0);
+ b.top -= Math.min(b.top, b.top + f > k && k > f ? Math.abs(f + i) : 0);
+ return b
+ }, _findPos: function (a) {
+ for (var b = this._get(this._getInst(a), "isRTL"); a && (a.type == "hidden" || a.nodeType != 1);)a = a[b ? "previousSibling" : "nextSibling"];
+ a = d(a).offset();
+ return[a.left, a.top]
+ }, _hideDatepicker: function (a) {
+ var b = this._curInst;
+ if (!(!b || a && b != d.data(a, "datepicker")))if (this._datepickerShowing) {
+ a = this._get(b, "showAnim");
+ var c = this._get(b, "duration"), e = function () {
+ d.datepicker._tidyDialog(b);
+ this._curInst = null
+ };
+ d.effects && d.effects[a] ? b.dpDiv.hide(a, d.datepicker._get(b, "showOptions"), c, e) : b.dpDiv[a == "slideDown" ? "slideUp" : a == "fadeIn" ? "fadeOut" : "hide"](a ? c : null, e);
+ a || e();
+ if (a = this._get(b, "onClose"))a.apply(b.input ? b.input[0] : null, [b.input ? b.input.val() :
+ "", b]);
+ this._datepickerShowing = false;
+ this._lastInput = null;
+ if (this._inDialog) {
+ this._dialogInput.css({position: "absolute", left: "0", top: "-100px"});
+ if (d.blockUI) {
+ d.unblockUI();
+ d("body").append(this.dpDiv)
+ }
+ }
+ this._inDialog = false
+ }
+ }, _tidyDialog: function (a) {
+ a.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar")
+ }, _checkExternalClick: function (a) {
+ if (d.datepicker._curInst) {
+ a = d(a.target);
+ a[0].id != d.datepicker._mainDivId && a.parents("#" + d.datepicker._mainDivId).length == 0 && !a.hasClass(d.datepicker.markerClassName) && !a.hasClass(d.datepicker._triggerClass) && d.datepicker._datepickerShowing && !(d.datepicker._inDialog && d.blockUI) && d.datepicker._hideDatepicker()
+ }
+ }, _adjustDate: function (a, b, c) {
+ a = d(a);
+ var e = this._getInst(a[0]);
+ if (!this._isDisabledDatepicker(a[0])) {
+ this._adjustInstDate(e, b + (c == "M" ? this._get(e, "showCurrentAtPos") : 0), c);
+ this._updateDatepicker(e)
+ }
+ }, _gotoToday: function (a) {
+ a = d(a);
+ var b = this._getInst(a[0]);
+ if (this._get(b, "gotoCurrent") && b.currentDay) {
+ b.selectedDay = b.currentDay;
+ b.drawMonth = b.selectedMonth = b.currentMonth;
+ b.drawYear = b.selectedYear = b.currentYear
+ } else {
+ var c = new Date;
+ b.selectedDay = c.getDate();
+ b.drawMonth = b.selectedMonth = c.getMonth();
+ b.drawYear = b.selectedYear = c.getFullYear()
+ }
+ this._notifyChange(b);
+ this._adjustDate(a)
+ }, _selectMonthYear: function (a, b, c) {
+ a = d(a);
+ var e = this._getInst(a[0]);
+ e._selectingMonthYear = false;
+ e["selected" + (c == "M" ? "Month" : "Year")] = e["draw" + (c == "M" ? "Month" : "Year")] = parseInt(b.options[b.selectedIndex].value, 10);
+ this._notifyChange(e);
+ this._adjustDate(a)
+ }, _clickMonthYear: function (a) {
+ var b =
+ this._getInst(d(a)[0]);
+ b.input && b._selectingMonthYear && setTimeout(function () {
+ b.input.focus()
+ }, 0);
+ b._selectingMonthYear = !b._selectingMonthYear
+ }, _selectDay: function (a, b, c, e) {
+ var f = d(a);
+ if (!(d(e).hasClass(this._unselectableClass) || this._isDisabledDatepicker(f[0]))) {
+ f = this._getInst(f[0]);
+ f.selectedDay = f.currentDay = d("a", e).html();
+ f.selectedMonth = f.currentMonth = b;
+ f.selectedYear = f.currentYear = c;
+ this._selectDate(a, this._formatDate(f, f.currentDay, f.currentMonth, f.currentYear))
+ }
+ }, _clearDate: function (a) {
+ a =
+ d(a);
+ this._getInst(a[0]);
+ this._selectDate(a, "")
+ }, _selectDate: function (a, b) {
+ a = this._getInst(d(a)[0]);
+ b = b != null ? b : this._formatDate(a);
+ a.input && a.input.val(b);
+ this._updateAlternate(a);
+ var c = this._get(a, "onSelect");
+ if (c)c.apply(a.input ? a.input[0] : null, [b, a]); else a.input && a.input.trigger("change");
+ if (a.inline)this._updateDatepicker(a); else {
+ this._hideDatepicker();
+ this._lastInput = a.input[0];
+ typeof a.input[0] != "object" && a.input.focus();
+ this._lastInput = null
+ }
+ }, _updateAlternate: function (a) {
+ var b = this._get(a,
+ "altField");
+ if (b) {
+ var c = this._get(a, "altFormat") || this._get(a, "dateFormat"), e = this._getDate(a), f = this.formatDate(c, e, this._getFormatConfig(a));
+ d(b).each(function () {
+ d(this).val(f)
+ })
+ }
+ }, noWeekends: function (a) {
+ a = a.getDay();
+ return[a > 0 && a < 6, ""]
+ }, iso8601Week: function (a) {
+ a = new Date(a.getTime());
+ a.setDate(a.getDate() + 4 - (a.getDay() || 7));
+ var b = a.getTime();
+ a.setMonth(0);
+ a.setDate(1);
+ return Math.floor(Math.round((b - a) / 864E5) / 7) + 1
+ }, parseDate: function (a, b, c) {
+ if (a == null || b == null)throw"Invalid arguments";
+ b = typeof b ==
+ "object" ? b.toString() : b + "";
+ if (b == "")return null;
+ for (var e = (c ? c.shortYearCutoff : null) || this._defaults.shortYearCutoff, f = (c ? c.dayNamesShort : null) || this._defaults.dayNamesShort, h = (c ? c.dayNames : null) || this._defaults.dayNames, i = (c ? c.monthNamesShort : null) || this._defaults.monthNamesShort, g = (c ? c.monthNames : null) || this._defaults.monthNames, k = c = -1, l = -1, u = -1, j = false, o = function (p) {
+ (p = z + 1 < a.length && a.charAt(z + 1) == p) && z++;
+ return p
+ }, m = function (p) {
+ o(p);
+ p = new RegExp("^\\d{1," + (p == "@" ? 14 : p == "!" ? 20 : p == "y" ? 4 : p == "o" ?
+ 3 : 2) + "}");
+ p = b.substring(s).match(p);
+ if (!p)throw"Missing number at position " + s;
+ s += p[0].length;
+ return parseInt(p[0], 10)
+ }, n = function (p, w, H) {
+ p = o(p) ? H : w;
+ for (w = 0; w < p.length; w++)if (b.substr(s, p[w].length).toLowerCase() == p[w].toLowerCase()) {
+ s += p[w].length;
+ return w + 1
+ }
+ throw"Unknown name at position " + s;
+ }, r = function () {
+ if (b.charAt(s) != a.charAt(z))throw"Unexpected literal at position " + s;
+ s++
+ }, s = 0, z = 0; z < a.length; z++)if (j)if (a.charAt(z) == "'" && !o("'"))j = false; else r(); else switch (a.charAt(z)) {
+ case "d":
+ l = m("d");
+ break;
+ case "D":
+ n("D", f, h);
+ break;
+ case "o":
+ u = m("o");
+ break;
+ case "m":
+ k = m("m");
+ break;
+ case "M":
+ k = n("M", i, g);
+ break;
+ case "y":
+ c = m("y");
+ break;
+ case "@":
+ var v = new Date(m("@"));
+ c = v.getFullYear();
+ k = v.getMonth() + 1;
+ l = v.getDate();
+ break;
+ case "!":
+ v = new Date((m("!") - this._ticksTo1970) / 1E4);
+ c = v.getFullYear();
+ k = v.getMonth() + 1;
+ l = v.getDate();
+ break;
+ case "'":
+ if (o("'"))r(); else j = true;
+ break;
+ default:
+ r()
+ }
+ if (c == -1)c = (new Date).getFullYear(); else if (c < 100)c += (new Date).getFullYear() - (new Date).getFullYear() % 100 + (c <= e ? 0 : -100);
+ if (u > -1) {
+ k = 1;
+ l = u;
+ do {
+ e = this._getDaysInMonth(c, k - 1);
+ if (l <= e)break;
+ k++;
+ l -= e
+ } while (1)
+ }
+ v = this._daylightSavingAdjust(new Date(c, k - 1, l));
+ if (v.getFullYear() != c || v.getMonth() + 1 != k || v.getDate() != l)throw"Invalid date";
+ return v
+ }, ATOM: "yy-mm-dd", COOKIE: "D, dd M yy", ISO_8601: "yy-mm-dd", RFC_822: "D, d M y", RFC_850: "DD, dd-M-y", RFC_1036: "D, d M y", RFC_1123: "D, d M yy", RFC_2822: "D, d M yy", RSS: "D, d M y", TICKS: "!", TIMESTAMP: "@", W3C: "yy-mm-dd", _ticksTo1970: (718685 + Math.floor(492.5) - Math.floor(19.7) + Math.floor(4.925)) * 24 *
+ 60 * 60 * 1E7, formatDate: function (a, b, c) {
+ if (!b)return"";
+ var e = (c ? c.dayNamesShort : null) || this._defaults.dayNamesShort, f = (c ? c.dayNames : null) || this._defaults.dayNames, h = (c ? c.monthNamesShort : null) || this._defaults.monthNamesShort;
+ c = (c ? c.monthNames : null) || this._defaults.monthNames;
+ var i = function (o) {
+ (o = j + 1 < a.length && a.charAt(j + 1) == o) && j++;
+ return o
+ }, g = function (o, m, n) {
+ m = "" + m;
+ if (i(o))for (; m.length < n;)m = "0" + m;
+ return m
+ }, k = function (o, m, n, r) {
+ return i(o) ? r[m] : n[m]
+ }, l = "", u = false;
+ if (b)for (var j = 0; j < a.length; j++)if (u)if (a.charAt(j) ==
+ "'" && !i("'"))u = false; else l += a.charAt(j); else switch (a.charAt(j)) {
+ case "d":
+ l += g("d", b.getDate(), 2);
+ break;
+ case "D":
+ l += k("D", b.getDay(), e, f);
+ break;
+ case "o":
+ l += g("o", (b.getTime() - (new Date(b.getFullYear(), 0, 0)).getTime()) / 864E5, 3);
+ break;
+ case "m":
+ l += g("m", b.getMonth() + 1, 2);
+ break;
+ case "M":
+ l += k("M", b.getMonth(), h, c);
+ break;
+ case "y":
+ l += i("y") ? b.getFullYear() : (b.getYear() % 100 < 10 ? "0" : "") + b.getYear() % 100;
+ break;
+ case "@":
+ l += b.getTime();
+ break;
+ case "!":
+ l += b.getTime() * 1E4 + this._ticksTo1970;
+ break;
+ case "'":
+ if (i("'"))l +=
+ "'"; else u = true;
+ break;
+ default:
+ l += a.charAt(j)
+ }
+ return l
+ }, _possibleChars: function (a) {
+ for (var b = "", c = false, e = function (h) {
+ (h = f + 1 < a.length && a.charAt(f + 1) == h) && f++;
+ return h
+ }, f = 0; f < a.length; f++)if (c)if (a.charAt(f) == "'" && !e("'"))c = false; else b += a.charAt(f); else switch (a.charAt(f)) {
+ case "d":
+ case "m":
+ case "y":
+ case "@":
+ b += "0123456789";
+ break;
+ case "D":
+ case "M":
+ return null;
+ case "'":
+ if (e("'"))b += "'"; else c = true;
+ break;
+ default:
+ b += a.charAt(f)
+ }
+ return b
+ }, _get: function (a, b) {
+ return a.settings[b] !== G ? a.settings[b] : this._defaults[b]
+ },
+ _setDateFromField: function (a, b) {
+ if (a.input.val() != a.lastVal) {
+ var c = this._get(a, "dateFormat"), e = a.lastVal = a.input ? a.input.val() : null, f, h;
+ f = h = this._getDefaultDate(a);
+ var i = this._getFormatConfig(a);
+ try {
+ f = this.parseDate(c, e, i) || h
+ } catch (g) {
+ this.log(g);
+ e = b ? "" : e
+ }
+ a.selectedDay = f.getDate();
+ a.drawMonth = a.selectedMonth = f.getMonth();
+ a.drawYear = a.selectedYear = f.getFullYear();
+ a.currentDay = e ? f.getDate() : 0;
+ a.currentMonth = e ? f.getMonth() : 0;
+ a.currentYear = e ? f.getFullYear() : 0;
+ this._adjustInstDate(a)
+ }
+ }, _getDefaultDate: function (a) {
+ return this._restrictMinMax(a,
+ this._determineDate(a, this._get(a, "defaultDate"), new Date))
+ }, _determineDate: function (a, b, c) {
+ var e = function (h) {
+ var i = new Date;
+ i.setDate(i.getDate() + h);
+ return i
+ }, f = function (h) {
+ try {
+ return d.datepicker.parseDate(d.datepicker._get(a, "dateFormat"), h, d.datepicker._getFormatConfig(a))
+ } catch (i) {
+ }
+ var g = (h.toLowerCase().match(/^c/) ? d.datepicker._getDate(a) : null) || new Date, k = g.getFullYear(), l = g.getMonth();
+ g = g.getDate();
+ for (var u = /([+-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g, j = u.exec(h); j;) {
+ switch (j[2] || "d") {
+ case "d":
+ case "D":
+ g +=
+ parseInt(j[1], 10);
+ break;
+ case "w":
+ case "W":
+ g += parseInt(j[1], 10) * 7;
+ break;
+ case "m":
+ case "M":
+ l += parseInt(j[1], 10);
+ g = Math.min(g, d.datepicker._getDaysInMonth(k, l));
+ break;
+ case "y":
+ case "Y":
+ k += parseInt(j[1], 10);
+ g = Math.min(g, d.datepicker._getDaysInMonth(k, l));
+ break
+ }
+ j = u.exec(h)
+ }
+ return new Date(k, l, g)
+ };
+ if (b = (b = b == null ? c : typeof b == "string" ? f(b) : typeof b == "number" ? isNaN(b) ? c : e(b) : b) && b.toString() == "Invalid Date" ? c : b) {
+ b.setHours(0);
+ b.setMinutes(0);
+ b.setSeconds(0);
+ b.setMilliseconds(0)
+ }
+ return this._daylightSavingAdjust(b)
+ },
+ _daylightSavingAdjust: function (a) {
+ if (!a)return null;
+ a.setHours(a.getHours() > 12 ? a.getHours() + 2 : 0);
+ return a
+ }, _setDate: function (a, b, c) {
+ var e = !b, f = a.selectedMonth, h = a.selectedYear;
+ b = this._restrictMinMax(a, this._determineDate(a, b, new Date));
+ a.selectedDay = a.currentDay = b.getDate();
+ a.drawMonth = a.selectedMonth = a.currentMonth = b.getMonth();
+ a.drawYear = a.selectedYear = a.currentYear = b.getFullYear();
+ if ((f != a.selectedMonth || h != a.selectedYear) && !c)this._notifyChange(a);
+ this._adjustInstDate(a);
+ if (a.input)a.input.val(e ?
+ "" : this._formatDate(a))
+ }, _getDate: function (a) {
+ return!a.currentYear || a.input && a.input.val() == "" ? null : this._daylightSavingAdjust(new Date(a.currentYear, a.currentMonth, a.currentDay))
+ }, _generateHTML: function (a) {
+ var b = new Date;
+ b = this._daylightSavingAdjust(new Date(b.getFullYear(), b.getMonth(), b.getDate()));
+ var c = this._get(a, "isRTL"), e = this._get(a, "showButtonPanel"), f = this._get(a, "hideIfNoPrevNext"), h = this._get(a, "navigationAsDateFormat"), i = this._getNumberOfMonths(a), g = this._get(a, "showCurrentAtPos"), k =
+ this._get(a, "stepMonths"), l = i[0] != 1 || i[1] != 1, u = this._daylightSavingAdjust(!a.currentDay ? new Date(9999, 9, 9) : new Date(a.currentYear, a.currentMonth, a.currentDay)), j = this._getMinMaxDate(a, "min"), o = this._getMinMaxDate(a, "max");
+ g = a.drawMonth - g;
+ var m = a.drawYear;
+ if (g < 0) {
+ g += 12;
+ m--
+ }
+ if (o) {
+ var n = this._daylightSavingAdjust(new Date(o.getFullYear(), o.getMonth() - i[0] * i[1] + 1, o.getDate()));
+ for (n = j && n < j ? j : n; this._daylightSavingAdjust(new Date(m, g, 1)) > n;) {
+ g--;
+ if (g < 0) {
+ g = 11;
+ m--
+ }
+ }
+ }
+ a.drawMonth = g;
+ a.drawYear = m;
+ n = this._get(a,
+ "prevText");
+ n = !h ? n : this.formatDate(n, this._daylightSavingAdjust(new Date(m, g - k, 1)), this._getFormatConfig(a));
+ n = this._canAdjustMonth(a, -1, m, g) ? '' + n + " " : f ? "" : '' +
+ n + " ";
+ var r = this._get(a, "nextText");
+ r = !h ? r : this.formatDate(r, this._daylightSavingAdjust(new Date(m, g + k, 1)), this._getFormatConfig(a));
+ f = this._canAdjustMonth(a, +1, m, g) ? '' + r + " " : f ? "" : '' + r + " ";
+ k = this._get(a, "currentText");
+ r = this._get(a, "gotoCurrent") && a.currentDay ? u : b;
+ k = !h ? k : this.formatDate(k, r, this._getFormatConfig(a));
+ h = !a.inline ? '' + this._get(a, "closeText") + " " : "";
+ e = e ? '' + (c ? h : "") + (this._isInRange(a, r) ? '" + k + " " : "") + (c ? "" : h) + "
" : "";
+ h = parseInt(this._get(a, "firstDay"), 10);
+ h = isNaN(h) ? 0 : h;
+ k = this._get(a, "showWeek");
+ r = this._get(a, "dayNames");
+ this._get(a, "dayNamesShort");
+ var s = this._get(a, "dayNamesMin"), z = this._get(a, "monthNames"), v = this._get(a, "monthNamesShort"), p = this._get(a, "beforeShowDay"), w = this._get(a, "showOtherMonths"), H = this._get(a, "selectOtherMonths");
+ this._get(a, "calculateWeek");
+ for (var M = this._getDefaultDate(a), I = "", C = 0; C < i[0]; C++) {
+ for (var N =
+ "", D = 0; D < i[1]; D++) {
+ var J = this._daylightSavingAdjust(new Date(m, g, a.selectedDay)), t = " ui-corner-all", x = "";
+ if (l) {
+ x += ''
+ }
+ x += '
';
+ var A = k ? '' + this._get(a, "weekHeader") + " " : "";
+ for (t = 0; t < 7; t++) {
+ var q = (t + h) % 7;
+ A += "= 5 ? ' class="ui-datepicker-week-end"' : "") + '>' + s[q] + " "
+ }
+ x += A + " ";
+ A = this._getDaysInMonth(m, g);
+ if (m == a.selectedYear && g == a.selectedMonth)a.selectedDay = Math.min(a.selectedDay,
+ A);
+ t = (this._getFirstDayOfMonth(m, g) - h + 7) % 7;
+ A = l ? 6 : Math.ceil((t + A) / 7);
+ q = this._daylightSavingAdjust(new Date(m, g, 1 - t));
+ for (var O = 0; O < A; O++) {
+ x += "";
+ var P = !k ? "" : '' + this._get(a, "calculateWeek")(q) + " ";
+ for (t = 0; t < 7; t++) {
+ var F = p ? p.apply(a.input ? a.input[0] : null, [q]) : [true, ""], B = q.getMonth() != g, K = B && !H || !F[0] || j && q < j || o && q > o;
+ P += '" + (B && !w ? " " : K ? '' + q.getDate() +
+ " " : '' + q.getDate() + " ") + " ";
+ q.setDate(q.getDate() + 1);
+ q = this._daylightSavingAdjust(q)
+ }
+ x += P + " "
+ }
+ g++;
+ if (g > 11) {
+ g = 0;
+ m++
+ }
+ x += "
" + (l ? "
" + (i[0] > 0 && D == i[1] - 1 ? '
' : "") : "");
+ N += x
+ }
+ I += N
+ }
+ I += e + (d.browser.msie && parseInt(d.browser.version, 10) < 7 && !a.inline ? '' :
+ "");
+ a._keyEvent = false;
+ return I
+ }, _generateMonthYearHeader: function (a, b, c, e, f, h, i, g) {
+ var k = this._get(a, "changeMonth"), l = this._get(a, "changeYear"), u = this._get(a, "showMonthAfterYear"), j = '', o = "";
+ if (h || !k)o += '' + i[b] + " "; else {
+ i = e && e.getFullYear() == c;
+ var m = f && f.getFullYear() == c;
+ o += '";
+ for (var n = 0; n < 12; n++)if ((!i || n >= e.getMonth()) && (!m || n <= f.getMonth()))o += '" + g[n] + " ";
+ o += " "
+ }
+ u || (j += o + (h || !(k && l) ? " " : ""));
+ if (h || !l)j += '' + c + " "; else {
+ g = this._get(a, "yearRange").split(":");
+ var r = (new Date).getFullYear();
+ i = function (s) {
+ s = s.match(/c[+-].*/) ? c + parseInt(s.substring(1), 10) : s.match(/[+-].*/) ? r + parseInt(s, 10) : parseInt(s, 10);
+ return isNaN(s) ? r : s
+ };
+ b = i(g[0]);
+ g = Math.max(b,
+ i(g[1] || ""));
+ b = e ? Math.max(b, e.getFullYear()) : b;
+ g = f ? Math.min(g, f.getFullYear()) : g;
+ for (j += '"; b <= g; b++)j += '" + b + " ";
+ j += " "
+ }
+ j += this._get(a, "yearSuffix");
+ if (u)j += (h || !(k && l) ? " " : "") + o;
+ j += "
";
+ return j
+ }, _adjustInstDate: function (a, b, c) {
+ var e =
+ a.drawYear + (c == "Y" ? b : 0), f = a.drawMonth + (c == "M" ? b : 0);
+ b = Math.min(a.selectedDay, this._getDaysInMonth(e, f)) + (c == "D" ? b : 0);
+ e = this._restrictMinMax(a, this._daylightSavingAdjust(new Date(e, f, b)));
+ a.selectedDay = e.getDate();
+ a.drawMonth = a.selectedMonth = e.getMonth();
+ a.drawYear = a.selectedYear = e.getFullYear();
+ if (c == "M" || c == "Y")this._notifyChange(a)
+ }, _restrictMinMax: function (a, b) {
+ var c = this._getMinMaxDate(a, "min");
+ a = this._getMinMaxDate(a, "max");
+ b = c && b < c ? c : b;
+ return b = a && b > a ? a : b
+ }, _notifyChange: function (a) {
+ var b = this._get(a,
+ "onChangeMonthYear");
+ if (b)b.apply(a.input ? a.input[0] : null, [a.selectedYear, a.selectedMonth + 1, a])
+ }, _getNumberOfMonths: function (a) {
+ a = this._get(a, "numberOfMonths");
+ return a == null ? [1, 1] : typeof a == "number" ? [1, a] : a
+ }, _getMinMaxDate: function (a, b) {
+ return this._determineDate(a, this._get(a, b + "Date"), null)
+ }, _getDaysInMonth: function (a, b) {
+ return 32 - (new Date(a, b, 32)).getDate()
+ }, _getFirstDayOfMonth: function (a, b) {
+ return(new Date(a, b, 1)).getDay()
+ }, _canAdjustMonth: function (a, b, c, e) {
+ var f = this._getNumberOfMonths(a);
+ c = this._daylightSavingAdjust(new Date(c, e + (b < 0 ? b : f[0] * f[1]), 1));
+ b < 0 && c.setDate(this._getDaysInMonth(c.getFullYear(), c.getMonth()));
+ return this._isInRange(a, c)
+ }, _isInRange: function (a, b) {
+ var c = this._getMinMaxDate(a, "min");
+ a = this._getMinMaxDate(a, "max");
+ return(!c || b.getTime() >= c.getTime()) && (!a || b.getTime() <= a.getTime())
+ }, _getFormatConfig: function (a) {
+ var b = this._get(a, "shortYearCutoff");
+ b = typeof b != "string" ? b : (new Date).getFullYear() % 100 + parseInt(b, 10);
+ return{shortYearCutoff: b, dayNamesShort: this._get(a,
+ "dayNamesShort"), dayNames: this._get(a, "dayNames"), monthNamesShort: this._get(a, "monthNamesShort"), monthNames: this._get(a, "monthNames")}
+ }, _formatDate: function (a, b, c, e) {
+ if (!b) {
+ a.currentDay = a.selectedDay;
+ a.currentMonth = a.selectedMonth;
+ a.currentYear = a.selectedYear
+ }
+ b = b ? typeof b == "object" ? b : this._daylightSavingAdjust(new Date(e, c, b)) : this._daylightSavingAdjust(new Date(a.currentYear, a.currentMonth, a.currentDay));
+ return this.formatDate(this._get(a, "dateFormat"), b, this._getFormatConfig(a))
+ }});
+ d.fn.datepicker =
+ function (a) {
+ if (!d.datepicker.initialized) {
+ d(document).mousedown(d.datepicker._checkExternalClick).find("body").append(d.datepicker.dpDiv);
+ d.datepicker.initialized = true
+ }
+ var b = Array.prototype.slice.call(arguments, 1);
+ if (typeof a == "string" && (a == "isDisabled" || a == "getDate" || a == "widget"))return d.datepicker["_" + a + "Datepicker"].apply(d.datepicker, [this[0]].concat(b));
+ if (a == "option" && arguments.length == 2 && typeof arguments[1] == "string")return d.datepicker["_" + a + "Datepicker"].apply(d.datepicker, [this[0]].concat(b));
+ return this.each(function () {
+ typeof a == "string" ? d.datepicker["_" + a + "Datepicker"].apply(d.datepicker, [this].concat(b)) : d.datepicker._attachDatepicker(this, a)
+ })
+ };
+ d.datepicker = new L;
+ d.datepicker.initialized = false;
+ d.datepicker.uuid = (new Date).getTime();
+ d.datepicker.version = "1.8.5";
+ window["DP_jQuery_" + y] = d
+})(jQuery);
+;
+/*
+ * jQuery UI Progressbar 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Progressbar
+ *
+ * Depends:
+ * jquery.ui.core.js
+ * jquery.ui.widget.js
+ */
+(function (b, c) {
+ b.widget("ui.progressbar", {options: {value: 0}, min: 0, max: 100, _create: function () {
+ this.element.addClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").attr({role: "progressbar", "aria-valuemin": this.min, "aria-valuemax": this.max, "aria-valuenow": this._value()});
+ this.valueDiv = b("").appendTo(this.element);
+ this._refreshValue()
+ }, destroy: function () {
+ this.element.removeClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow");
+ this.valueDiv.remove();
+ b.Widget.prototype.destroy.apply(this, arguments)
+ }, value: function (a) {
+ if (a === c)return this._value();
+ this._setOption("value", a);
+ return this
+ }, _setOption: function (a, d) {
+ if (a === "value") {
+ this.options.value = d;
+ this._refreshValue();
+ this._trigger("change")
+ }
+ b.Widget.prototype._setOption.apply(this, arguments)
+ }, _value: function () {
+ var a = this.options.value;
+ if (typeof a !== "number")a = 0;
+ return Math.min(this.max, Math.max(this.min, a))
+ }, _refreshValue: function () {
+ var a = this.value();
+ this.valueDiv.toggleClass("ui-corner-right",
+ a === this.max).width(a + "%");
+ this.element.attr("aria-valuenow", a)
+ }});
+ b.extend(b.ui.progressbar, {version: "1.8.5"})
+})(jQuery);
+;
\ No newline at end of file
diff --git a/vendor/treebeard/static/treebeard/treebeard-admin.css b/vendor/treebeard/static/treebeard/treebeard-admin.css
new file mode 100644
index 00000000..dba330b5
--- /dev/null
+++ b/vendor/treebeard/static/treebeard/treebeard-admin.css
@@ -0,0 +1,83 @@
+/* Treebeard Admin */
+
+#roots {
+ margin: 0;
+ padding: 0;
+}
+
+#roots li {
+ list-style: none;
+ padding: 5px !important;
+ line-height: 13px;
+ border-bottom: 1px solid #EEE;
+}
+
+#roots li a {
+ font-weight: bold;
+ font-size: 12px;
+}
+
+#roots li input {
+ margin: 0 5px;
+}
+
+.oder-grabber {
+ width: 1.5em;
+ text-align: center;
+}
+
+.drag-handler span {
+ width: 16px;
+ background: transparent url(expand-collapse.png) no-repeat left -48px;
+ height: 16px;
+ margin: 0 5px;
+ display: inline-block;
+}
+
+.drag-handler span.active {
+ background: transparent url(expand-collapse.png) no-repeat left -32px;
+ cursor: move;
+}
+
+.spacer {
+ width: 10px;
+ margin: 0 10px;
+}
+
+.collapse {
+ width: 16px;
+ height: 16px;
+ display: inline-block;
+ text-indent: -999px;
+}
+
+.collapsed {
+ background: transparent url(expand-collapse.png) no-repeat left -16px;
+}
+
+.expanded {
+ background: transparent url(expand-collapse.png) no-repeat left 0;
+}
+
+#drag_line {
+ border-top: 5px solid #A0A;
+ background: #A0A;
+ display: block;
+ position: absolute;
+}
+
+#drag_line span {
+ position: relative;
+ display: block;
+ width: 100px;
+ background: #FFD;
+ color: #000;
+ left: 100px;
+ text-align: center;
+ border: 1px solid #000;
+ vertical-align: center;
+}
+
+/*tr:target { I'm handling the highlight with js to have more control
+background-color: #FF0;
+}*/
diff --git a/vendor/treebeard/static/treebeard/treebeard-admin.js b/vendor/treebeard/static/treebeard/treebeard-admin.js
new file mode 100644
index 00000000..b8daa6b6
--- /dev/null
+++ b/vendor/treebeard/static/treebeard/treebeard-admin.js
@@ -0,0 +1,314 @@
+(function ($) {
+// Ok, let's do eeet
+
+ ACTIVE_NODE_BG_COLOR = '#B7D7E8';
+ RECENTLY_MOVED_COLOR = '#FFFF00';
+ RECENTLY_MOVED_FADEOUT = '#FFFFFF';
+ ABORT_COLOR = '#EECCCC';
+ DRAG_LINE_COLOR = '#AA00AA';
+
+ RECENTLY_FADE_DURATION = 2000;
+
+// This is the basic Node class, which handles UI tree operations for each 'row'
+ var Node = function (elem) {
+ var $elem = $(elem);
+ var node_id = $elem.attr('node');
+ var parent_id = $elem.attr('parent');
+ var level = parseInt($elem.attr('level'));
+ var children_num = parseInt($elem.attr('children-num'));
+ return {
+ elem: elem,
+ $elem: $elem,
+ node_id: node_id,
+ parent_id: parent_id,
+ level: level,
+ has_children: function () {
+ return children_num > 0;
+ },
+ node_name: function () {
+ // Returns the text of the node
+ return $elem.find('th a:not(.collapse)').text();
+ },
+ is_collapsed: function () {
+ return $elem.find('a.collapse').hasClass('collapsed');
+ },
+ children: function () {
+ return $('tr[parent=' + node_id + ']');
+ },
+ collapse: function () {
+ // For each children, hide it's childrens and so on...
+ $.each(this.children(),function () {
+ var node = new Node(this);
+ node.collapse();
+ }).hide();
+ // Swicth class to set the proprt expand/collapse icon
+ $elem.find('a.collapse').removeClass('expanded').addClass('collapsed');
+ },
+ parent_node: function () {
+ // Returns a Node object of the parent
+ return new Node($('tr[node=' + parent_id + ']', $elem.parent())[0]);
+ },
+ expand: function () {
+ // Display each kid (will display in collapsed state)
+ this.children().show();
+ // Swicth class to set the proprt expand/collapse icon
+ $elem.find('a.collapse').removeClass('collapsed').addClass('expanded');
+
+ },
+ toggle: function () {
+ if (this.is_collapsed()) {
+ this.expand();
+ } else {
+ this.collapse();
+ }
+ },
+ clone: function () {
+ return $elem.clone();
+ }
+ }
+ };
+
+ $(document).ready(function () {
+
+ // begin csrf token code
+ // Taken from http://docs.djangoproject.com/en/dev/ref/contrib/csrf/#ajax
+ $(document).ajaxSend(function (event, xhr, settings) {
+ function getCookie(name) {
+ var cookieValue = null;
+ if (document.cookie && document.cookie != '') {
+ var cookies = document.cookie.split(';');
+ for (var i = 0; i < cookies.length; i++) {
+ var cookie = jQuery.trim(cookies[i]);
+ // Does this cookie string begin with the name we want?
+ if (cookie.substring(0, name.length + 1) == (name + '=')) {
+ cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
+ break;
+ }
+ }
+ }
+ return cookieValue;
+ }
+
+ if (!(/^http:.*/.test(settings.url) || /^https:.*/.test(settings.url))) {
+ // Only send the token to relative URLs i.e. locally.
+ xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
+ }
+ });
+ // end csrf token code
+
+
+ // Don't activate drag or collapse if GET filters are set on the page
+ if ($('#has-filters').val() === "1") {
+ return;
+ }
+
+ $body = $('body');
+
+ // Activate all rows for drag & drop
+ // then bind mouse down event
+ $('td.drag-handler span').addClass('active').bind('mousedown', function (evt) {
+ $ghost = $('
');
+ $drag_line = $('
');
+ $ghost.appendTo($body);
+ $drag_line.appendTo($body);
+
+ var stop_drag = function () {
+ $ghost.remove();
+ $drag_line.remove();
+ $body.enableSelection().unbind('mousemove').unbind('mouseup');
+ node.elem.removeAttribute('style');
+ };
+
+ // Create a clone create the illusion that we're moving the node
+ var node = new Node($(this).closest('tr')[0]);
+ cloned_node = node.clone();
+ node.$elem.css({
+ 'background': ACTIVE_NODE_BG_COLOR
+ });
+
+ $targetRow = null;
+ as_child = false;
+
+ // Now make the new clone move with the mouse
+ $body.disableSelection().bind('mousemove',function (evt2) {
+ $ghost.html(cloned_node).css({ // from FeinCMS :P
+ 'opacity': .8,
+ 'position': 'absolute',
+ 'top': evt2.pageY,
+ 'left': evt2.pageX - 30,
+ 'width': 600
+ });
+ // Iterate through all rows and see where am I moving so I can place
+ // the drag line accordingly
+ rowHeight = node.$elem.height();
+ $('tr', node.$elem.parent()).each(function (index, element) {
+ $row = $(element);
+ rtop = $row.offset().top;
+ // The tooltop will display whether I'm droping the element as
+ // child or sibling
+ $tooltip = $drag_line.find('span');
+ $tooltip.css({
+ 'left': node.$elem.width() - $tooltip.width(),
+ 'height': rowHeight,
+ });
+ node_top = node.$elem.offset().top;
+ // Check if you are dragging over the same node
+ if (evt2.pageY >= node_top && evt2.pageY <= node_top + rowHeight) {
+ $targetRow = null;
+ $tooltip.text(gettext('Abort'));
+ $drag_line.css({
+ 'top': node_top,
+ 'height': rowHeight,
+ 'borderWidth': 0,
+ 'opacity': 0.8,
+ 'backgroundColor': ABORT_COLOR
+ });
+ } else
+ // Check if mouse is over this row
+ if (evt2.pageY >= rtop && evt2.pageY <= rtop + rowHeight / 2) {
+ // The mouse is positioned on the top half of a $row
+ $targetRow = $row;
+ as_child = false;
+ $drag_line.css({
+ 'left': node.$elem.offset().left,
+ 'width': node.$elem.width(),
+ 'top': rtop,
+ 'borderWidth': '5px',
+ 'height': 0,
+ 'opacity': 1
+ });
+ $tooltip.text(gettext('As Sibling'));
+ } else if (evt2.pageY >= rtop + rowHeight / 2 && evt2.pageY <= rtop + rowHeight) {
+ // The mouse is positioned on the bottom half of a row
+ $targetRow = $row;
+ target_node = new Node($targetRow[0]);
+ if (target_node.is_collapsed()) {
+ target_node.expand();
+ }
+ as_child = true;
+ $drag_line.css({
+ 'top': rtop,
+ 'left': node.$elem.offset().left,
+ 'height': rowHeight,
+ 'opacity': 0.4,
+ 'width': node.$elem.width(),
+ 'borderWidth': 0,
+ 'backgroundColor': DRAG_LINE_COLOR
+ });
+ $tooltip.text(gettext('As child'));
+ }
+ });
+ }).bind('mouseup',function () {
+ if ($targetRow !== null) {
+ target_node = new Node($targetRow[0]);
+ if (target_node.node_id !== node.node_id) {
+ /*alert('Insert node ' + node.node_name() + ' as child of: '
+ + target_node.parent_node().node_name() + '\n and sibling of: '
+ + target_node.node_name());*/
+ // Call $.ajax so we can handle the error
+ // On Drop, make an XHR call to perform the node move
+ $.ajax({
+ url: window.MOVE_NODE_ENDPOINT,
+ type: 'POST',
+ data: {
+ node_id: node.node_id,
+ parent_id: target_node.parent_id,
+ sibling_id: target_node.node_id,
+ as_child: as_child ? 1 : 0
+ },
+ complete: function (req, status) {
+ // http://stackoverflow.com/questions/1439895/add-a-hash-with-javascript-to-url-without-scrolling-page/1439910#1439910
+ node.$elem.remove();
+ window.location.hash = 'node-' + node.node_id;
+ window.location.reload();
+ },
+ error: function (req, status, error) {
+ // On error (!200) also reload to display
+ // the message
+ node.$elem.remove();
+ window.location.hash = 'node-' + node.node_id;
+ window.location.reload();
+ }
+ });
+ }
+ }
+ stop_drag();
+ }).bind('keyup', function (kbevt) {
+ // Cancel drag on escape
+ if (kbevt.keyCode === 27) {
+ stop_drag();
+ }
+ });
+ });
+
+ $('a.collapse').click(function () {
+ var node = new Node($(this).closest('tr')[0]); // send the DOM node, not jQ
+ node.toggle();
+ return false;
+ });
+ var hash = window.location.hash;
+ // This is a hack, the actual element's id ends in '-id' but the url's hash
+ // doesn't, I'm doing this to avoid scrolling the page... is that a good thing?
+ if (hash) {
+ $(hash + '-id').animate({
+ backgroundColor: RECENTLY_MOVED_COLOR
+ }, RECENTLY_FADE_DURATION, function () {
+ $(this).animate({
+ backgroundColor: RECENTLY_MOVED_FADEOUT
+ }, RECENTLY_FADE_DURATION, function () {
+ this.removeAttribute('style');
+ });
+ });
+ }
+ });
+})(django.jQuery);
+
+// http://stackoverflow.com/questions/190560/jquery-animate-backgroundcolor/2302005#2302005
+(function (d) {
+ d.each(["backgroundColor", "borderBottomColor", "borderLeftColor", "borderRightColor", "borderTopColor", "color", "outlineColor"], function (f, e) {
+ d.fx.step[e] = function (g) {
+ if (!g.colorInit) {
+ g.start = c(g.elem, e);
+ g.end = b(g.end);
+ g.colorInit = true
+ }
+ g.elem.style[e] = "rgb(" + [Math.max(Math.min(parseInt((g.pos * (g.end[0] - g.start[0])) + g.start[0]), 255), 0), Math.max(Math.min(parseInt((g.pos * (g.end[1] - g.start[1])) + g.start[1]), 255), 0), Math.max(Math.min(parseInt((g.pos * (g.end[2] - g.start[2])) + g.start[2]), 255), 0)].join(",") + ")"
+ }
+ });
+ function b(f) {
+ var e;
+ if (f && f.constructor == Array && f.length == 3) {
+ return f
+ }
+ if (e = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(f)) {
+ return[parseInt(e[1]), parseInt(e[2]), parseInt(e[3])]
+ }
+ if (e = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(f)) {
+ return[parseFloat(e[1]) * 2.55, parseFloat(e[2]) * 2.55, parseFloat(e[3]) * 2.55]
+ }
+ if (e = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(f)) {
+ return[parseInt(e[1], 16), parseInt(e[2], 16), parseInt(e[3], 16)]
+ }
+ if (e = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(f)) {
+ return[parseInt(e[1] + e[1], 16), parseInt(e[2] + e[2], 16), parseInt(e[3] + e[3], 16)]
+ }
+ if (e = /rgba\(0, 0, 0, 0\)/.exec(f)) {
+ return a.transparent
+ }
+ return a[d.trim(f).toLowerCase()]
+ }
+
+ function c(g, e) {
+ var f;
+ do {
+ f = d.css(g, e);
+ if (f != "" && f != "transparent" || d.nodeName(g, "body")) {
+ break
+ }
+ e = "backgroundColor"
+ } while (g = g.parentNode);
+ return b(f)
+ }
+
+ var a = {aqua: [0, 255, 255], azure: [240, 255, 255], beige: [245, 245, 220], black: [0, 0, 0], blue: [0, 0, 255], brown: [165, 42, 42], cyan: [0, 255, 255], darkblue: [0, 0, 139], darkcyan: [0, 139, 139], darkgrey: [169, 169, 169], darkgreen: [0, 100, 0], darkkhaki: [189, 183, 107], darkmagenta: [139, 0, 139], darkolivegreen: [85, 107, 47], darkorange: [255, 140, 0], darkorchid: [153, 50, 204], darkred: [139, 0, 0], darksalmon: [233, 150, 122], darkviolet: [148, 0, 211], fuchsia: [255, 0, 255], gold: [255, 215, 0], green: [0, 128, 0], indigo: [75, 0, 130], khaki: [240, 230, 140], lightblue: [173, 216, 230], lightcyan: [224, 255, 255], lightgreen: [144, 238, 144], lightgrey: [211, 211, 211], lightpink: [255, 182, 193], lightyellow: [255, 255, 224], lime: [0, 255, 0], magenta: [255, 0, 255], maroon: [128, 0, 0], navy: [0, 0, 128], olive: [128, 128, 0], orange: [255, 165, 0], pink: [255, 192, 203], purple: [128, 0, 128], violet: [128, 0, 128], red: [255, 0, 0], silver: [192, 192, 192], white: [255, 255, 255], yellow: [255, 255, 0], transparent: [255, 255, 255]}
+})(django.jQuery);
diff --git a/vendor/treebeard/templates/admin/tree_change_list.html b/vendor/treebeard/templates/admin/tree_change_list.html
new file mode 100644
index 00000000..d98e819c
--- /dev/null
+++ b/vendor/treebeard/templates/admin/tree_change_list.html
@@ -0,0 +1,23 @@
+{# Used for MP and NS trees #}
+{% extends "admin/change_list.html" %}
+{% load admin_list admin_tree i18n %}
+
+{% block extrastyle %}
+ {{ block.super }}
+ {% treebeard_css %}
+{% endblock %}
+
+{% block extrahead %}
+ {{ block.super }}
+ {% treebeard_js %}
+{% endblock %}
+
+{% block result_list %}
+ {% if action_form and actions_on_top and cl.full_result_count %}
+ {% admin_actions %}
+ {% endif %}
+ {% result_tree cl request %}
+ {% if action_form and actions_on_bottom and cl.full_result_count %}
+ {% admin_actions %}
+ {% endif %}
+{% endblock %}
diff --git a/vendor/treebeard/templates/admin/tree_change_list_results.html b/vendor/treebeard/templates/admin/tree_change_list_results.html
new file mode 100644
index 00000000..2b95c2af
--- /dev/null
+++ b/vendor/treebeard/templates/admin/tree_change_list_results.html
@@ -0,0 +1,40 @@
+{% if result_hidden_fields %}
+ {# DIV for HTML validation #}
+ {% for item in result_hidden_fields %}{{ item }}{% endfor %}
+
+{% endif %}
+{% if results %}
+
+
+
+
+ {% for header in result_headers %}
+
+ {% if header.sortable %}{% endif %}
+ {{ header.text|capfirst }}
+ {% if header.sortable %} {% endif %} {% endfor %}
+
+
+
+ {% for node_id, parent_id, node_level, children_num, result in results %}
+
+ {% for item in result %}
+ {% if forloop.counter == 1 %}
+ {% for spacer in item.depth %}
+ {% endfor %}
+ {% endif %}
+ {{ item }}
+ {% endfor %}
+ {% endfor %}
+
+
+
+
+
+{% endif %}
+
diff --git a/vendor/treebeard/templates/admin/tree_list.html b/vendor/treebeard/templates/admin/tree_list.html
new file mode 100644
index 00000000..9a7a27ff
--- /dev/null
+++ b/vendor/treebeard/templates/admin/tree_list.html
@@ -0,0 +1,21 @@
+{# Used for AL trees #}
+{% extends "admin/change_list.html" %}
+{% load admin_list admin_tree_list i18n %}
+
+{% block extrastyle %}
+ {{ block.super }}
+{% endblock %}
+
+{% block extrahead %}
+ {{ block.super }}
+{% endblock %}
+
+{% block result_list %}
+ {% if action_form and actions_on_top and cl.full_result_count %}
+ {% admin_actions %}
+ {% endif %}
+ {% result_tree cl request %}
+ {% if action_form and actions_on_bottom and cl.full_result_count %}
+ {% admin_actions %}
+ {% endif %}
+{% endblock %}
diff --git a/vendor/treebeard/templates/admin/tree_list_results.html b/vendor/treebeard/templates/admin/tree_list_results.html
new file mode 100644
index 00000000..a4fe2c22
--- /dev/null
+++ b/vendor/treebeard/templates/admin/tree_list_results.html
@@ -0,0 +1,7 @@
+{% if results %}
+
+ {% for result in results %}
+ {{ result }}
+ {% endfor %}
+
+{% endif %}
diff --git a/vendor/treebeard/templatetags/__init__.py b/vendor/treebeard/templatetags/__init__.py
new file mode 100644
index 00000000..450fe445
--- /dev/null
+++ b/vendor/treebeard/templatetags/__init__.py
@@ -0,0 +1,12 @@
+from django.template import Variable, VariableDoesNotExist
+
+
+action_form_var = Variable('action_form')
+
+
+def needs_checkboxes(context):
+ try:
+ return action_form_var.resolve(context) is not None
+ except VariableDoesNotExist:
+ return False
+
diff --git a/vendor/treebeard/templatetags/admin_tree.py b/vendor/treebeard/templatetags/admin_tree.py
new file mode 100644
index 00000000..df7656a5
--- /dev/null
+++ b/vendor/treebeard/templatetags/admin_tree.py
@@ -0,0 +1,302 @@
+# -*- coding: utf-8 -*-
+"""
+Templatetags for django-treebeard to add drag and drop capabilities to the
+nodes change list - @jjdelc
+
+"""
+
+import datetime
+import sys
+
+import django
+from django.db import models
+from django.conf import settings
+from django.contrib.admin.templatetags.admin_list import (
+ result_headers, result_hidden_fields)
+try:
+ from django.contrib.admin.utils import (
+ lookup_field, display_for_field, display_for_value)
+except ImportError: # < Django 1.8
+ from django.contrib.admin.util import (
+ lookup_field, display_for_field, display_for_value)
+from django.core.exceptions import ObjectDoesNotExist
+from django.template import Library
+from django.utils.html import conditional_escape
+from django.utils.safestring import mark_safe
+from django.utils.translation import gettext_lazy as _
+
+
+if sys.version < '3':
+ import codecs
+
+ def u(x):
+ return codecs.unicode_escape_decode(x)[0]
+else:
+ def u(x):
+ return x
+
+register = Library()
+
+if sys.version_info >= (3, 0):
+ from django.utils.encoding import force_str
+ from urllib.parse import urljoin
+else:
+ from django.utils.encoding import force_unicode as force_str
+ from urlparse import urljoin
+
+
+from django.utils.html import format_html
+
+from treebeard.templatetags import needs_checkboxes
+
+
+def get_result_and_row_class(cl, field_name, result):
+ if django.VERSION >= (1, 9):
+ empty_value_display = cl.model_admin.get_empty_value_display()
+ else:
+ from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
+ empty_value_display = EMPTY_CHANGELIST_VALUE
+ row_classes = ['field-%s' % field_name]
+ try:
+ f, attr, value = lookup_field(field_name, result, cl.model_admin)
+ except ObjectDoesNotExist:
+ result_repr = empty_value_display
+ else:
+ if django.VERSION >= (1, 9):
+ empty_value_display = getattr(
+ attr, 'empty_value_display', empty_value_display)
+ if f is None:
+ if field_name == 'action_checkbox':
+ row_classes = ['action-checkbox']
+ allow_tags = getattr(attr, 'allow_tags', False)
+ boolean = getattr(attr, 'boolean', False)
+ if django.VERSION >= (1, 9):
+ result_repr = display_for_value(
+ value, empty_value_display, boolean)
+ else:
+ result_repr = display_for_value(value, boolean)
+ # Strip HTML tags in the resulting text, except if the
+ # function has an "allow_tags" attribute set to True.
+ # WARNING: this will be deprecated in Django 2.0
+ if allow_tags:
+ result_repr = mark_safe(result_repr)
+ if isinstance(value, (datetime.date, datetime.time)):
+ row_classes.append('nowrap')
+ else:
+ related_field_name = 'rel' if django.VERSION <= (2, 0) else 'remote_field'
+ if isinstance(getattr(f, related_field_name), models.ManyToOneRel):
+ field_val = getattr(result, f.name)
+ if field_val is None:
+ result_repr = empty_value_display
+ else:
+ result_repr = field_val
+ else:
+ if django.VERSION >= (1, 9):
+ result_repr = display_for_field(
+ value, f, empty_value_display)
+ else:
+ result_repr = display_for_field(value, f)
+ if isinstance(f, (models.DateField, models.TimeField,
+ models.ForeignKey)):
+ row_classes.append('nowrap')
+ if force_str(result_repr) == '':
+ result_repr = mark_safe(' ')
+ row_class = mark_safe(' class="%s"' % ' '.join(row_classes))
+ return result_repr, row_class
+
+
+def get_spacer(first, result):
+ if first:
+ spacer = ' ' * (
+ result.get_depth() - 1)
+ else:
+ spacer = ''
+
+ return spacer
+
+
+def get_collapse(result):
+ if result.get_children_count():
+ collapse = (''
+ '- ')
+ else:
+ collapse = ' '
+
+ return collapse
+
+
+def get_drag_handler(first):
+ drag_handler = ''
+ if first:
+ drag_handler = (''
+ ' ')
+ return drag_handler
+
+
+def items_for_result(cl, result, form):
+ """
+ Generates the actual list of data.
+
+ @jjdelc:
+ This has been shamelessly copied from original
+ django.contrib.admin.templatetags.admin_list.items_for_result
+ in order to alter the dispay for the first element
+ """
+ first = True
+ pk = cl.lookup_opts.pk.attname
+ for field_name in cl.list_display:
+ result_repr, row_class = get_result_and_row_class(cl, field_name,
+ result)
+ # If list_display_links not defined, add the link tag to the
+ # first field
+ if (first and not cl.list_display_links) or \
+ field_name in cl.list_display_links:
+ table_tag = {True: 'th', False: 'td'}[first]
+ # This spacer indents the nodes based on their depth
+ spacer = get_spacer(first, result)
+ # This shows a collapse or expand link for nodes with childs
+ collapse = get_collapse(result)
+ # Add a before the first col to show the drag handler
+ drag_handler = get_drag_handler(first)
+ first = False
+ url = cl.url_for_result(result)
+ # Convert the pk to something that can be used in Javascript.
+ # Problem cases are long ints (23L) and non-ASCII strings.
+ if cl.to_field:
+ attr = str(cl.to_field)
+ else:
+ attr = pk
+ value = result.serializable_value(attr)
+ result_id = "'%s'" % force_str(value)
+ onclickstr = (
+ ' onclick="opener.dismissRelatedLookupPopup(window, %s);'
+ ' return false;"')
+ yield mark_safe(
+ u('%s<%s%s>%s %s %s %s>') % (
+ drag_handler, table_tag, row_class, spacer, collapse, url,
+ (cl.is_popup and onclickstr % result_id or ''),
+ conditional_escape(result_repr), table_tag))
+ else:
+ # By default the fields come from ModelAdmin.list_editable, but if
+ # we pull the fields out of the form instead of list_editable
+ # custom admins can provide fields on a per request basis
+ if (
+ form and
+ field_name in form.fields and
+ not (
+ field_name == cl.model._meta.pk.name and
+ form[cl.model._meta.pk.name].is_hidden
+ )
+ ):
+ bf = form[field_name]
+ result_repr = mark_safe(force_str(bf.errors) + force_str(bf))
+ yield format_html(u('{1} '), row_class, result_repr)
+ if form and not form[cl.model._meta.pk.name].is_hidden:
+ yield format_html(u('{0} '),
+ force_str(form[cl.model._meta.pk.name]))
+
+
+def get_parent_id(node):
+ """Return the node's parent id or 0 if node is a root node."""
+ if node.is_root():
+ return 0
+ return node.get_parent().pk
+
+
+def results(cl):
+ if cl.formset:
+ for res, form in zip(cl.result_list, cl.formset.forms):
+ yield (res.pk, get_parent_id(res), res.get_depth(),
+ res.get_children_count(),
+ list(items_for_result(cl, res, form)))
+ else:
+ for res in cl.result_list:
+ yield (res.pk, get_parent_id(res), res.get_depth(),
+ res.get_children_count(),
+ list(items_for_result(cl, res, None)))
+
+
+def check_empty_dict(GET_dict):
+ """
+ Returns True if the GET querstring contains on values, but it can contain
+ empty keys.
+ This is better than doing not bool(request.GET) as an empty key will return
+ True
+ """
+ empty = True
+ for k, v in GET_dict.items():
+ # Don't disable on p(age) or 'all' GET param
+ if v and k != 'p' and k != 'all':
+ empty = False
+ return empty
+
+
+@register.inclusion_tag(
+ 'admin/tree_change_list_results.html', takes_context=True)
+def result_tree(context, cl, request):
+ """
+ Added 'filtered' param, so the template's js knows whether the results have
+ been affected by a GET param or not. Only when the results are not filtered
+ you can drag and sort the tree
+ """
+
+ # Here I'm adding an extra col on pos 2 for the drag handlers
+ headers = list(result_headers(cl))
+ headers.insert(1 if needs_checkboxes(context) else 0, {
+ 'text': '+',
+ 'sortable': True,
+ 'url': request.path,
+ 'tooltip': _('Return to ordered tree'),
+ 'class_attrib': mark_safe(' class="oder-grabber"')
+ })
+ return {
+ 'filtered': not check_empty_dict(request.GET),
+ 'result_hidden_fields': list(result_hidden_fields(cl)),
+ 'result_headers': headers,
+ 'results': list(results(cl)),
+ }
+
+
+def get_static_url():
+ """Return a base static url, always ending with a /"""
+ path = getattr(settings, 'STATIC_URL', None)
+ if not path:
+ path = getattr(settings, 'MEDIA_URL', None)
+ if not path:
+ path = '/'
+ return path
+
+
+@register.simple_tag
+def treebeard_css():
+ """
+ Template tag to print out the proper tag to include a custom .css
+ """
+ css_file = urljoin(get_static_url(), 'treebeard/treebeard-admin.css')
+ return format_html(
+ """ """,
+ mark_safe(css_file)
+ )
+
+
+@register.simple_tag
+def treebeard_js():
+ """
+ Template tag to print out the proper tag to include a custom .js
+ """
+ path = get_static_url()
+ js_file = urljoin(path, 'treebeard/treebeard-admin.js')
+ jquery_ui = urljoin(path, 'treebeard/jquery-ui-1.8.5.custom.min.js')
+
+ # Jquery UI is needed to call disableSelection() on drag and drop so
+ # text selections arent marked while dragging a table row
+ # http://www.lokkju.com/blog/archives/143
+ TEMPLATE = (
+ ''
+ ''
+ ''
+ '')
+ return format_html(
+ TEMPLATE, "jsi18n", mark_safe(js_file), mark_safe(jquery_ui))
diff --git a/vendor/treebeard/templatetags/admin_tree_list.py b/vendor/treebeard/templatetags/admin_tree_list.py
new file mode 100644
index 00000000..156d9da2
--- /dev/null
+++ b/vendor/treebeard/templatetags/admin_tree_list.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+
+from django.template import Library
+from django.utils.html import format_html
+from django.utils.safestring import mark_safe
+from treebeard.templatetags import needs_checkboxes
+from treebeard.admin import TO_FIELD_VAR
+
+
+register = Library()
+CHECKBOX_TMPL = (' ')
+
+
+def _line(context, node, request):
+ if TO_FIELD_VAR in request.GET and request.GET[TO_FIELD_VAR] == 'id':
+ raw_id_fields = format_html("""
+ onclick="opener.dismissRelatedLookupPopup(window, '{}'); return false;"
+ """, mark_safe(node.pk))
+ else:
+ raw_id_fields = ''
+ output = ''
+ if needs_checkboxes(context):
+ output += format_html(CHECKBOX_TMPL, mark_safe(node.pk))
+ return output + format_html(
+ '{} ',
+ mark_safe(node.pk), mark_safe(raw_id_fields), mark_safe(str(node)))
+
+
+def _subtree(context, node, request):
+ tree = ''
+ for subnode in node.get_children():
+ tree += format_html(
+ '{} ',
+ mark_safe(_subtree(context, subnode, request)))
+ if tree:
+ tree = format_html('', mark_safe(tree))
+ return _line(context, node, request) + tree
+
+
+@register.simple_tag(takes_context=True)
+def result_tree(context, cl, request):
+ tree = ''
+ for root_node in cl.model.get_root_nodes():
+ tree += format_html(
+ '{} ', mark_safe(_subtree(context, root_node, request)))
+ return format_html("", mark_safe(tree))
diff --git a/vendor/treebeard/tests/__init__.py b/vendor/treebeard/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/vendor/treebeard/tests/admin.py b/vendor/treebeard/tests/admin.py
new file mode 100644
index 00000000..5ebff759
--- /dev/null
+++ b/vendor/treebeard/tests/admin.py
@@ -0,0 +1,18 @@
+import itertools
+
+from django.contrib import admin
+from treebeard.admin import admin_factory
+from treebeard.forms import movenodeform_factory
+
+from treebeard.tests.models import BASE_MODELS, UNICODE_MODELS, DEP_MODELS
+
+
+def register(admin_site, model):
+ form_class = movenodeform_factory(model)
+ admin_class = admin_factory(form_class)
+ admin_site.register(model, admin_class)
+
+
+def register_all(admin_site=admin.site):
+ for model in itertools.chain(BASE_MODELS, UNICODE_MODELS, DEP_MODELS):
+ register(admin_site, model)
diff --git a/vendor/treebeard/tests/conftest.py b/vendor/treebeard/tests/conftest.py
new file mode 100644
index 00000000..dcae08e8
--- /dev/null
+++ b/vendor/treebeard/tests/conftest.py
@@ -0,0 +1,34 @@
+"""
+Py.test configuration file
+
+Maik says: There's some magic in here that I don't fully understand. Normally, one would
+use pytest-django to run the tests, which takes care of all the Django-related testcase
+set up and tear down. But that doesn't work because the current tests attempt to access the
+database not just in tests, but in their set up methods, which trips up the
+"can't access database without mark.django_db" no matter if tests are marked as being
+allowed to access the database or not.
+
+So instead, we run the tests without pytest-django and make it work ourselves.
+"""
+import os
+
+
+os.environ['DJANGO_SETTINGS_MODULE'] = 'treebeard.tests.settings'
+
+import django
+from django.test.utils import setup_test_environment, teardown_test_environment
+from django.db import connection
+
+
+def pytest_report_header(config):
+ return 'Django: ' + django.get_version()
+
+
+def pytest_configure(config):
+ django.setup()
+ setup_test_environment()
+ connection.creation.create_test_db(verbosity=2, autoclobber=True)
+
+
+def pytest_unconfigure(config):
+ teardown_test_environment()
diff --git a/vendor/treebeard/tests/jenkins/rm_workspace_coverage.py b/vendor/treebeard/tests/jenkins/rm_workspace_coverage.py
new file mode 100644
index 00000000..bcb13e2f
--- /dev/null
+++ b/vendor/treebeard/tests/jenkins/rm_workspace_coverage.py
@@ -0,0 +1,23 @@
+"""Remove `.coverage.$HOST.$ID` files from previous runs.
+
+In Python because of portability with Windows.
+"""
+
+import sys
+
+import os
+import os.path
+
+
+def main():
+ workspace = os.environ['WORKSPACE']
+ for filename in os.listdir(os.path.join(workspace, ".tests")):
+ if filename.startswith('coverage.'):
+ file_full_name = os.path.join(workspace, filename)
+ sys.stdout.write(
+ '* Removing old .coverage file: `%s`\n' % file_full_name)
+ os.unlink(file_full_name)
+ sys.stdout.flush()
+
+if __name__ == '__main__':
+ main()
diff --git a/vendor/treebeard/tests/jenkins/toxhelper.py b/vendor/treebeard/tests/jenkins/toxhelper.py
new file mode 100644
index 00000000..402f5954
--- /dev/null
+++ b/vendor/treebeard/tests/jenkins/toxhelper.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python
+""" toxhelper is a simple wrapper of pytest and coverage to be used with tox.
+
+It is specially useful to avoid path and interpreter problems while running
+tests with jenkins in OS X, Linux and Windows using the same configuration.
+
+See https://tabo.pe/jenkins/ for the results.
+"""
+
+import sys
+
+import os
+import os.path
+import pytest
+
+from coverage import coverage
+
+
+def run_the_tests():
+ tests_dir = os.path.join(os.path.dirname(__file__), "../../../.tests")
+ if not os.path.isdir(tests_dir):
+ os.mkdir(tests_dir)
+ if 'TOX_DB' in os.environ:
+ os.environ['DATABASE_HOST'], os.environ['DATABASE_PORT'] = {
+ 'pgsql': ('dummy_test_database_server', '5434'),
+ 'mysql': ('dummy_test_database_server', '3308'),
+ 'sqlite': ('', ''),
+ }[os.environ['TOX_DB']]
+ cov = coverage()
+ cov.start()
+ test_result = pytest.main(sys.argv[1:])
+ cov.stop()
+ cov.save()
+ return test_result
+
+if __name__ == '__main__':
+ sys.exit(run_the_tests())
diff --git a/vendor/treebeard/tests/models.py b/vendor/treebeard/tests/models.py
new file mode 100644
index 00000000..763aa53a
--- /dev/null
+++ b/vendor/treebeard/tests/models.py
@@ -0,0 +1,265 @@
+from django.db import models
+from django.contrib.auth.models import User
+
+from treebeard.mp_tree import MP_Node
+from treebeard.al_tree import AL_Node
+from treebeard.ns_tree import NS_Node
+
+
+class RelatedModel(models.Model):
+ desc = models.CharField(max_length=255)
+
+ def __str__(self):
+ return self.desc
+
+
+class MP_TestNode(MP_Node):
+ steplen = 3
+
+ desc = models.CharField(max_length=255)
+
+ def __str__(self): # pragma: no cover
+ return 'Node %d' % self.pk
+
+
+class MP_UnicodeNode(MP_Node):
+ steplen = 3
+
+ desc = models.CharField(max_length=255)
+
+ def __str__(self): # pragma: no cover
+ return self.desc
+
+
+class MP_TestNodeSomeDep(models.Model):
+ node = models.ForeignKey(MP_TestNode, on_delete=models.CASCADE)
+
+ def __str__(self): # pragma: no cover
+ return 'Node %d' % self.pk
+
+
+class MP_TestNodeRelated(MP_Node):
+ steplen = 3
+
+ desc = models.CharField(max_length=255)
+ related = models.ForeignKey(RelatedModel, on_delete=models.CASCADE)
+
+ def __str__(self): # pragma: no cover
+ return 'Node %d' % self.pk
+
+
+class MP_TestNodeInherited(MP_TestNode):
+ extra_desc = models.CharField(max_length=255)
+
+
+class NS_TestNode(NS_Node):
+ desc = models.CharField(max_length=255)
+
+ def __str__(self): # pragma: no cover
+ return 'Node %d' % self.pk
+
+
+class NS_UnicodetNode(NS_Node):
+ desc = models.CharField(max_length=255)
+
+ def __str__(self): # pragma: no cover
+ return self.desc
+
+
+class NS_TestNodeSomeDep(models.Model):
+ node = models.ForeignKey(NS_TestNode, on_delete=models.CASCADE)
+
+ def __str__(self): # pragma: no cover
+ return 'Node %d' % self.pk
+
+
+class NS_TestNodeRelated(NS_Node):
+ desc = models.CharField(max_length=255)
+ related = models.ForeignKey(RelatedModel, on_delete=models.CASCADE)
+
+ def __str__(self): # pragma: no cover
+ return 'Node %d' % self.pk
+
+
+class NS_TestNodeInherited(NS_TestNode):
+ extra_desc = models.CharField(max_length=255)
+
+
+class AL_TestNode(AL_Node):
+ parent = models.ForeignKey('self',
+ related_name='children_set',
+ null=True,
+ db_index=True,
+ on_delete=models.CASCADE)
+ sib_order = models.PositiveIntegerField()
+ desc = models.CharField(max_length=255)
+
+ def __str__(self): # pragma: no cover
+ return 'Node %d' % self.pk
+
+
+class AL_UnicodeNode(AL_Node):
+ parent = models.ForeignKey('self',
+ related_name='children_set',
+ null=True,
+ db_index=True,
+ on_delete=models.CASCADE)
+ sib_order = models.PositiveIntegerField()
+ desc = models.CharField(max_length=255)
+
+ def __str__(self): # pragma: no cover
+ return self.desc
+
+
+class AL_TestNodeSomeDep(models.Model):
+ node = models.ForeignKey(AL_TestNode, on_delete=models.CASCADE)
+
+ def __str__(self): # pragma: no cover
+ return 'Node %d' % self.pk
+
+
+class AL_TestNodeRelated(AL_Node):
+ parent = models.ForeignKey('self',
+ related_name='children_set',
+ null=True,
+ db_index=True,
+ on_delete=models.CASCADE)
+ sib_order = models.PositiveIntegerField()
+ desc = models.CharField(max_length=255)
+ related = models.ForeignKey(RelatedModel, on_delete=models.CASCADE)
+
+ def __str__(self): # pragma: no cover
+ return 'Node %d' % self.pk
+
+
+class AL_TestNodeInherited(AL_TestNode):
+ extra_desc = models.CharField(max_length=255)
+
+
+class MP_TestNodeSorted(MP_Node):
+ steplen = 1
+ node_order_by = ['val1', 'val2', 'desc']
+ val1 = models.IntegerField()
+ val2 = models.IntegerField()
+ desc = models.CharField(max_length=255)
+
+ def __str__(self): # pragma: no cover
+ return 'Node %d' % self.pk
+
+
+class NS_TestNodeSorted(NS_Node):
+ node_order_by = ['val1', 'val2', 'desc']
+ val1 = models.IntegerField()
+ val2 = models.IntegerField()
+ desc = models.CharField(max_length=255)
+
+ def __str__(self): # pragma: no cover
+ return 'Node %d' % self.pk
+
+
+class AL_TestNodeSorted(AL_Node):
+ parent = models.ForeignKey('self',
+ related_name='children_set',
+ null=True,
+ db_index=True,
+ on_delete=models.CASCADE)
+ node_order_by = ['val1', 'val2', 'desc']
+ val1 = models.IntegerField()
+ val2 = models.IntegerField()
+ desc = models.CharField(max_length=255)
+
+ def __str__(self): # pragma: no cover
+ return 'Node %d' % self.pk
+
+
+class MP_TestNodeAlphabet(MP_Node):
+ steplen = 2
+
+ numval = models.IntegerField()
+
+ def __str__(self): # pragma: no cover
+ return 'Node %d' % self.pk
+
+
+class MP_TestNodeSmallStep(MP_Node):
+ steplen = 1
+ alphabet = '0123456789'
+
+ def __str__(self): # pragma: no cover
+ return 'Node %d' % self.pk
+
+
+class MP_TestNodeSortedAutoNow(MP_Node):
+ desc = models.CharField(max_length=255)
+ created = models.DateTimeField(auto_now_add=True)
+
+ node_order_by = ['created']
+
+ def __str__(self): # pragma: no cover
+ return 'Node %d' % self.pk
+
+
+class MP_TestNodeShortPath(MP_Node):
+ steplen = 1
+ alphabet = '01234'
+ desc = models.CharField(max_length=255)
+
+ def __str__(self): # pragma: no cover
+ return 'Node %d' % self.pk
+
+
+# This is how you change the default fields defined in a Django abstract class
+# (in this case, MP_Node), since Django doesn't allow overriding fields, only
+# mehods and attributes
+MP_TestNodeShortPath._meta.get_field('path').max_length = 4
+
+
+class MP_TestNode_Proxy(MP_TestNode):
+ class Meta:
+ proxy = True
+
+
+class NS_TestNode_Proxy(NS_TestNode):
+ class Meta:
+ proxy = True
+
+
+class AL_TestNode_Proxy(AL_TestNode):
+ class Meta:
+ proxy = True
+
+
+class MP_TestSortedNodeShortPath(MP_Node):
+ steplen = 1
+ alphabet = '01234'
+ desc = models.CharField(max_length=255)
+
+ node_order_by = ['desc']
+
+ def __str__(self): # pragma: no cover
+ return 'Node %d' % self.pk
+
+
+MP_TestSortedNodeShortPath._meta.get_field('path').max_length = 4
+
+
+class MP_TestManyToManyWithUser(MP_Node):
+ name = models.CharField(max_length=255)
+ users = models.ManyToManyField(User)
+
+
+BASE_MODELS = AL_TestNode, MP_TestNode, NS_TestNode
+PROXY_MODELS = AL_TestNode_Proxy, MP_TestNode_Proxy, NS_TestNode_Proxy
+SORTED_MODELS = AL_TestNodeSorted, MP_TestNodeSorted, NS_TestNodeSorted
+DEP_MODELS = AL_TestNodeSomeDep, MP_TestNodeSomeDep, NS_TestNodeSomeDep
+MP_SHORTPATH_MODELS = MP_TestNodeShortPath, MP_TestSortedNodeShortPath
+RELATED_MODELS = AL_TestNodeRelated, MP_TestNodeRelated, NS_TestNodeRelated
+UNICODE_MODELS = AL_UnicodeNode, MP_UnicodeNode, NS_UnicodetNode
+INHERITED_MODELS = (
+ AL_TestNodeInherited, MP_TestNodeInherited, NS_TestNodeInherited
+)
+
+
+def empty_models_tables(models):
+ for model in models:
+ model.objects.all().delete()
diff --git a/vendor/treebeard/tests/settings.py b/vendor/treebeard/tests/settings.py
new file mode 100644
index 00000000..39c9d43b
--- /dev/null
+++ b/vendor/treebeard/tests/settings.py
@@ -0,0 +1,110 @@
+"""
+Django settings for testing treebeard
+"""
+
+import os
+import django
+
+
+def get_db_conf():
+ """
+ Configures database according to the DATABASE_ENGINE environment
+ variable. Defaults to SQlite.
+
+ This method is used to let Travis run against different database backends.
+ """
+ database_engine = os.environ.get('DATABASE_ENGINE', 'sqlite')
+ if database_engine == 'sqlite':
+ return {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': ':memory:'
+ }
+ elif database_engine == 'psql':
+ return {
+ 'ENGINE': 'django.db.backends.postgresql_psycopg2',
+ 'NAME': 'travis_ci_test',
+ 'USER': 'postgres',
+ 'PASSWORD': '',
+ 'HOST': '127.0.0.1',
+ 'PORT': '',
+ }
+ elif database_engine == "mysql":
+ return {
+ 'ENGINE': 'django.db.backends.mysql',
+ 'NAME': 'travis_ci_test',
+ 'USER': 'travis',
+ 'PASSWORD': '',
+ 'HOST': '127.0.0.1',
+ 'PORT': '',
+ }
+ elif database_engine == "mssql":
+ return {
+ 'ENGINE': 'sql_server.pyodbc',
+ 'NAME': 'master',
+ 'USER': 'sa',
+ 'PASSWORD': 'Password12!',
+ 'HOST': '(local)\SQL2016',
+ 'PORT': '',
+ 'OPTIONS': {
+ 'driver': 'SQL Server Native Client 11.0',
+ 'MARS_Connection': 'True',
+ },
+ }
+
+DATABASES = {'default': get_db_conf()}
+SECRET_KEY = '7r33b34rd'
+
+INSTALLED_APPS = [
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.admin',
+ 'django.contrib.messages',
+ 'treebeard',
+ 'treebeard.tests'
+]
+
+# This little hacks forces Django into the old syncdb behaviour,
+# creating models without migrations.
+
+if django.VERSION >= (1, 9):
+ MIGRATION_MODULES = {app.split('.')[-1]: None for app in INSTALLED_APPS}
+else:
+ class DisableMigrations(object):
+
+ def __contains__(self, item):
+ return True
+
+ def __getitem__(self, item):
+ return "notmigrations"
+
+
+ MIGRATION_MODULES = DisableMigrations()
+
+MIDDLEWARE_CLASSES = [
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware'
+]
+
+ROOT_URLCONF = 'treebeard.tests.urls'
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.contrib.auth.context_processors.auth',
+ 'django.template.context_processors.debug',
+ 'django.template.context_processors.i18n',
+ 'django.template.context_processors.media',
+ 'django.template.context_processors.static',
+ 'django.template.context_processors.tz',
+ 'django.template.context_processors.request',
+ 'django.contrib.messages.context_processors.messages',
+ ],
+ },
+ },
+]
diff --git a/vendor/treebeard/tests/test_treebeard.py b/vendor/treebeard/tests/test_treebeard.py
new file mode 100644
index 00000000..9756db86
--- /dev/null
+++ b/vendor/treebeard/tests/test_treebeard.py
@@ -0,0 +1,2704 @@
+# -*- coding: utf-8 -*-
+"""Unit/Functional tests"""
+
+from __future__ import unicode_literals
+import datetime
+import os
+
+from django.contrib.admin.sites import AdminSite
+from django.contrib.admin.views.main import ChangeList
+from django.contrib.auth.models import User
+from django.contrib.messages.storage.fallback import FallbackStorage
+from django.db.models import Q
+from django.template import Template, Context
+from django.test import TestCase
+from django.test.client import RequestFactory
+import pytest
+
+from treebeard import numconv
+from treebeard.admin import admin_factory, TO_FIELD_VAR
+from treebeard.exceptions import InvalidPosition, InvalidMoveToDescendant,\
+ PathOverflow, MissingNodeOrderBy, NodeAlreadySaved
+from treebeard.forms import movenodeform_factory
+from treebeard.templatetags.admin_tree import get_static_url
+from treebeard.tests import models
+from treebeard.tests.admin import register_all as admin_register_all
+
+
+admin_register_all()
+
+
+BASE_DATA = [
+ {'data': {'desc': '1'}},
+ {'data': {'desc': '2'}, 'children': [
+ {'data': {'desc': '21'}},
+ {'data': {'desc': '22'}},
+ {'data': {'desc': '23'}, 'children': [
+ {'data': {'desc': '231'}},
+ ]},
+ {'data': {'desc': '24'}},
+ ]},
+ {'data': {'desc': '3'}},
+ {'data': {'desc': '4'}, 'children': [
+ {'data': {'desc': '41'}},
+ ]}]
+UNCHANGED = [
+ ('1', 1, 0),
+ ('2', 1, 4),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 1),
+ ('231', 3, 0),
+ ('24', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+
+
+def _prepare_db_test(request):
+ case = TestCase(methodName='__init__')
+ case._pre_setup()
+ request.addfinalizer(case._post_teardown)
+ return request.param
+
+
+def idfn(fixture_value):
+ return fixture_value.__name__
+
+@pytest.fixture(scope='function',
+ params=models.BASE_MODELS + models.PROXY_MODELS,
+ ids=idfn)
+def model(request):
+ return _prepare_db_test(request)
+
+
+@pytest.fixture(scope='function', params=models.BASE_MODELS, ids=idfn)
+def model_without_proxy(request):
+ return _prepare_db_test(request)
+
+
+@pytest.fixture(scope='function', params=models.UNICODE_MODELS, ids=idfn)
+def model_with_unicode(request):
+ return _prepare_db_test(request)
+
+
+@pytest.fixture(scope='function', params=models.SORTED_MODELS, ids=idfn)
+def sorted_model(request):
+ return _prepare_db_test(request)
+
+
+@pytest.fixture(scope='function', params=models.RELATED_MODELS, ids=idfn)
+def related_model(request):
+ return _prepare_db_test(request)
+
+
+@pytest.fixture(scope='function', params=models.INHERITED_MODELS, ids=idfn)
+def inherited_model(request):
+ return _prepare_db_test(request)
+
+
+@pytest.fixture(scope='function', params=models.MP_SHORTPATH_MODELS, ids=idfn)
+def mpshort_model(request):
+ return _prepare_db_test(request)
+
+
+@pytest.fixture(scope='function', params=[models.MP_TestNodeShortPath], ids=idfn)
+def mpshortnotsorted_model(request):
+ return _prepare_db_test(request)
+
+
+@pytest.fixture(scope='function', params=[models.MP_TestNodeAlphabet], ids=idfn)
+def mpalphabet_model(request):
+ return _prepare_db_test(request)
+
+
+@pytest.fixture(scope='function', params=[models.MP_TestNodeSortedAutoNow], ids=idfn)
+def mpsortedautonow_model(request):
+ return _prepare_db_test(request)
+
+
+@pytest.fixture(scope='function', params=[models.MP_TestNodeSmallStep])
+def mpsmallstep_model(request):
+ return _prepare_db_test(request)
+
+
+@pytest.fixture(scope='function', params=[models.MP_TestManyToManyWithUser])
+def mpm2muser_model(request):
+ return _prepare_db_test(request)
+
+
+class TestTreeBase(object):
+ def got(self, model):
+ if model in [models.NS_TestNode, models.NS_TestNode_Proxy]:
+ # this slows down nested sets tests quite a bit, but it has the
+ # advantage that we'll check the node edges are correct
+ d = {}
+ for tree_id, lft, rgt in model.objects.values_list('tree_id',
+ 'lft',
+ 'rgt'):
+ d.setdefault(tree_id, []).extend([lft, rgt])
+ for tree_id, got_edges in d.items():
+ assert len(got_edges) == max(got_edges)
+ good_edges = list(range(1, len(got_edges) + 1))
+ assert sorted(got_edges) == good_edges
+
+ return [(o.desc, o.get_depth(), o.get_children_count())
+ for o in model.get_tree()]
+
+ def _assert_get_annotated_list(self, model, expected, parent=None):
+ results = model.get_annotated_list(parent)
+ got = [
+ (obj[0].desc, obj[1]['open'], obj[1]['close'], obj[1]['level'])
+ for obj in results
+ ]
+ assert expected == got
+ assert all([type(obj[0]) == model for obj in results])
+
+
+class TestEmptyTree(TestTreeBase):
+
+ def test_load_bulk_empty(self, model):
+ ids = model.load_bulk(BASE_DATA)
+ got_descs = [obj.desc
+ for obj in model.objects.filter(id__in=ids)]
+ expected_descs = [x[0] for x in UNCHANGED]
+ assert sorted(got_descs) == sorted(expected_descs)
+ assert self.got(model) == UNCHANGED
+
+ def test_dump_bulk_empty(self, model):
+ assert model.dump_bulk() == []
+
+ def test_add_root_empty(self, model):
+ model.add_root(desc='1')
+ expected = [('1', 1, 0)]
+ assert self.got(model) == expected
+
+ def test_get_root_nodes_empty(self, model):
+ got = model.get_root_nodes()
+ expected = []
+ assert [node.desc for node in got] == expected
+
+ def test_get_first_root_node_empty(self, model):
+ got = model.get_first_root_node()
+ assert got is None
+
+ def test_get_last_root_node_empty(self, model):
+ got = model.get_last_root_node()
+ assert got is None
+
+ def test_get_tree(self, model):
+ got = list(model.get_tree())
+ assert got == []
+
+ def test_get_annotated_list(self, model):
+ expected = []
+ self._assert_get_annotated_list(model, expected)
+
+
+class TestNonEmptyTree(TestTreeBase):
+
+ @classmethod
+ def setup_class(cls):
+ for model in models.BASE_MODELS:
+ model.load_bulk(BASE_DATA)
+
+ @classmethod
+ def teardown_class(cls):
+ models.empty_models_tables(models.BASE_MODELS)
+
+
+class TestClassMethods(TestNonEmptyTree):
+
+ def test_load_bulk_existing(self, model):
+ # inserting on an existing node
+ node = model.objects.get(desc='231')
+ ids = model.load_bulk(BASE_DATA, node)
+ expected = [('1', 1, 0),
+ ('2', 1, 4),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 1),
+ ('231', 3, 4),
+ ('1', 4, 0),
+ ('2', 4, 4),
+ ('21', 5, 0),
+ ('22', 5, 0),
+ ('23', 5, 1),
+ ('231', 6, 0),
+ ('24', 5, 0),
+ ('3', 4, 0),
+ ('4', 4, 1),
+ ('41', 5, 0),
+ ('24', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ expected_descs = ['1', '2', '21', '22', '23', '231', '24',
+ '3', '4', '41']
+ got_descs = [obj.desc for obj in model.objects.filter(id__in=ids)]
+ assert sorted(got_descs) == sorted(expected_descs)
+ assert self.got(model) == expected
+
+ def test_get_tree_all(self, model):
+ nodes = model.get_tree()
+ got = [(o.desc, o.get_depth(), o.get_children_count())
+ for o in nodes]
+ assert got == UNCHANGED
+ assert all([type(o) == model for o in nodes])
+
+ def test_dump_bulk_all(self, model):
+ assert model.dump_bulk(keep_ids=False) == BASE_DATA
+
+ def test_get_tree_node(self, model):
+ node = model.objects.get(desc='231')
+ model.load_bulk(BASE_DATA, node)
+
+ # the tree was modified by load_bulk, so we reload our node object
+ node = model.objects.get(pk=node.pk)
+
+ nodes = model.get_tree(node)
+ got = [(o.desc, o.get_depth(), o.get_children_count())
+ for o in nodes]
+ expected = [('231', 3, 4),
+ ('1', 4, 0),
+ ('2', 4, 4),
+ ('21', 5, 0),
+ ('22', 5, 0),
+ ('23', 5, 1),
+ ('231', 6, 0),
+ ('24', 5, 0),
+ ('3', 4, 0),
+ ('4', 4, 1),
+ ('41', 5, 0)]
+ assert got == expected
+ assert all([type(o) == model for o in nodes])
+
+ def test_get_tree_leaf(self, model):
+ node = model.objects.get(desc='1')
+
+ assert 0 == node.get_children_count()
+ nodes = model.get_tree(node)
+ got = [(o.desc, o.get_depth(), o.get_children_count())
+ for o in nodes]
+ expected = [('1', 1, 0)]
+ assert got == expected
+ assert all([type(o) == model for o in nodes])
+
+ def test_get_annotated_list_all(self, model):
+ expected = [('1', True, [], 0), ('2', False, [], 0),
+ ('21', True, [], 1), ('22', False, [], 1),
+ ('23', False, [], 1), ('231', True, [0], 2),
+ ('24', False, [0], 1), ('3', False, [], 0),
+ ('4', False, [], 0), ('41', True, [0, 1], 1)]
+ self._assert_get_annotated_list(model, expected)
+
+ def test_get_annotated_list_node(self, model):
+ node = model.objects.get(desc='2')
+ expected = [('2', True, [], 0), ('21', True, [], 1),
+ ('22', False, [], 1), ('23', False, [], 1),
+ ('231', True, [0], 2), ('24', False, [0, 1], 1)]
+ self._assert_get_annotated_list(model, expected, node)
+
+ def test_get_annotated_list_leaf(self, model):
+ node = model.objects.get(desc='1')
+ expected = [('1', True, [0], 0)]
+ self._assert_get_annotated_list(model, expected, node)
+
+ def test_dump_bulk_node(self, model):
+ node = model.objects.get(desc='231')
+ model.load_bulk(BASE_DATA, node)
+
+ # the tree was modified by load_bulk, so we reload our node object
+ node = model.objects.get(pk=node.pk)
+
+ got = model.dump_bulk(node, False)
+ expected = [{'data': {'desc': '231'}, 'children': BASE_DATA}]
+ assert got == expected
+
+ def test_load_and_dump_bulk_keeping_ids(self, model):
+ exp = model.dump_bulk(keep_ids=True)
+ model.objects.all().delete()
+ model.load_bulk(exp, None, True)
+ got = model.dump_bulk(keep_ids=True)
+ assert got == exp
+ # do we really have an unchaged tree after the dump/delete/load?
+ got = [(o.desc, o.get_depth(), o.get_children_count())
+ for o in model.get_tree()]
+ assert got == UNCHANGED
+
+ def test_load_and_dump_bulk_with_fk(self, related_model):
+ # https://bitbucket.org/tabo/django-treebeard/issue/48/
+ related_model.objects.all().delete()
+ related, created = models.RelatedModel.objects.get_or_create(
+ desc="Test %s" % related_model.__name__)
+
+ related_data = [
+ {'data': {'desc': '1', 'related': related.pk}},
+ {'data': {'desc': '2', 'related': related.pk}, 'children': [
+ {'data': {'desc': '21', 'related': related.pk}},
+ {'data': {'desc': '22', 'related': related.pk}},
+ {'data': {'desc': '23', 'related': related.pk}, 'children': [
+ {'data': {'desc': '231', 'related': related.pk}},
+ ]},
+ {'data': {'desc': '24', 'related': related.pk}},
+ ]},
+ {'data': {'desc': '3', 'related': related.pk}},
+ {'data': {'desc': '4', 'related': related.pk}, 'children': [
+ {'data': {'desc': '41', 'related': related.pk}},
+ ]}]
+ related_model.load_bulk(related_data)
+ got = related_model.dump_bulk(keep_ids=False)
+ assert got == related_data
+
+ def test_get_root_nodes(self, model):
+ got = model.get_root_nodes()
+ expected = ['1', '2', '3', '4']
+ assert [node.desc for node in got] == expected
+ assert all([type(node) == model for node in got])
+
+ def test_get_first_root_node(self, model):
+ got = model.get_first_root_node()
+ assert got.desc == '1'
+ assert type(got) == model
+
+ def test_get_last_root_node(self, model):
+ got = model.get_last_root_node()
+ assert got.desc == '4'
+ assert type(got) == model
+
+ def test_add_root(self, model):
+ obj = model.add_root(desc='5')
+ assert obj.get_depth() == 1
+ got = model.get_last_root_node()
+ assert got.desc == '5'
+ assert type(got) == model
+
+ def test_add_root_with_passed_instance(self, model):
+ obj = model(desc='5')
+ result = model.add_root(instance=obj)
+ assert result == obj
+ got = model.get_last_root_node()
+ assert got.desc == '5'
+ assert type(got) == model
+
+ def test_add_root_with_already_saved_instance(self, model):
+ obj = model.objects.get(desc='4')
+ with pytest.raises(NodeAlreadySaved):
+ model.add_root(instance=obj)
+
+
+class TestSimpleNodeMethods(TestNonEmptyTree):
+ def test_is_root(self, model):
+ data = [
+ ('2', True),
+ ('1', True),
+ ('4', True),
+ ('21', False),
+ ('24', False),
+ ('22', False),
+ ('231', False),
+ ]
+ for desc, expected in data:
+ got = model.objects.get(desc=desc).is_root()
+ assert got == expected
+
+ def test_is_leaf(self, model):
+ data = [
+ ('2', False),
+ ('23', False),
+ ('231', True),
+ ]
+ for desc, expected in data:
+ got = model.objects.get(desc=desc).is_leaf()
+ assert got == expected
+
+ def test_get_root(self, model):
+ data = [
+ ('2', '2'),
+ ('1', '1'),
+ ('4', '4'),
+ ('21', '2'),
+ ('24', '2'),
+ ('22', '2'),
+ ('231', '2'),
+ ]
+ for desc, expected in data:
+ node = model.objects.get(desc=desc).get_root()
+ assert node.desc == expected
+ assert type(node) == model
+
+ def test_get_parent(self, model):
+ data = [
+ ('2', None),
+ ('1', None),
+ ('4', None),
+ ('21', '2'),
+ ('24', '2'),
+ ('22', '2'),
+ ('231', '23'),
+ ]
+ data = dict(data)
+ objs = {}
+ for desc, expected in data.items():
+ node = model.objects.get(desc=desc)
+ parent = node.get_parent()
+ if expected:
+ assert parent.desc == expected
+ assert type(parent) == model
+ else:
+ assert parent is None
+ objs[desc] = node
+ # corrupt the objects' parent cache
+ node._parent_obj = 'CORRUPTED!!!'
+
+ for desc, expected in data.items():
+ node = objs[desc]
+ # asking get_parent to not use the parent cache (since we
+ # corrupted it in the previous loop)
+ parent = node.get_parent(True)
+ if expected:
+ assert parent.desc == expected
+ assert type(parent) == model
+ else:
+ assert parent is None
+
+ def test_get_children(self, model):
+ data = [
+ ('2', ['21', '22', '23', '24']),
+ ('23', ['231']),
+ ('231', []),
+ ]
+ for desc, expected in data:
+ children = model.objects.get(desc=desc).get_children()
+ assert [node.desc for node in children] == expected
+ assert all([type(node) == model for node in children])
+
+ def test_get_children_count(self, model):
+ data = [
+ ('2', 4),
+ ('23', 1),
+ ('231', 0),
+ ]
+ for desc, expected in data:
+ got = model.objects.get(desc=desc).get_children_count()
+ assert got == expected
+
+ def test_get_siblings(self, model):
+ data = [
+ ('2', ['1', '2', '3', '4']),
+ ('21', ['21', '22', '23', '24']),
+ ('231', ['231']),
+ ]
+ for desc, expected in data:
+ siblings = model.objects.get(desc=desc).get_siblings()
+ assert [node.desc for node in siblings] == expected
+ assert all([type(node) == model for node in siblings])
+
+ def test_get_first_sibling(self, model):
+ data = [
+ ('2', '1'),
+ ('1', '1'),
+ ('4', '1'),
+ ('21', '21'),
+ ('24', '21'),
+ ('22', '21'),
+ ('231', '231'),
+ ]
+ for desc, expected in data:
+ node = model.objects.get(desc=desc).get_first_sibling()
+ assert node.desc == expected
+ assert type(node) == model
+
+ def test_get_prev_sibling(self, model):
+ data = [
+ ('2', '1'),
+ ('1', None),
+ ('4', '3'),
+ ('21', None),
+ ('24', '23'),
+ ('22', '21'),
+ ('231', None),
+ ]
+ for desc, expected in data:
+ node = model.objects.get(desc=desc).get_prev_sibling()
+ if expected is None:
+ assert node is None
+ else:
+ assert node.desc == expected
+ assert type(node) == model
+
+ def test_get_next_sibling(self, model):
+ data = [
+ ('2', '3'),
+ ('1', '2'),
+ ('4', None),
+ ('21', '22'),
+ ('24', None),
+ ('22', '23'),
+ ('231', None),
+ ]
+ for desc, expected in data:
+ node = model.objects.get(desc=desc).get_next_sibling()
+ if expected is None:
+ assert node is None
+ else:
+ assert node.desc == expected
+ assert type(node) == model
+
+ def test_get_last_sibling(self, model):
+ data = [
+ ('2', '4'),
+ ('1', '4'),
+ ('4', '4'),
+ ('21', '24'),
+ ('24', '24'),
+ ('22', '24'),
+ ('231', '231'),
+ ]
+ for desc, expected in data:
+ node = model.objects.get(desc=desc).get_last_sibling()
+ assert node.desc == expected
+ assert type(node) == model
+
+ def test_get_first_child(self, model):
+ data = [
+ ('2', '21'),
+ ('21', None),
+ ('23', '231'),
+ ('231', None),
+ ]
+ for desc, expected in data:
+ node = model.objects.get(desc=desc).get_first_child()
+ if expected is None:
+ assert node is None
+ else:
+ assert node.desc == expected
+ assert type(node) == model
+
+ def test_get_last_child(self, model):
+ data = [
+ ('2', '24'),
+ ('21', None),
+ ('23', '231'),
+ ('231', None),
+ ]
+ for desc, expected in data:
+ node = model.objects.get(desc=desc).get_last_child()
+ if expected is None:
+ assert node is None
+ else:
+ assert node.desc == expected
+ assert type(node) == model
+
+ def test_get_ancestors(self, model):
+ data = [
+ ('2', []),
+ ('21', ['2']),
+ ('231', ['2', '23']),
+ ]
+ for desc, expected in data:
+ nodes = model.objects.get(desc=desc).get_ancestors()
+ assert [node.desc for node in nodes] == expected
+ assert all([type(node) == model for node in nodes])
+
+ def test_get_descendants(self, model):
+ data = [
+ ('2', ['21', '22', '23', '231', '24']),
+ ('23', ['231']),
+ ('231', []),
+ ('1', []),
+ ('4', ['41']),
+ ]
+ for desc, expected in data:
+ nodes = model.objects.get(desc=desc).get_descendants()
+ assert [node.desc for node in nodes] == expected
+ assert all([type(node) == model for node in nodes])
+
+ def test_get_descendant_count(self, model):
+ data = [
+ ('2', 5),
+ ('23', 1),
+ ('231', 0),
+ ('1', 0),
+ ('4', 1),
+ ]
+ for desc, expected in data:
+ got = model.objects.get(desc=desc).get_descendant_count()
+ assert got == expected
+
+ def test_is_sibling_of(self, model):
+ data = [
+ ('2', '2', True),
+ ('2', '1', True),
+ ('21', '2', False),
+ ('231', '2', False),
+ ('22', '23', True),
+ ('231', '23', False),
+ ('231', '231', True),
+ ]
+ for desc1, desc2, expected in data:
+ node1 = model.objects.get(desc=desc1)
+ node2 = model.objects.get(desc=desc2)
+ assert node1.is_sibling_of(node2) == expected
+
+ def test_is_child_of(self, model):
+ data = [
+ ('2', '2', False),
+ ('2', '1', False),
+ ('21', '2', True),
+ ('231', '2', False),
+ ('231', '23', True),
+ ('231', '231', False),
+ ]
+ for desc1, desc2, expected in data:
+ node1 = model.objects.get(desc=desc1)
+ node2 = model.objects.get(desc=desc2)
+ assert node1.is_child_of(node2) == expected
+
+ def test_is_descendant_of(self, model):
+ data = [
+ ('2', '2', False),
+ ('2', '1', False),
+ ('21', '2', True),
+ ('231', '2', True),
+ ('231', '23', True),
+ ('231', '231', False),
+ ]
+ for desc1, desc2, expected in data:
+ node1 = model.objects.get(desc=desc1)
+ node2 = model.objects.get(desc=desc2)
+ assert node1.is_descendant_of(node2) == expected
+
+
+class TestAddChild(TestNonEmptyTree):
+ def test_add_child_to_leaf(self, model):
+ model.objects.get(desc='231').add_child(desc='2311')
+ expected = [('1', 1, 0),
+ ('2', 1, 4),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 1),
+ ('231', 3, 1),
+ ('2311', 4, 0),
+ ('24', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+ def test_add_child_to_node(self, model):
+ model.objects.get(desc='2').add_child(desc='25')
+ expected = [('1', 1, 0),
+ ('2', 1, 5),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 1),
+ ('231', 3, 0),
+ ('24', 2, 0),
+ ('25', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+ def test_add_child_with_passed_instance(self, model):
+ child = model(desc='2311')
+ result = model.objects.get(desc='231').add_child(instance=child)
+ assert result == child
+ expected = [('1', 1, 0),
+ ('2', 1, 4),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 1),
+ ('231', 3, 1),
+ ('2311', 4, 0),
+ ('24', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+ def test_add_child_with_already_saved_instance(self, model):
+ child = model.objects.get(desc='21')
+ with pytest.raises(NodeAlreadySaved):
+ model.objects.get(desc='2').add_child(instance=child)
+
+
+class TestAddSibling(TestNonEmptyTree):
+ def test_add_sibling_invalid_pos(self, model):
+ with pytest.raises(InvalidPosition):
+ model.objects.get(desc='231').add_sibling('invalid_pos')
+
+ def test_add_sibling_missing_nodeorderby(self, model):
+ node_wchildren = model.objects.get(desc='2')
+ with pytest.raises(MissingNodeOrderBy):
+ node_wchildren.add_sibling('sorted-sibling', desc='aaa')
+
+ def test_add_sibling_last_root(self, model):
+ node_wchildren = model.objects.get(desc='2')
+ obj = node_wchildren.add_sibling('last-sibling', desc='5')
+ assert obj.get_depth() == 1
+ assert node_wchildren.get_last_sibling().desc == '5'
+
+ def test_add_sibling_last(self, model):
+ node = model.objects.get(desc='231')
+ obj = node.add_sibling('last-sibling', desc='232')
+ assert obj.get_depth() == 3
+ assert node.get_last_sibling().desc == '232'
+
+ def test_add_sibling_first_root(self, model):
+ node_wchildren = model.objects.get(desc='2')
+ obj = node_wchildren.add_sibling('first-sibling', desc='new')
+ assert obj.get_depth() == 1
+ expected = [('new', 1, 0),
+ ('1', 1, 0),
+ ('2', 1, 4),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 1),
+ ('231', 3, 0),
+ ('24', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+ def test_add_sibling_first(self, model):
+ node_wchildren = model.objects.get(desc='23')
+ obj = node_wchildren.add_sibling('first-sibling', desc='new')
+ assert obj.get_depth() == 2
+ expected = [('1', 1, 0),
+ ('2', 1, 5),
+ ('new', 2, 0),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 1),
+ ('231', 3, 0),
+ ('24', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+ def test_add_sibling_left_root(self, model):
+ node_wchildren = model.objects.get(desc='2')
+ obj = node_wchildren.add_sibling('left', desc='new')
+ assert obj.get_depth() == 1
+ expected = [('1', 1, 0),
+ ('new', 1, 0),
+ ('2', 1, 4),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 1),
+ ('231', 3, 0),
+ ('24', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+ def test_add_sibling_left(self, model):
+ node_wchildren = model.objects.get(desc='23')
+ obj = node_wchildren.add_sibling('left', desc='new')
+ assert obj.get_depth() == 2
+ expected = [('1', 1, 0),
+ ('2', 1, 5),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('new', 2, 0),
+ ('23', 2, 1),
+ ('231', 3, 0),
+ ('24', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+ def test_add_sibling_left_noleft_root(self, model):
+ node = model.objects.get(desc='1')
+ obj = node.add_sibling('left', desc='new')
+ assert obj.get_depth() == 1
+ expected = [('new', 1, 0),
+ ('1', 1, 0),
+ ('2', 1, 4),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 1),
+ ('231', 3, 0),
+ ('24', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+ def test_add_sibling_left_noleft(self, model):
+ node = model.objects.get(desc='231')
+ obj = node.add_sibling('left', desc='new')
+ assert obj.get_depth() == 3
+ expected = [('1', 1, 0),
+ ('2', 1, 4),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 2),
+ ('new', 3, 0),
+ ('231', 3, 0),
+ ('24', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+ def test_add_sibling_right_root(self, model):
+ node_wchildren = model.objects.get(desc='2')
+ obj = node_wchildren.add_sibling('right', desc='new')
+ assert obj.get_depth() == 1
+ expected = [('1', 1, 0),
+ ('2', 1, 4),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 1),
+ ('231', 3, 0),
+ ('24', 2, 0),
+ ('new', 1, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+ def test_add_sibling_right(self, model):
+ node_wchildren = model.objects.get(desc='23')
+ obj = node_wchildren.add_sibling('right', desc='new')
+ assert obj.get_depth() == 2
+ expected = [('1', 1, 0),
+ ('2', 1, 5),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 1),
+ ('231', 3, 0),
+ ('new', 2, 0),
+ ('24', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+ def test_add_sibling_right_noright_root(self, model):
+ node = model.objects.get(desc='4')
+ obj = node.add_sibling('right', desc='new')
+ assert obj.get_depth() == 1
+ expected = [('1', 1, 0),
+ ('2', 1, 4),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 1),
+ ('231', 3, 0),
+ ('24', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0),
+ ('new', 1, 0)]
+ assert self.got(model) == expected
+
+ def test_add_sibling_right_noright(self, model):
+ node = model.objects.get(desc='231')
+ obj = node.add_sibling('right', desc='new')
+ assert obj.get_depth() == 3
+ expected = [('1', 1, 0),
+ ('2', 1, 4),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 2),
+ ('231', 3, 0),
+ ('new', 3, 0),
+ ('24', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+ def test_add_sibling_with_passed_instance(self, model):
+ node_wchildren = model.objects.get(desc='2')
+ obj = model(desc='5')
+ result = node_wchildren.add_sibling('last-sibling', instance=obj)
+ assert result == obj
+ assert obj.get_depth() == 1
+ assert node_wchildren.get_last_sibling().desc == '5'
+
+ def test_add_sibling_already_saved_instance(self, model):
+ node_wchildren = model.objects.get(desc='2')
+ existing_node = model.objects.get(desc='4')
+ with pytest.raises(NodeAlreadySaved):
+ node_wchildren.add_sibling('last-sibling', instance=existing_node)
+
+
+class TestDelete(TestNonEmptyTree):
+
+ @classmethod
+ def setup_class(cls):
+ TestNonEmptyTree.setup_class()
+ for model, dep_model in zip(models.BASE_MODELS, models.DEP_MODELS):
+ for node in model.objects.all():
+ dep_model(node=node).save()
+
+ @classmethod
+ def teardown_class(cls):
+ models.empty_models_tables(models.DEP_MODELS + models.BASE_MODELS)
+
+ def test_delete_leaf(self, model):
+ model.objects.get(desc='231').delete()
+ expected = [('1', 1, 0),
+ ('2', 1, 4),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 0),
+ ('24', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+ def test_delete_node(self, model):
+ model.objects.get(desc='23').delete()
+ expected = [('1', 1, 0),
+ ('2', 1, 3),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('24', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+ def test_delete_root(self, model):
+ model.objects.get(desc='2').delete()
+ expected = [('1', 1, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+ def test_delete_filter_root_nodes(self, model):
+ model.objects.filter(desc__in=('2', '3')).delete()
+ expected = [('1', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+ def test_delete_filter_children(self, model):
+ model.objects.filter(desc__in=('2', '23', '231')).delete()
+ expected = [('1', 1, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+ def test_delete_nonexistant_nodes(self, model):
+ model.objects.filter(desc__in=('ZZZ', 'XXX')).delete()
+ assert self.got(model) == UNCHANGED
+
+ def test_delete_same_node_twice(self, model):
+ model.objects.filter(desc__in=('2', '2')).delete()
+ expected = [('1', 1, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+ def test_delete_all_root_nodes(self, model):
+ model.get_root_nodes().delete()
+ count = model.objects.count()
+ assert count == 0
+
+ def test_delete_all_nodes(self, model):
+ model.objects.all().delete()
+ count = model.objects.count()
+ assert count == 0
+
+
+class TestMoveErrors(TestNonEmptyTree):
+ def test_move_invalid_pos(self, model):
+ node = model.objects.get(desc='231')
+ with pytest.raises(InvalidPosition):
+ node.move(node, 'invalid_pos')
+
+ def test_move_to_descendant(self, model):
+ node = model.objects.get(desc='2')
+ target = model.objects.get(desc='231')
+ with pytest.raises(InvalidMoveToDescendant):
+ node.move(target, 'first-sibling')
+
+ def test_move_missing_nodeorderby(self, model):
+ node = model.objects.get(desc='231')
+ with pytest.raises(MissingNodeOrderBy):
+ node.move(node, 'sorted-child')
+ with pytest.raises(MissingNodeOrderBy):
+ node.move(node, 'sorted-sibling')
+
+
+class TestMoveSortedErrors(TestTreeBase):
+
+ def test_nonsorted_move_in_sorted(self, sorted_model):
+ node = sorted_model.add_root(val1=3, val2=3, desc='zxy')
+ with pytest.raises(InvalidPosition):
+ node.move(node, 'left')
+
+
+class TestMoveLeafRoot(TestNonEmptyTree):
+ def test_move_leaf_last_sibling_root(self, model):
+ target = model.objects.get(desc='2')
+ model.objects.get(desc='231').move(target, 'last-sibling')
+ expected = [('1', 1, 0),
+ ('2', 1, 4),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 0),
+ ('24', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0),
+ ('231', 1, 0)]
+ assert self.got(model) == expected
+
+ def test_move_leaf_first_sibling_root(self, model):
+ target = model.objects.get(desc='2')
+ model.objects.get(desc='231').move(target, 'first-sibling')
+ expected = [('231', 1, 0),
+ ('1', 1, 0),
+ ('2', 1, 4),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 0),
+ ('24', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+ def test_move_leaf_left_sibling_root(self, model):
+ target = model.objects.get(desc='2')
+ model.objects.get(desc='231').move(target, 'left')
+ expected = [('1', 1, 0),
+ ('231', 1, 0),
+ ('2', 1, 4),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 0),
+ ('24', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+ def test_move_leaf_right_sibling_root(self, model):
+ target = model.objects.get(desc='2')
+ model.objects.get(desc='231').move(target, 'right')
+ expected = [('1', 1, 0),
+ ('2', 1, 4),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 0),
+ ('24', 2, 0),
+ ('231', 1, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+ def test_move_leaf_last_child_root(self, model):
+ target = model.objects.get(desc='2')
+ model.objects.get(desc='231').move(target, 'last-child')
+ expected = [('1', 1, 0),
+ ('2', 1, 5),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 0),
+ ('24', 2, 0),
+ ('231', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+ def test_move_leaf_first_child_root(self, model):
+ target = model.objects.get(desc='2')
+ model.objects.get(desc='231').move(target, 'first-child')
+ expected = [('1', 1, 0),
+ ('2', 1, 5),
+ ('231', 2, 0),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 0),
+ ('24', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+
+class TestMoveLeaf(TestNonEmptyTree):
+ def test_move_leaf_last_sibling(self, model):
+ target = model.objects.get(desc='22')
+ model.objects.get(desc='231').move(target, 'last-sibling')
+ expected = [('1', 1, 0),
+ ('2', 1, 5),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 0),
+ ('24', 2, 0),
+ ('231', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+ def test_move_leaf_first_sibling(self, model):
+ target = model.objects.get(desc='22')
+ model.objects.get(desc='231').move(target, 'first-sibling')
+ expected = [('1', 1, 0),
+ ('2', 1, 5),
+ ('231', 2, 0),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 0),
+ ('24', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+ def test_move_leaf_left_sibling(self, model):
+ target = model.objects.get(desc='22')
+ model.objects.get(desc='231').move(target, 'left')
+ expected = [('1', 1, 0),
+ ('2', 1, 5),
+ ('21', 2, 0),
+ ('231', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 0),
+ ('24', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+ def test_move_leaf_right_sibling(self, model):
+ target = model.objects.get(desc='22')
+ model.objects.get(desc='231').move(target, 'right')
+ expected = [('1', 1, 0),
+ ('2', 1, 5),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('231', 2, 0),
+ ('23', 2, 0),
+ ('24', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+ def test_move_leaf_left_sibling_itself(self, model):
+ target = model.objects.get(desc='231')
+ model.objects.get(desc='231').move(target, 'left')
+ assert self.got(model) == UNCHANGED
+
+ def test_move_leaf_last_child(self, model):
+ target = model.objects.get(desc='22')
+ model.objects.get(desc='231').move(target, 'last-child')
+ expected = [('1', 1, 0),
+ ('2', 1, 4),
+ ('21', 2, 0),
+ ('22', 2, 1),
+ ('231', 3, 0),
+ ('23', 2, 0),
+ ('24', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+ def test_move_leaf_first_child(self, model):
+ target = model.objects.get(desc='22')
+ model.objects.get(desc='231').move(target, 'first-child')
+ expected = [('1', 1, 0),
+ ('2', 1, 4),
+ ('21', 2, 0),
+ ('22', 2, 1),
+ ('231', 3, 0),
+ ('23', 2, 0),
+ ('24', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+
+class TestMoveBranchRoot(TestNonEmptyTree):
+ def test_move_branch_first_sibling_root(self, model):
+ target = model.objects.get(desc='2')
+ model.objects.get(desc='4').move(target, 'first-sibling')
+ expected = [('4', 1, 1),
+ ('41', 2, 0),
+ ('1', 1, 0),
+ ('2', 1, 4),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 1),
+ ('231', 3, 0),
+ ('24', 2, 0),
+ ('3', 1, 0)]
+ assert self.got(model) == expected
+
+ def test_move_branch_last_sibling_root(self, model):
+ target = model.objects.get(desc='2')
+ model.objects.get(desc='4').move(target, 'last-sibling')
+ expected = [('1', 1, 0),
+ ('2', 1, 4),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 1),
+ ('231', 3, 0),
+ ('24', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+ def test_move_branch_left_sibling_root(self, model):
+ target = model.objects.get(desc='2')
+ model.objects.get(desc='4').move(target, 'left')
+ expected = [('1', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0),
+ ('2', 1, 4),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 1),
+ ('231', 3, 0),
+ ('24', 2, 0),
+ ('3', 1, 0)]
+ assert self.got(model) == expected
+
+ def test_move_branch_right_sibling_root(self, model):
+ target = model.objects.get(desc='2')
+ model.objects.get(desc='4').move(target, 'right')
+ expected = [('1', 1, 0),
+ ('2', 1, 4),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 1),
+ ('231', 3, 0),
+ ('24', 2, 0),
+ ('4', 1, 1),
+ ('41', 2, 0),
+ ('3', 1, 0)]
+ assert self.got(model) == expected
+
+ def test_move_branch_left_noleft_sibling_root(self, model):
+ target = model.objects.get(desc='2').get_first_sibling()
+ model.objects.get(desc='4').move(target, 'left')
+ expected = [('4', 1, 1),
+ ('41', 2, 0),
+ ('1', 1, 0),
+ ('2', 1, 4),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 1),
+ ('231', 3, 0),
+ ('24', 2, 0),
+ ('3', 1, 0)]
+ assert self.got(model) == expected
+
+ def test_move_branch_right_noright_sibling_root(self, model):
+ target = model.objects.get(desc='2').get_last_sibling()
+ model.objects.get(desc='4').move(target, 'right')
+ expected = [('1', 1, 0),
+ ('2', 1, 4),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 1),
+ ('231', 3, 0),
+ ('24', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+ def test_move_branch_first_child_root(self, model):
+ target = model.objects.get(desc='2')
+ model.objects.get(desc='4').move(target, 'first-child')
+ expected = [('1', 1, 0),
+ ('2', 1, 5),
+ ('4', 2, 1),
+ ('41', 3, 0),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 1),
+ ('231', 3, 0),
+ ('24', 2, 0),
+ ('3', 1, 0)]
+ assert self.got(model) == expected
+
+ def test_move_branch_last_child_root(self, model):
+ target = model.objects.get(desc='2')
+ model.objects.get(desc='4').move(target, 'last-child')
+ expected = [('1', 1, 0),
+ ('2', 1, 5),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 1),
+ ('231', 3, 0),
+ ('24', 2, 0),
+ ('4', 2, 1),
+ ('41', 3, 0),
+ ('3', 1, 0)]
+ assert self.got(model) == expected
+
+
+class TestMoveBranch(TestNonEmptyTree):
+ def test_move_branch_first_sibling(self, model):
+ target = model.objects.get(desc='23')
+ model.objects.get(desc='4').move(target, 'first-sibling')
+ expected = [('1', 1, 0),
+ ('2', 1, 5),
+ ('4', 2, 1),
+ ('41', 3, 0),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 1),
+ ('231', 3, 0),
+ ('24', 2, 0),
+ ('3', 1, 0)]
+ assert self.got(model) == expected
+
+ def test_move_branch_last_sibling(self, model):
+ target = model.objects.get(desc='23')
+ model.objects.get(desc='4').move(target, 'last-sibling')
+ expected = [('1', 1, 0),
+ ('2', 1, 5),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 1),
+ ('231', 3, 0),
+ ('24', 2, 0),
+ ('4', 2, 1),
+ ('41', 3, 0),
+ ('3', 1, 0)]
+ assert self.got(model) == expected
+
+ def test_move_branch_left_sibling(self, model):
+ target = model.objects.get(desc='23')
+ model.objects.get(desc='4').move(target, 'left')
+ expected = [('1', 1, 0),
+ ('2', 1, 5),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('4', 2, 1),
+ ('41', 3, 0),
+ ('23', 2, 1),
+ ('231', 3, 0),
+ ('24', 2, 0),
+ ('3', 1, 0)]
+ assert self.got(model) == expected
+
+ def test_move_branch_right_sibling(self, model):
+ target = model.objects.get(desc='23')
+ model.objects.get(desc='4').move(target, 'right')
+ expected = [('1', 1, 0),
+ ('2', 1, 5),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 1),
+ ('231', 3, 0),
+ ('4', 2, 1),
+ ('41', 3, 0),
+ ('24', 2, 0),
+ ('3', 1, 0)]
+ assert self.got(model) == expected
+
+ def test_move_branch_left_noleft_sibling(self, model):
+ target = model.objects.get(desc='23').get_first_sibling()
+ model.objects.get(desc='4').move(target, 'left')
+ expected = [('1', 1, 0),
+ ('2', 1, 5),
+ ('4', 2, 1),
+ ('41', 3, 0),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 1),
+ ('231', 3, 0),
+ ('24', 2, 0),
+ ('3', 1, 0)]
+ assert self.got(model) == expected
+
+ def test_move_branch_right_noright_sibling(self, model):
+ target = model.objects.get(desc='23').get_last_sibling()
+ model.objects.get(desc='4').move(target, 'right')
+ expected = [('1', 1, 0),
+ ('2', 1, 5),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 1),
+ ('231', 3, 0),
+ ('24', 2, 0),
+ ('4', 2, 1),
+ ('41', 3, 0),
+ ('3', 1, 0)]
+ assert self.got(model) == expected
+
+ def test_move_branch_left_itself_sibling(self, model):
+ target = model.objects.get(desc='4')
+ model.objects.get(desc='4').move(target, 'left')
+ assert self.got(model) == UNCHANGED
+
+ def test_move_branch_first_child(self, model):
+ target = model.objects.get(desc='23')
+ model.objects.get(desc='4').move(target, 'first-child')
+ expected = [('1', 1, 0),
+ ('2', 1, 4),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 2),
+ ('4', 3, 1),
+ ('41', 4, 0),
+ ('231', 3, 0),
+ ('24', 2, 0),
+ ('3', 1, 0)]
+ assert self.got(model) == expected
+
+ def test_move_branch_last_child(self, model):
+ target = model.objects.get(desc='23')
+ model.objects.get(desc='4').move(target, 'last-child')
+ expected = [('1', 1, 0),
+ ('2', 1, 4),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 2),
+ ('231', 3, 0),
+ ('4', 3, 1),
+ ('41', 4, 0),
+ ('24', 2, 0),
+ ('3', 1, 0)]
+ assert self.got(model) == expected
+
+
+class TestTreeSorted(TestTreeBase):
+
+ def got(self, sorted_model):
+ return [(o.val1, o.val2, o.desc, o.get_depth(), o.get_children_count())
+ for o in sorted_model.get_tree()]
+
+ def test_add_root_sorted(self, sorted_model):
+ sorted_model.add_root(val1=3, val2=3, desc='zxy')
+ sorted_model.add_root(val1=1, val2=4, desc='bcd')
+ sorted_model.add_root(val1=2, val2=5, desc='zxy')
+ sorted_model.add_root(val1=3, val2=3, desc='abc')
+ sorted_model.add_root(val1=4, val2=1, desc='fgh')
+ sorted_model.add_root(val1=3, val2=3, desc='abc')
+ sorted_model.add_root(val1=2, val2=2, desc='qwe')
+ sorted_model.add_root(val1=3, val2=2, desc='vcx')
+ expected = [(1, 4, 'bcd', 1, 0),
+ (2, 2, 'qwe', 1, 0),
+ (2, 5, 'zxy', 1, 0),
+ (3, 2, 'vcx', 1, 0),
+ (3, 3, 'abc', 1, 0),
+ (3, 3, 'abc', 1, 0),
+ (3, 3, 'zxy', 1, 0),
+ (4, 1, 'fgh', 1, 0)]
+ assert self.got(sorted_model) == expected
+
+ def test_add_child_root_sorted(self, sorted_model):
+ root = sorted_model.add_root(val1=0, val2=0, desc='aaa')
+ root.add_child(val1=3, val2=3, desc='zxy')
+ root.add_child(val1=1, val2=4, desc='bcd')
+ root.add_child(val1=2, val2=5, desc='zxy')
+ root.add_child(val1=3, val2=3, desc='abc')
+ root.add_child(val1=4, val2=1, desc='fgh')
+ root.add_child(val1=3, val2=3, desc='abc')
+ root.add_child(val1=2, val2=2, desc='qwe')
+ root.add_child(val1=3, val2=2, desc='vcx')
+ expected = [(0, 0, 'aaa', 1, 8),
+ (1, 4, 'bcd', 2, 0),
+ (2, 2, 'qwe', 2, 0),
+ (2, 5, 'zxy', 2, 0),
+ (3, 2, 'vcx', 2, 0),
+ (3, 3, 'abc', 2, 0),
+ (3, 3, 'abc', 2, 0),
+ (3, 3, 'zxy', 2, 0),
+ (4, 1, 'fgh', 2, 0)]
+ assert self.got(sorted_model) == expected
+
+ def test_add_child_nonroot_sorted(self, sorted_model):
+ get_node = lambda node_id: sorted_model.objects.get(pk=node_id)
+
+ root_id = sorted_model.add_root(val1=0, val2=0, desc='a').pk
+ node_id = get_node(root_id).add_child(val1=0, val2=0, desc='ac').pk
+ get_node(root_id).add_child(val1=0, val2=0, desc='aa')
+ get_node(root_id).add_child(val1=0, val2=0, desc='av')
+ get_node(node_id).add_child(val1=0, val2=0, desc='aca')
+ get_node(node_id).add_child(val1=0, val2=0, desc='acc')
+ get_node(node_id).add_child(val1=0, val2=0, desc='acb')
+
+ expected = [(0, 0, 'a', 1, 3),
+ (0, 0, 'aa', 2, 0),
+ (0, 0, 'ac', 2, 3),
+ (0, 0, 'aca', 3, 0),
+ (0, 0, 'acb', 3, 0),
+ (0, 0, 'acc', 3, 0),
+ (0, 0, 'av', 2, 0)]
+ assert self.got(sorted_model) == expected
+
+ def test_move_sorted(self, sorted_model):
+ sorted_model.add_root(val1=3, val2=3, desc='zxy')
+ sorted_model.add_root(val1=1, val2=4, desc='bcd')
+ sorted_model.add_root(val1=2, val2=5, desc='zxy')
+ sorted_model.add_root(val1=3, val2=3, desc='abc')
+ sorted_model.add_root(val1=4, val2=1, desc='fgh')
+ sorted_model.add_root(val1=3, val2=3, desc='abc')
+ sorted_model.add_root(val1=2, val2=2, desc='qwe')
+ sorted_model.add_root(val1=3, val2=2, desc='vcx')
+ root_nodes = sorted_model.get_root_nodes()
+ target = root_nodes[0]
+ for node in root_nodes[1:]:
+ # because raw queries don't update django objects
+ node = sorted_model.objects.get(pk=node.pk)
+ target = sorted_model.objects.get(pk=target.pk)
+ node.move(target, 'sorted-child')
+ expected = [(1, 4, 'bcd', 1, 7),
+ (2, 2, 'qwe', 2, 0),
+ (2, 5, 'zxy', 2, 0),
+ (3, 2, 'vcx', 2, 0),
+ (3, 3, 'abc', 2, 0),
+ (3, 3, 'abc', 2, 0),
+ (3, 3, 'zxy', 2, 0),
+ (4, 1, 'fgh', 2, 0)]
+ assert self.got(sorted_model) == expected
+
+ def test_move_sortedsibling(self, sorted_model):
+ # https://bitbucket.org/tabo/django-treebeard/issue/27
+ sorted_model.add_root(val1=3, val2=3, desc='zxy')
+ sorted_model.add_root(val1=1, val2=4, desc='bcd')
+ sorted_model.add_root(val1=2, val2=5, desc='zxy')
+ sorted_model.add_root(val1=3, val2=3, desc='abc')
+ sorted_model.add_root(val1=4, val2=1, desc='fgh')
+ sorted_model.add_root(val1=3, val2=3, desc='abc')
+ sorted_model.add_root(val1=2, val2=2, desc='qwe')
+ sorted_model.add_root(val1=3, val2=2, desc='vcx')
+ root_nodes = sorted_model.get_root_nodes()
+ target = root_nodes[0]
+ for node in root_nodes[1:]:
+ # because raw queries don't update django objects
+ node = sorted_model.objects.get(pk=node.pk)
+ target = sorted_model.objects.get(pk=target.pk)
+ node.val1 -= 2
+ node.save()
+ node.move(target, 'sorted-sibling')
+ expected = [(0, 2, 'qwe', 1, 0),
+ (0, 5, 'zxy', 1, 0),
+ (1, 2, 'vcx', 1, 0),
+ (1, 3, 'abc', 1, 0),
+ (1, 3, 'abc', 1, 0),
+ (1, 3, 'zxy', 1, 0),
+ (1, 4, 'bcd', 1, 0),
+ (2, 1, 'fgh', 1, 0)]
+ assert self.got(sorted_model) == expected
+
+
+class TestInheritedModels(TestTreeBase):
+
+ @classmethod
+ def setup_class(cls):
+ themodels = zip(models.BASE_MODELS, models.INHERITED_MODELS)
+ for model, inherited_model in themodels:
+ model.add_root(desc='1')
+ model.add_root(desc='2')
+
+ node21 = inherited_model(desc='21')
+ model.objects.get(desc='2').add_child(instance=node21)
+
+ model.objects.get(desc='21').add_child(desc='211')
+ model.objects.get(desc='21').add_child(desc='212')
+ model.objects.get(desc='2').add_child(desc='22')
+
+ node3 = inherited_model(desc='3')
+ model.add_root(instance=node3)
+
+ @classmethod
+ def teardown_class(cls):
+ # Will also empty INHERITED_MODELS by cascade
+ models.empty_models_tables(models.BASE_MODELS)
+
+ def test_get_tree_all(self, inherited_model):
+ got = [(o.desc, o.get_depth(), o.get_children_count())
+ for o in inherited_model.get_tree()]
+ expected = [
+ ('1', 1, 0),
+ ('2', 1, 2),
+ ('21', 2, 2),
+ ('211', 3, 0),
+ ('212', 3, 0),
+ ('22', 2, 0),
+ ('3', 1, 0),
+ ]
+ assert got == expected
+
+ def test_get_tree_node(self, inherited_model):
+ node = inherited_model.objects.get(desc='21')
+
+ got = [(o.desc, o.get_depth(), o.get_children_count())
+ for o in inherited_model.get_tree(node)]
+ expected = [
+ ('21', 2, 2),
+ ('211', 3, 0),
+ ('212', 3, 0),
+ ]
+ assert got == expected
+
+ def test_get_root_nodes(self, inherited_model):
+ got = inherited_model.get_root_nodes()
+ expected = ['1', '2', '3']
+ assert [node.desc for node in got] == expected
+
+ def test_get_first_root_node(self, inherited_model):
+ got = inherited_model.get_first_root_node()
+ assert got.desc == '1'
+
+ def test_get_last_root_node(self, inherited_model):
+ got = inherited_model.get_last_root_node()
+ assert got.desc == '3'
+
+ def test_is_root(self, inherited_model):
+ node21 = inherited_model.objects.get(desc='21')
+ node3 = inherited_model.objects.get(desc='3')
+ assert node21.is_root() is False
+ assert node3.is_root() is True
+
+ def test_is_leaf(self, inherited_model):
+ node21 = inherited_model.objects.get(desc='21')
+ node3 = inherited_model.objects.get(desc='3')
+ assert node21.is_leaf() is False
+ assert node3.is_leaf() is True
+
+ def test_get_root(self, inherited_model):
+ node21 = inherited_model.objects.get(desc='21')
+ node3 = inherited_model.objects.get(desc='3')
+ assert node21.get_root().desc == '2'
+ assert node3.get_root().desc == '3'
+
+ def test_get_parent(self, inherited_model):
+ node21 = inherited_model.objects.get(desc='21')
+ node3 = inherited_model.objects.get(desc='3')
+ assert node21.get_parent().desc == '2'
+ assert node3.get_parent() is None
+
+ def test_get_children(self, inherited_model):
+ node21 = inherited_model.objects.get(desc='21')
+ node3 = inherited_model.objects.get(desc='3')
+ assert [node.desc for node in node21.get_children()] == ['211', '212']
+ assert [node.desc for node in node3.get_children()] == []
+
+ def test_get_children_count(self, inherited_model):
+ node21 = inherited_model.objects.get(desc='21')
+ node3 = inherited_model.objects.get(desc='3')
+ assert node21.get_children_count() == 2
+ assert node3.get_children_count() == 0
+
+ def test_get_siblings(self, inherited_model):
+ node21 = inherited_model.objects.get(desc='21')
+ node3 = inherited_model.objects.get(desc='3')
+ assert [node.desc for node in node21.get_siblings()] == ['21', '22']
+ assert [node.desc for node in node3.get_siblings()] == ['1', '2', '3']
+
+ def test_get_first_sibling(self, inherited_model):
+ node21 = inherited_model.objects.get(desc='21')
+ node3 = inherited_model.objects.get(desc='3')
+ assert node21.get_first_sibling().desc == '21'
+ assert node3.get_first_sibling().desc == '1'
+
+ def test_get_prev_sibling(self, inherited_model):
+ node21 = inherited_model.objects.get(desc='21')
+ node3 = inherited_model.objects.get(desc='3')
+ assert node21.get_prev_sibling() is None
+ assert node3.get_prev_sibling().desc == '2'
+
+ def test_get_next_sibling(self, inherited_model):
+ node21 = inherited_model.objects.get(desc='21')
+ node3 = inherited_model.objects.get(desc='3')
+ assert node21.get_next_sibling().desc == '22'
+ assert node3.get_next_sibling() is None
+
+ def test_get_last_sibling(self, inherited_model):
+ node21 = inherited_model.objects.get(desc='21')
+ node3 = inherited_model.objects.get(desc='3')
+ assert node21.get_last_sibling().desc == '22'
+ assert node3.get_last_sibling().desc == '3'
+
+ def test_get_first_child(self, inherited_model):
+ node21 = inherited_model.objects.get(desc='21')
+ node3 = inherited_model.objects.get(desc='3')
+ assert node21.get_first_child().desc == '211'
+ assert node3.get_first_child() is None
+
+ def test_get_last_child(self, inherited_model):
+ node21 = inherited_model.objects.get(desc='21')
+ node3 = inherited_model.objects.get(desc='3')
+ assert node21.get_last_child().desc == '212'
+ assert node3.get_last_child() is None
+
+ def test_get_ancestors(self, inherited_model):
+ node21 = inherited_model.objects.get(desc='21')
+ node3 = inherited_model.objects.get(desc='3')
+ assert [node.desc for node in node21.get_ancestors()] == ['2']
+ assert [node.desc for node in node3.get_ancestors()] == []
+
+ def test_get_descendants(self, inherited_model):
+ node21 = inherited_model.objects.get(desc='21')
+ node3 = inherited_model.objects.get(desc='3')
+ assert [node.desc for node in node21.get_descendants()] == [
+ '211', '212']
+ assert [node.desc for node in node3.get_descendants()] == []
+
+ def test_get_descendant_count(self, inherited_model):
+ node21 = inherited_model.objects.get(desc='21')
+ node3 = inherited_model.objects.get(desc='3')
+ assert node21.get_descendant_count() == 2
+ assert node3.get_descendant_count() == 0
+
+ def test_cascading_deletion(self, inherited_model):
+ # Deleting a node by calling delete() on the inherited_model class
+ # should delete descendants, even if those descendants are not
+ # instances of inherited_model
+ base_model = inherited_model.__bases__[0]
+
+ node21 = inherited_model.objects.get(desc='21')
+ node21.delete()
+ node2 = base_model.objects.get(desc='2')
+ for desc in ['21', '211', '212']:
+ assert not base_model.objects.filter(desc=desc).exists()
+ assert [node.desc for node in node2.get_descendants()] == ['22']
+
+
+class TestMP_TreeAlphabet(TestTreeBase):
+ @pytest.mark.skipif(
+ not os.getenv('TREEBEARD_TEST_ALPHABET', False),
+ reason='TREEBEARD_TEST_ALPHABET env variable not set.'
+ )
+ def test_alphabet(self, mpalphabet_model):
+ """This isn't actually a test, it's an informational routine."""
+ basealpha = numconv.BASE85
+ got_err = False
+ last_good = None
+ for alphabetlen in range(3, len(basealpha) + 1):
+ alphabet = basealpha[0:alphabetlen]
+ assert len(alphabet) >= 3
+ expected = [alphabet[0] + char for char in alphabet[1:]]
+ expected.extend([alphabet[1] + char for char in alphabet])
+ expected.append(alphabet[2] + alphabet[0])
+
+ # remove all nodes
+ mpalphabet_model.objects.all().delete()
+
+ # change the model's alphabet
+ mpalphabet_model.alphabet = alphabet
+ mpalphabet_model.numconv_obj_ = None
+
+ # insert root nodes
+ for pos in range(len(alphabet) * 2):
+ try:
+ mpalphabet_model.add_root(numval=pos)
+ except:
+ got_err = True
+ break
+ if got_err:
+ break
+ got = [obj.path
+ for obj in mpalphabet_model.objects.all()]
+ if got != expected:
+ break
+ last_good = alphabet
+ assert False, (
+ 'Best BASE85 based alphabet for your setup: {} (base {})'.format(
+ last_good, len(last_good))
+ )
+
+
+class TestHelpers(TestTreeBase):
+
+ @classmethod
+ def setup_class(cls):
+ for model in models.BASE_MODELS:
+ model.load_bulk(BASE_DATA)
+ for node in model.get_root_nodes():
+ model.load_bulk(BASE_DATA, node)
+ model.add_root(desc='5')
+
+ @classmethod
+ def teardown_class(cls):
+ models.empty_models_tables(models.BASE_MODELS)
+
+ def test_descendants_group_count_root(self, model):
+ expected = [(o.desc, o.get_descendant_count())
+ for o in model.get_root_nodes()]
+ got = [(o.desc, o.descendants_count)
+ for o in model.get_descendants_group_count()]
+ assert got == expected
+
+ def test_descendants_group_count_node(self, model):
+ parent = model.get_root_nodes().get(desc='2')
+ expected = [(o.desc, o.get_descendant_count())
+ for o in parent.get_children()]
+ got = [(o.desc, o.descendants_count)
+ for o in model.get_descendants_group_count(parent)]
+ assert got == expected
+
+
+class TestMP_TreeSortedAutoNow(TestTreeBase):
+ """
+ The sorting mechanism used by treebeard when adding a node can fail if the
+ ordering is using an "auto_now" field
+ """
+
+ def test_sorted_by_autonow_workaround(self, mpsortedautonow_model):
+ # workaround
+ for i in range(1, 5):
+ mpsortedautonow_model.add_root(desc='node%d' % (i, ),
+ created=datetime.datetime.now())
+
+ def test_sorted_by_autonow_FAIL(self, mpsortedautonow_model):
+ """
+ This test asserts that we have a problem.
+ fix this, somehow
+ """
+ mpsortedautonow_model.add_root(desc='node1')
+ with pytest.raises(ValueError):
+ mpsortedautonow_model.add_root(desc='node2')
+
+
+class TestMP_TreeStepOverflow(TestTreeBase):
+ def test_add_root(self, mpsmallstep_model):
+ method = mpsmallstep_model.add_root
+ for i in range(1, 10):
+ method()
+ with pytest.raises(PathOverflow):
+ method()
+
+ def test_add_child(self, mpsmallstep_model):
+ root = mpsmallstep_model.add_root()
+ method = root.add_child
+ for i in range(1, 10):
+ method()
+ with pytest.raises(PathOverflow):
+ method()
+
+ def test_add_sibling(self, mpsmallstep_model):
+ root = mpsmallstep_model.add_root()
+ for i in range(1, 10):
+ root.add_child()
+ positions = ('first-sibling', 'left', 'right', 'last-sibling')
+ for pos in positions:
+ with pytest.raises(PathOverflow):
+ root.get_last_child().add_sibling(pos)
+
+ def test_move(self, mpsmallstep_model):
+ root = mpsmallstep_model.add_root()
+ for i in range(1, 10):
+ root.add_child()
+ newroot = mpsmallstep_model.add_root()
+ targets = [(root, ['first-child', 'last-child']),
+ (root.get_first_child(), ['first-sibling',
+ 'left',
+ 'right',
+ 'last-sibling'])]
+ for target, positions in targets:
+ for pos in positions:
+ with pytest.raises(PathOverflow):
+ newroot.move(target, pos)
+
+
+class TestMP_TreeShortPath(TestTreeBase):
+ """Test a tree with a very small path field (max_length=4) and a
+ steplen of 1
+ """
+
+ def test_short_path(self, mpshortnotsorted_model):
+ obj = mpshortnotsorted_model.add_root()
+ obj = obj.add_child().add_child().add_child()
+ with pytest.raises(PathOverflow):
+ obj.add_child()
+
+
+class TestMP_TreeFindProblems(TestTreeBase):
+ def test_find_problems(self, mpalphabet_model):
+ mpalphabet_model.alphabet = '01234'
+ mpalphabet_model(path='01', depth=1, numchild=0, numval=0).save()
+ mpalphabet_model(path='1', depth=1, numchild=0, numval=0).save()
+ mpalphabet_model(path='111', depth=1, numchild=0, numval=0).save()
+ mpalphabet_model(path='abcd', depth=1, numchild=0, numval=0).save()
+ mpalphabet_model(path='qa#$%!', depth=1, numchild=0, numval=0).save()
+ mpalphabet_model(path='0201', depth=2, numchild=0, numval=0).save()
+ mpalphabet_model(path='020201', depth=3, numchild=0, numval=0).save()
+ mpalphabet_model(path='03', depth=1, numchild=2, numval=0).save()
+ mpalphabet_model(path='0301', depth=2, numchild=0, numval=0).save()
+ mpalphabet_model(path='030102', depth=3, numchild=10, numval=0).save()
+ mpalphabet_model(path='04', depth=10, numchild=1, numval=0).save()
+ mpalphabet_model(path='0401', depth=20, numchild=0, numval=0).save()
+
+ def got(ids):
+ return [o.path for o in
+ mpalphabet_model.objects.filter(id__in=ids)]
+
+ (evil_chars, bad_steplen, orphans, wrong_depth, wrong_numchild) = (
+ mpalphabet_model.find_problems())
+ assert ['abcd', 'qa#$%!'] == got(evil_chars)
+ assert ['1', '111'] == got(bad_steplen)
+ assert ['0201', '020201'] == got(orphans)
+ assert ['03', '0301', '030102'] == got(wrong_numchild)
+ assert ['04', '0401'] == got(wrong_depth)
+
+
+class TestMP_TreeFix(TestTreeBase):
+
+ expected_no_holes = {
+ models.MP_TestNodeShortPath: [
+ ('1', 'b', 1, 2),
+ ('11', 'u', 2, 1),
+ ('111', 'i', 3, 1),
+ ('1111', 'e', 4, 0),
+ ('12', 'o', 2, 0),
+ ('2', 'd', 1, 0),
+ ('3', 'g', 1, 0),
+ ('4', 'a', 1, 4),
+ ('41', 'a', 2, 0),
+ ('42', 'a', 2, 0),
+ ('43', 'u', 2, 1),
+ ('431', 'i', 3, 1),
+ ('4311', 'e', 4, 0),
+ ('44', 'o', 2, 0)],
+ models.MP_TestSortedNodeShortPath: [
+ ('1', 'a', 1, 4),
+ ('11', 'a', 2, 0),
+ ('12', 'a', 2, 0),
+ ('13', 'o', 2, 0),
+ ('14', 'u', 2, 1),
+ ('141', 'i', 3, 1),
+ ('1411', 'e', 4, 0),
+ ('2', 'b', 1, 2),
+ ('21', 'o', 2, 0),
+ ('22', 'u', 2, 1),
+ ('221', 'i', 3, 1),
+ ('2211', 'e', 4, 0),
+ ('3', 'd', 1, 0),
+ ('4', 'g', 1, 0)]}
+ expected_with_holes = {
+ models.MP_TestNodeShortPath: [
+ ('1', 'b', 1, 2),
+ ('13', 'u', 2, 1),
+ ('134', 'i', 3, 1),
+ ('1343', 'e', 4, 0),
+ ('14', 'o', 2, 0),
+ ('2', 'd', 1, 0),
+ ('3', 'g', 1, 0),
+ ('4', 'a', 1, 4),
+ ('41', 'a', 2, 0),
+ ('42', 'a', 2, 0),
+ ('43', 'u', 2, 1),
+ ('434', 'i', 3, 1),
+ ('4343', 'e', 4, 0),
+ ('44', 'o', 2, 0)],
+ models.MP_TestSortedNodeShortPath: [
+ ('1', 'b', 1, 2),
+ ('13', 'u', 2, 1),
+ ('134', 'i', 3, 1),
+ ('1343', 'e', 4, 0),
+ ('14', 'o', 2, 0),
+ ('2', 'd', 1, 0),
+ ('3', 'g', 1, 0),
+ ('4', 'a', 1, 4),
+ ('41', 'a', 2, 0),
+ ('42', 'a', 2, 0),
+ ('43', 'u', 2, 1),
+ ('434', 'i', 3, 1),
+ ('4343', 'e', 4, 0),
+ ('44', 'o', 2, 0)]}
+
+ def got(self, model):
+ return [(o.path, o.desc, o.get_depth(), o.get_children_count())
+ for o in model.get_tree()]
+
+ def add_broken_test_data(self, model):
+ model(path='4', depth=2, numchild=2, desc='a').save()
+ model(path='13', depth=1000, numchild=0, desc='u').save()
+ model(path='14', depth=4, numchild=500, desc='o').save()
+ model(path='134', depth=321, numchild=543, desc='i').save()
+ model(path='1343', depth=321, numchild=543, desc='e').save()
+ model(path='42', depth=1, numchild=1, desc='a').save()
+ model(path='43', depth=1000, numchild=0, desc='u').save()
+ model(path='44', depth=4, numchild=500, desc='o').save()
+ model(path='434', depth=321, numchild=543, desc='i').save()
+ model(path='4343', depth=321, numchild=543, desc='e').save()
+ model(path='41', depth=1, numchild=1, desc='a').save()
+ model(path='3', depth=221, numchild=322, desc='g').save()
+ model(path='1', depth=10, numchild=3, desc='b').save()
+ model(path='2', depth=10, numchild=3, desc='d').save()
+
+ def test_fix_tree_non_destructive(self, mpshort_model):
+ self.add_broken_test_data(mpshort_model)
+ mpshort_model.fix_tree(destructive=False)
+ got = self.got(mpshort_model)
+ expected = self.expected_with_holes[mpshort_model]
+ assert got == expected
+ mpshort_model.find_problems()
+
+ def test_fix_tree_destructive(self, mpshort_model):
+ self.add_broken_test_data(mpshort_model)
+ mpshort_model.fix_tree(destructive=True)
+ got = self.got(mpshort_model)
+ expected = self.expected_no_holes[mpshort_model]
+ assert got == expected
+ mpshort_model.find_problems()
+
+
+class TestIssues(TestTreeBase):
+ # test for http://code.google.com/p/django-treebeard/issues/detail?id=14
+
+ def test_many_to_many_django_user_anonymous(self, mpm2muser_model):
+ # Using AnonymousUser() in the querysets will expose non-treebeard
+ # related problems in Django 1.0
+ #
+ # Postgres:
+ # ProgrammingError: can't adapt
+ # SQLite:
+ # InterfaceError: Error binding parameter 4 - probably unsupported
+ # type.
+ # MySQL compared a string to an integer field:
+ # `treebeard_mp_testissue14_users`.`user_id` = 'AnonymousUser'
+ #
+ # Using a None field instead works (will be translated to IS NULL).
+ #
+ # anonuserobj = AnonymousUser()
+ anonuserobj = None
+
+ def qs_check(qs, expected):
+ assert [o.name for o in qs] == expected
+
+ def qs_check_first_or_user(expected, root, user):
+ qs_check(
+ root.get_children().filter(Q(name="first") | Q(users=user)),
+ expected)
+
+ user = User.objects.create_user('test_user', 'test@example.com',
+ 'testpasswd')
+ user.save()
+ root = mpm2muser_model.add_root(name="the root node")
+
+ root.add_child(name="first")
+ second = root.add_child(name="second")
+
+ qs_check(root.get_children(), ['first', 'second'])
+ qs_check(root.get_children().filter(Q(name="first")), ['first'])
+ qs_check(root.get_children().filter(Q(users=user)), [])
+
+ qs_check_first_or_user(['first'], root, user)
+
+ qs_check_first_or_user(['first', 'second'], root, anonuserobj)
+
+ user = User.objects.get(username="test_user")
+ second.users.add(user)
+ qs_check_first_or_user(['first', 'second'], root, user)
+
+ qs_check_first_or_user(['first'], root, anonuserobj)
+
+
+class TestMoveNodeForm(TestNonEmptyTree):
+ def _get_nodes_list(self, nodes):
+ return [(pk, '%sNode %d' % (' ' * 4 * (depth - 1), pk))
+ for pk, depth in nodes]
+
+ def _assert_nodes_in_choices(self, form, nodes):
+ choices = form.fields['_ref_node_id'].choices
+ assert 0 == choices.pop(0)[0]
+ assert nodes == [(choice[0], choice[1]) for choice in choices]
+
+ def _move_node_helper(self, node, safe_parent_nodes):
+ form_class = movenodeform_factory(type(node))
+ form = form_class(instance=node)
+ assert ['desc', '_position', '_ref_node_id'] == list(
+ form.base_fields.keys())
+ got = [choice[0] for choice in form.fields['_position'].choices]
+ assert ['first-child', 'left', 'right'] == got
+ nodes = self._get_nodes_list(safe_parent_nodes)
+ self._assert_nodes_in_choices(form, nodes)
+
+ def _get_node_ids_and_depths(self, nodes):
+ return [(node.pk, node.get_depth()) for node in nodes]
+
+ def test_form_root_node(self, model):
+ nodes = list(model.get_tree())
+ node = nodes.pop(0)
+ safe_parent_nodes = self._get_node_ids_and_depths(nodes)
+ self._move_node_helper(node, safe_parent_nodes)
+
+ def test_form_leaf_node(self, model):
+ nodes = list(model.get_tree())
+ node = nodes.pop()
+ safe_parent_nodes = self._get_node_ids_and_depths(nodes)
+ self._move_node_helper(node, safe_parent_nodes)
+
+ def test_form_admin(self, model):
+ request = None
+ nodes = list(model.get_tree())
+ safe_parent_nodes = self._get_node_ids_and_depths(nodes)
+ for node in model.objects.all():
+ site = AdminSite()
+ form_class = movenodeform_factory(model)
+ admin_class = admin_factory(form_class)
+ ma = admin_class(model, site)
+ got = list(ma.get_form(request).base_fields.keys())
+ desc_pos_refnodeid = ['desc', '_position', '_ref_node_id']
+ assert desc_pos_refnodeid == got
+ got = ma.get_fieldsets(request)
+ expected = [(None, {'fields': desc_pos_refnodeid})]
+ assert got == expected
+ got = ma.get_fieldsets(request, node)
+ assert got == expected
+ form = ma.get_form(request)()
+ nodes = self._get_nodes_list(safe_parent_nodes)
+ self._assert_nodes_in_choices(form, nodes)
+
+
+class TestModelAdmin(TestNonEmptyTree):
+ def test_default_fields(self, model):
+ site = AdminSite()
+ form_class = movenodeform_factory(model)
+ admin_class = admin_factory(form_class)
+ ma = admin_class(model, site)
+ assert list(ma.get_form(None).base_fields.keys()) == [
+ 'desc', '_position', '_ref_node_id']
+
+
+class TestSortedForm(TestTreeSorted):
+ def test_sorted_form(self, sorted_model):
+ sorted_model.add_root(val1=3, val2=3, desc='zxy')
+ sorted_model.add_root(val1=1, val2=4, desc='bcd')
+ sorted_model.add_root(val1=2, val2=5, desc='zxy')
+ sorted_model.add_root(val1=3, val2=3, desc='abc')
+ sorted_model.add_root(val1=4, val2=1, desc='fgh')
+ sorted_model.add_root(val1=3, val2=3, desc='abc')
+ sorted_model.add_root(val1=2, val2=2, desc='qwe')
+ sorted_model.add_root(val1=3, val2=2, desc='vcx')
+
+ form_class = movenodeform_factory(sorted_model)
+ form = form_class()
+ assert list(form.fields.keys()) == ['val1', 'val2', 'desc',
+ '_position', '_ref_node_id']
+
+ form = form_class(instance=sorted_model.objects.get(desc='bcd'))
+ assert list(form.fields.keys()) == ['val1', 'val2', 'desc',
+ '_position', '_ref_node_id']
+ assert 'id__position' in str(form)
+ assert 'id__ref_node_id' in str(form)
+
+
+class TestForm(TestNonEmptyTree):
+ def test_form(self, model):
+ form_class = movenodeform_factory(model)
+ form = form_class()
+ assert list(form.fields.keys()) == ['desc', '_position',
+ '_ref_node_id']
+
+ form = form_class(instance=model.objects.get(desc='1'))
+ assert list(form.fields.keys()) == ['desc', '_position',
+ '_ref_node_id']
+ assert 'id__position' in str(form)
+ assert 'id__ref_node_id' in str(form)
+
+ def test_move_node_form(self, model):
+ form_class = movenodeform_factory(model)
+
+ bad_node = model.objects.get(desc='1').add_child(
+ desc='Benign'
+ )
+
+ form = form_class(instance=bad_node)
+ rendered_html = form.as_p()
+ assert "Benign" in rendered_html
+ assert "'
+ ''
+ ''
+ '')
+ assert expected == rendered
+
+ def test_get_static_url(self):
+ with self.settings(STATIC_URL=None, MEDIA_URL=None):
+ assert get_static_url() == '/'
+ with self.settings(STATIC_URL='/static/', MEDIA_URL=None):
+ assert get_static_url() == '/static/'
+ with self.settings(STATIC_URL=None, MEDIA_URL='/media/'):
+ assert get_static_url() == '/media/'
+ with self.settings(STATIC_URL='/static/', MEDIA_URL='/media/'):
+ assert get_static_url() == '/static/'
+
+
+class TestAdminTree(TestNonEmptyTree):
+ template = Template('{% load admin_tree %}{% spaceless %}'
+ '{% result_tree cl request %}{% endspaceless %}')
+
+ def test_result_tree(self, model_without_proxy):
+ """
+ Verifies that inclusion tag result_list generates a table when with
+ default ModelAdmin settings.
+ """
+ model = model_without_proxy
+ request = RequestFactory().get('/admin/tree/')
+ site = AdminSite()
+ form_class = movenodeform_factory(model)
+ admin_class = admin_factory(form_class)
+ m = admin_class(model, site)
+ list_display = m.get_list_display(request)
+ list_display_links = m.get_list_display_links(request, list_display)
+ cl = ChangeList(request, model, list_display, list_display_links,
+ m.list_filter, m.date_hierarchy, m.search_fields,
+ m.list_select_related, m.list_per_page,
+ m.list_max_show_all, m.list_editable, m)
+ cl.formset = None
+ context = Context({'cl': cl,
+ 'request': request})
+ table_output = self.template.render(context)
+ # We have the same amount of drag handlers as objects
+ drag_handler = ' '
+ assert table_output.count(drag_handler) == model.objects.count()
+ # All nodes are in the result tree
+ for object in model.objects.all():
+ url = cl.url_for_result(object)
+ node = 'Node %i ' % (url, object.pk)
+ assert node in table_output
+ # Unfiltered
+ assert ' ' in \
+ table_output
+
+ def test_unicode_result_tree(self, model_with_unicode):
+ """
+ Verifies that inclusion tag result_list generates a table when with
+ default ModelAdmin settings.
+ """
+ model = model_with_unicode
+ # Add a unicode description
+ model.add_root(desc='áéîøü')
+ request = RequestFactory().get('/admin/tree/')
+ site = AdminSite()
+ form_class = movenodeform_factory(model)
+ ModelAdmin = admin_factory(form_class)
+
+ class UnicodeModelAdmin(ModelAdmin):
+ list_display = ('__str__', 'desc')
+
+ m = UnicodeModelAdmin(model, site)
+ list_display = m.get_list_display(request)
+ list_display_links = m.get_list_display_links(request, list_display)
+ cl = ChangeList(request, model, list_display, list_display_links,
+ m.list_filter, m.date_hierarchy, m.search_fields,
+ m.list_select_related, m.list_per_page,
+ m.list_max_show_all, m.list_editable, m)
+ cl.formset = None
+ context = Context({'cl': cl,
+ 'request': request})
+ table_output = self.template.render(context)
+ # We have the same amount of drag handlers as objects
+ drag_handler = ' '
+ assert table_output.count(drag_handler) == model.objects.count()
+ # All nodes are in the result tree
+ for object in model.objects.all():
+ url = cl.url_for_result(object)
+ node = '%s ' % (url, object.desc)
+ assert node in table_output
+ # Unfiltered
+ assert ' ' in \
+ table_output
+
+ def test_result_filtered(self, model_without_proxy):
+ """ Test template changes with filters or pagination.
+ """
+ model = model_without_proxy
+ # Filtered GET
+ request = RequestFactory().get('/admin/tree/?desc=1')
+ site = AdminSite()
+ form_class = movenodeform_factory(model)
+ admin_class = admin_factory(form_class)
+ m = admin_class(model, site)
+ list_display = m.get_list_display(request)
+ list_display_links = m.get_list_display_links(request, list_display)
+ cl = ChangeList(request, model, list_display, list_display_links,
+ m.list_filter, m.date_hierarchy, m.search_fields,
+ m.list_select_related, m.list_per_page,
+ m.list_max_show_all, m.list_editable, m)
+ cl.formset = None
+ context = Context({'cl': cl,
+ 'request': request})
+ table_output = self.template.render(context)
+ # Filtered
+ assert ' ' in \
+ table_output
+
+ # Not Filtered GET, it should ignore pagination
+ request = RequestFactory().get('/admin/tree/?p=1')
+ list_display = m.get_list_display(request)
+ list_display_links = m.get_list_display_links(request, list_display)
+ cl = ChangeList(request, model, list_display, list_display_links,
+ m.list_filter, m.date_hierarchy, m.search_fields,
+ m.list_select_related, m.list_per_page,
+ m.list_max_show_all, m.list_editable, m)
+ cl.formset = None
+ context = Context({'cl': cl,
+ 'request': request})
+ table_output = self.template.render(context)
+ # Not Filtered
+ assert ' ' in \
+ table_output
+
+ # Not Filtered GET, it should ignore all
+ request = RequestFactory().get('/admin/tree/?all=1')
+ list_display = m.get_list_display(request)
+ list_display_links = m.get_list_display_links(request, list_display)
+ cl = ChangeList(request, model, list_display, list_display_links,
+ m.list_filter, m.date_hierarchy, m.search_fields,
+ m.list_select_related, m.list_per_page,
+ m.list_max_show_all, m.list_editable, m)
+ cl.formset = None
+ context = Context({'cl': cl,
+ 'request': request})
+ table_output = self.template.render(context)
+ # Not Filtered
+ assert ' ' in \
+ table_output
+
+
+class TestAdminTreeList(TestNonEmptyTree):
+ template = Template('{% load admin_tree_list %}{% spaceless %}'
+ '{% result_tree cl request %}{% endspaceless %}')
+
+ def test_result_tree_list(self, model_without_proxy):
+ """
+ Verifies that inclusion tag result_list generates a table when with
+ default ModelAdmin settings.
+ """
+ model = model_without_proxy
+ request = RequestFactory().get('/admin/tree/')
+ site = AdminSite()
+ form_class = movenodeform_factory(model)
+ admin_class = admin_factory(form_class)
+ m = admin_class(model, site)
+ list_display = m.get_list_display(request)
+ list_display_links = m.get_list_display_links(request, list_display)
+ cl = ChangeList(request, model, list_display, list_display_links,
+ m.list_filter, m.date_hierarchy, m.search_fields,
+ m.list_select_related, m.list_per_page,
+ m.list_max_show_all, m.list_editable, m)
+ cl.formset = None
+ context = Context({'cl': cl,
+ 'request': request})
+ table_output = self.template.render(context)
+
+ output_template = 'Node %i '
+ for object in model.objects.all():
+ expected_output = output_template % (object.pk, object.pk)
+ assert expected_output in table_output
+
+ def test_result_tree_list_with_action(self, model_without_proxy):
+ model = model_without_proxy
+ request = RequestFactory().get('/admin/tree/')
+ site = AdminSite()
+ form_class = movenodeform_factory(model)
+ admin_class = admin_factory(form_class)
+ m = admin_class(model, site)
+ list_display = m.get_list_display(request)
+ list_display_links = m.get_list_display_links(request, list_display)
+ cl = ChangeList(request, model, list_display, list_display_links,
+ m.list_filter, m.date_hierarchy, m.search_fields,
+ m.list_select_related, m.list_per_page,
+ m.list_max_show_all, m.list_editable, m)
+ cl.formset = None
+ context = Context({'cl': cl,
+ 'request': request,
+ 'action_form': True})
+ table_output = self.template.render(context)
+ output_template = (' '
+ 'Node %i ')
+
+ for object in model.objects.all():
+ expected_output = output_template % (object.pk, object.pk,
+ object.pk)
+ assert expected_output in table_output
+
+ def test_result_tree_list_with_get(self, model_without_proxy):
+ model = model_without_proxy
+ # Test t GET parameter with value id
+ request = RequestFactory().get(
+ '/admin/tree/?{0}=id'.format(TO_FIELD_VAR))
+ site = AdminSite()
+ admin_register_all(site)
+ form_class = movenodeform_factory(model)
+ admin_class = admin_factory(form_class)
+ m = admin_class(model, site)
+ list_display = m.get_list_display(request)
+ list_display_links = m.get_list_display_links(request, list_display)
+ cl = ChangeList(request, model, list_display, list_display_links,
+ m.list_filter, m.date_hierarchy, m.search_fields,
+ m.list_select_related, m.list_per_page,
+ m.list_max_show_all, m.list_editable, m)
+ cl.formset = None
+ context = Context({'cl': cl,
+ 'request': request})
+ table_output = self.template.render(context)
+ output_template = "opener.dismissRelatedLookupPopup(window, '%i');"
+ for object in model.objects.all():
+ expected_output = output_template % object.pk
+ assert expected_output in table_output
+
+
+class TestTreeAdmin(TestNonEmptyTree):
+ site = AdminSite()
+
+ def _create_superuser(self, username):
+ return User.objects.create(username=username, is_superuser=True)
+
+ def _mocked_authenticated_request(self, url, user):
+ request_factory = RequestFactory()
+ request = request_factory.get(url)
+ request.user = user
+ return request
+
+ def _mocked_request(self, data):
+ request_factory = RequestFactory()
+ request = request_factory.post('/', data=data)
+ setattr(request, 'session', 'session')
+ messages = FallbackStorage(request)
+ setattr(request, '_messages', messages)
+ return request
+
+ def _get_admin_obj(self, model_class):
+ form_class = movenodeform_factory(model_class)
+ admin_class = admin_factory(form_class)
+ return admin_class(model_class, self.site)
+
+ def test_changelist_view(self):
+ tmp_user = self._create_superuser('changelist_tmp')
+ request = self._mocked_authenticated_request('/', tmp_user)
+ admin_obj = self._get_admin_obj(models.AL_TestNode)
+ admin_obj.changelist_view(request)
+ assert admin_obj.change_list_template == 'admin/tree_list.html'
+
+ admin_obj = self._get_admin_obj(models.MP_TestNode)
+ admin_obj.changelist_view(request)
+ assert admin_obj.change_list_template != 'admin/tree_list.html'
+
+ def test_get_node(self, model):
+ admin_obj = self._get_admin_obj(model)
+ target = model.objects.get(desc='2')
+ assert admin_obj.get_node(target.pk) == target
+
+ def test_move_node_validate_keyerror(self, model):
+ admin_obj = self._get_admin_obj(model)
+ request = self._mocked_request(data={})
+ response = admin_obj.move_node(request)
+ assert response.status_code == 400
+ request = self._mocked_request(data={'node_id': 1})
+ response = admin_obj.move_node(request)
+ assert response.status_code == 400
+
+ def test_move_node_validate_valueerror(self, model):
+ admin_obj = self._get_admin_obj(model)
+ request = self._mocked_request(data={'node_id': 1,
+ 'sibling_id': 2,
+ 'as_child': 'invalid'})
+ response = admin_obj.move_node(request)
+ assert response.status_code == 400
+
+ def test_move_validate_missing_nodeorderby(self, model):
+ node = model.objects.get(desc='231')
+ admin_obj = self._get_admin_obj(model)
+ request = self._mocked_request(data={})
+ response = admin_obj.try_to_move_node(True, node, 'sorted-child',
+ request, target=node)
+ assert response.status_code == 400
+
+ response = admin_obj.try_to_move_node(True, node, 'sorted-sibling',
+ request, target=node)
+ assert response.status_code == 400
+
+ def test_move_validate_invalid_pos(self, model):
+ node = model.objects.get(desc='231')
+ admin_obj = self._get_admin_obj(model)
+ request = self._mocked_request(data={})
+ response = admin_obj.try_to_move_node(True, node, 'invalid_pos',
+ request, target=node)
+ assert response.status_code == 400
+
+ def test_move_validate_to_descendant(self, model):
+ node = model.objects.get(desc='2')
+ target = model.objects.get(desc='231')
+ admin_obj = self._get_admin_obj(model)
+ request = self._mocked_request(data={})
+ response = admin_obj.try_to_move_node(True, node, 'first-sibling',
+ request, target)
+ assert response.status_code == 400
+
+ def test_move_left(self, model):
+ node = model.objects.get(desc='231')
+ target = model.objects.get(desc='2')
+
+ admin_obj = self._get_admin_obj(model)
+ request = self._mocked_request(data={'node_id': node.pk,
+ 'sibling_id': target.pk,
+ 'as_child': 0})
+ response = admin_obj.move_node(request)
+ assert response.status_code == 200
+ expected = [('1', 1, 0),
+ ('231', 1, 0),
+ ('2', 1, 4),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 0),
+ ('24', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
+
+ def test_move_last_child(self, model):
+ node = model.objects.get(desc='231')
+ target = model.objects.get(desc='2')
+
+ admin_obj = self._get_admin_obj(model)
+ request = self._mocked_request(data={'node_id': node.pk,
+ 'sibling_id': target.pk,
+ 'as_child': 1})
+ response = admin_obj.move_node(request)
+ assert response.status_code == 200
+ expected = [('1', 1, 0),
+ ('2', 1, 5),
+ ('21', 2, 0),
+ ('22', 2, 0),
+ ('23', 2, 0),
+ ('24', 2, 0),
+ ('231', 2, 0),
+ ('3', 1, 0),
+ ('4', 1, 1),
+ ('41', 2, 0)]
+ assert self.got(model) == expected
diff --git a/vendor/treebeard/tests/urls.py b/vendor/treebeard/tests/urls.py
new file mode 100644
index 00000000..56cd0fef
--- /dev/null
+++ b/vendor/treebeard/tests/urls.py
@@ -0,0 +1,8 @@
+from django.conf.urls import include, url
+from django.contrib import admin
+
+admin.autodiscover()
+
+urlpatterns = [
+ url(r'^admin/', admin.site.urls),
+]