From 6defe00adbf9e1cf33a8b8879f2b243581ee70b4 Mon Sep 17 00:00:00 2001 From: Bruksx Date: Fri, 2 Oct 2026 15:44:50 +0100 Subject: [PATCH] vendor backport_collections, sekizai, stored_messages, treebeard --- alplora/urls.py | 2 +- datacenterlight/cms_models.py | 2 +- datacenterlight/urls.py | 2 +- digitalglarus/models.py | 2 +- digitalglarus/urls.py | 2 +- dynamicweb/settings/base.py | 4 +- dynamicweb/wsgi.py | 17 +- hosting/models.py | 6 +- hosting/urls.py | 2 +- legacy_compat.py | 8 +- membership/urls.py | 2 +- ungleich/models.py | 3 +- ungleich/urls.py | 2 +- ungleich_page/urls.py | 2 +- vendor/backport_collections.py | 732 +++++ vendor/sekizai/__init__.py | 1 + vendor/sekizai/context.py | 12 + vendor/sekizai/context_processors.py | 12 + vendor/sekizai/data.py | 28 + vendor/sekizai/helpers.py | 177 ++ vendor/sekizai/models.py | 0 vendor/sekizai/templatetags/__init__.py | 0 vendor/sekizai/templatetags/sekizai_tags.py | 164 + vendor/sekizai/test_templates/basic.html | 19 + vendor/sekizai/test_templates/css.html | 7 + vendor/sekizai/test_templates/css2.html | 6 + vendor/sekizai/test_templates/easy_base.html | 5 + .../sekizai/test_templates/easy_inherit.html | 3 + vendor/sekizai/test_templates/eat.html | 7 + .../test_templates/errors/failadd.html | 5 + .../test_templates/errors/failbase.html | 9 + .../test_templates/errors/failbase2.html | 9 + .../test_templates/errors/failinc.html | 5 + .../test_templates/errors/failrender.html | 3 + .../sekizai/test_templates/inherit/base.html | 29 + .../test_templates/inherit/baseinc.html | 7 + .../sekizai/test_templates/inherit/chain.html | 7 + .../test_templates/inherit/extend.html | 11 + .../test_templates/inherit/extinc.html | 3 + .../test_templates/inherit/nullbase.html | 0 .../test_templates/inherit/nullext.html | 5 + .../test_templates/inherit/spacechain.html | 1 + .../test_templates/inherit/subvarchain.html | 1 + .../test_templates/inherit/super_blocks.html | 15 + .../test_templates/inherit/varchain.html | 2 + vendor/sekizai/test_templates/named_end.html | 7 + vendor/sekizai/test_templates/namespaces.html | 16 + .../processors/addtoblock_namespace.html | 11 + .../processors/addtoblock_null.html | 11 + .../test_templates/processors/namespace.html | 11 + .../test_templates/processors/null.html | 11 + vendor/sekizai/test_templates/unique.html | 6 + vendor/sekizai/test_templates/variables.html | 16 + vendor/sekizai/test_templates/with_data.html | 9 + vendor/sekizai/tests.py | 483 +++ vendor/stored_messages/__init__.py | 11 + vendor/stored_messages/admin.py | 7 + vendor/stored_messages/api.py | 69 + vendor/stored_messages/backends/__init__.py | 3 + vendor/stored_messages/backends/base.py | 159 + .../backends/default/__init__.py | 2 + .../backends/default/backend.py | 81 + vendor/stored_messages/backends/exceptions.py | 8 + .../backends/redis/__init__.py | 2 + .../stored_messages/backends/redis/backend.py | 143 + vendor/stored_messages/backends/signals.py | 7 + vendor/stored_messages/compat.py | 6 + vendor/stored_messages/constants.py | 16 + .../migrations/0001_initial.py | 54 + .../migrations/0002_message_url.py | 19 + ...lter_inbox_id_alter_message_id_and_more.py | 28 + vendor/stored_messages/migrations/__init__.py | 0 vendor/stored_messages/models.py | 66 + vendor/stored_messages/serializers.py | 10 + vendor/stored_messages/settings.py | 130 + .../static/js/stored_messages.js | 0 vendor/stored_messages/storage.py | 103 + .../templates/stored_messages/base.html | 21 + .../stored_messages/stored_messages_list.html | 7 + .../stored_messages/templatetags/__init__.py | 0 .../templatetags/stored_messages_tags.py | 49 + vendor/stored_messages/tests/__init__.py | 0 vendor/stored_messages/tests/base.py | 40 + vendor/stored_messages/tests/settings.py | 69 + vendor/stored_messages/tests/test_api.py | 93 + .../tests/test_default_backend.py | 134 + vendor/stored_messages/tests/test_models.py | 22 + .../tests/test_redis_backend.py | 201 ++ vendor/stored_messages/tests/test_restapi.py | 80 + vendor/stored_messages/tests/test_storage.py | 59 + .../tests/test_templatetags.py | 68 + vendor/stored_messages/tests/urls.py | 14 + vendor/stored_messages/tests/views.py | 27 + vendor/stored_messages/urls.py | 17 + vendor/stored_messages/views.py | 60 + vendor/treebeard/__init__.py | 20 + vendor/treebeard/admin.py | 147 + vendor/treebeard/al_tree.py | 404 +++ vendor/treebeard/exceptions.py | 31 + vendor/treebeard/forms.py | 229 ++ .../treebeard/locale/de/LC_MESSAGES/django.po | 86 + .../locale/de/LC_MESSAGES/djangojs.po | 30 + .../treebeard/locale/es/LC_MESSAGES/django.po | 78 + .../locale/es/LC_MESSAGES/djangojs.po | 24 + .../treebeard/locale/nl/LC_MESSAGES/django.po | 80 + .../locale/nl/LC_MESSAGES/djangojs.po | 24 + .../treebeard/locale/pl/LC_MESSAGES/django.po | 44 + .../treebeard/locale/ru/LC_MESSAGES/django.po | 79 + .../locale/ru/LC_MESSAGES/djangojs.po | 25 + vendor/treebeard/models.py | 658 ++++ vendor/treebeard/mp_tree.py | 1152 +++++++ vendor/treebeard/ns_tree.py | 674 ++++ vendor/treebeard/numconv.py | 115 + .../static/treebeard/expand-collapse.png | Bin 0 -> 1020 bytes .../treebeard/jquery-ui-1.8.5.custom.min.js | 1816 +++++++++++ .../static/treebeard/treebeard-admin.css | 83 + .../static/treebeard/treebeard-admin.js | 314 ++ .../templates/admin/tree_change_list.html | 23 + .../admin/tree_change_list_results.html | 40 + .../treebeard/templates/admin/tree_list.html | 21 + .../templates/admin/tree_list_results.html | 7 + vendor/treebeard/templatetags/__init__.py | 12 + vendor/treebeard/templatetags/admin_tree.py | 302 ++ .../treebeard/templatetags/admin_tree_list.py | 47 + vendor/treebeard/tests/__init__.py | 0 vendor/treebeard/tests/admin.py | 18 + vendor/treebeard/tests/conftest.py | 34 + .../tests/jenkins/rm_workspace_coverage.py | 23 + vendor/treebeard/tests/jenkins/toxhelper.py | 37 + vendor/treebeard/tests/models.py | 265 ++ vendor/treebeard/tests/settings.py | 110 + vendor/treebeard/tests/test_treebeard.py | 2704 +++++++++++++++++ vendor/treebeard/tests/urls.py | 8 + 133 files changed, 13549 insertions(+), 22 deletions(-) create mode 100644 vendor/backport_collections.py create mode 100644 vendor/sekizai/__init__.py create mode 100644 vendor/sekizai/context.py create mode 100644 vendor/sekizai/context_processors.py create mode 100644 vendor/sekizai/data.py create mode 100644 vendor/sekizai/helpers.py create mode 100644 vendor/sekizai/models.py create mode 100644 vendor/sekizai/templatetags/__init__.py create mode 100644 vendor/sekizai/templatetags/sekizai_tags.py create mode 100644 vendor/sekizai/test_templates/basic.html create mode 100644 vendor/sekizai/test_templates/css.html create mode 100644 vendor/sekizai/test_templates/css2.html create mode 100644 vendor/sekizai/test_templates/easy_base.html create mode 100644 vendor/sekizai/test_templates/easy_inherit.html create mode 100644 vendor/sekizai/test_templates/eat.html create mode 100644 vendor/sekizai/test_templates/errors/failadd.html create mode 100644 vendor/sekizai/test_templates/errors/failbase.html create mode 100644 vendor/sekizai/test_templates/errors/failbase2.html create mode 100644 vendor/sekizai/test_templates/errors/failinc.html create mode 100644 vendor/sekizai/test_templates/errors/failrender.html create mode 100644 vendor/sekizai/test_templates/inherit/base.html create mode 100644 vendor/sekizai/test_templates/inherit/baseinc.html create mode 100644 vendor/sekizai/test_templates/inherit/chain.html create mode 100644 vendor/sekizai/test_templates/inherit/extend.html create mode 100644 vendor/sekizai/test_templates/inherit/extinc.html create mode 100644 vendor/sekizai/test_templates/inherit/nullbase.html create mode 100644 vendor/sekizai/test_templates/inherit/nullext.html create mode 100644 vendor/sekizai/test_templates/inherit/spacechain.html create mode 100644 vendor/sekizai/test_templates/inherit/subvarchain.html create mode 100644 vendor/sekizai/test_templates/inherit/super_blocks.html create mode 100644 vendor/sekizai/test_templates/inherit/varchain.html create mode 100644 vendor/sekizai/test_templates/named_end.html create mode 100644 vendor/sekizai/test_templates/namespaces.html create mode 100644 vendor/sekizai/test_templates/processors/addtoblock_namespace.html create mode 100644 vendor/sekizai/test_templates/processors/addtoblock_null.html create mode 100644 vendor/sekizai/test_templates/processors/namespace.html create mode 100644 vendor/sekizai/test_templates/processors/null.html create mode 100644 vendor/sekizai/test_templates/unique.html create mode 100644 vendor/sekizai/test_templates/variables.html create mode 100644 vendor/sekizai/test_templates/with_data.html create mode 100644 vendor/sekizai/tests.py create mode 100644 vendor/stored_messages/__init__.py create mode 100644 vendor/stored_messages/admin.py create mode 100644 vendor/stored_messages/api.py create mode 100644 vendor/stored_messages/backends/__init__.py create mode 100644 vendor/stored_messages/backends/base.py create mode 100644 vendor/stored_messages/backends/default/__init__.py create mode 100644 vendor/stored_messages/backends/default/backend.py create mode 100644 vendor/stored_messages/backends/exceptions.py create mode 100644 vendor/stored_messages/backends/redis/__init__.py create mode 100644 vendor/stored_messages/backends/redis/backend.py create mode 100644 vendor/stored_messages/backends/signals.py create mode 100644 vendor/stored_messages/compat.py create mode 100644 vendor/stored_messages/constants.py create mode 100644 vendor/stored_messages/migrations/0001_initial.py create mode 100644 vendor/stored_messages/migrations/0002_message_url.py create mode 100644 vendor/stored_messages/migrations/0003_alter_inbox_id_alter_message_id_and_more.py create mode 100644 vendor/stored_messages/migrations/__init__.py create mode 100644 vendor/stored_messages/models.py create mode 100644 vendor/stored_messages/serializers.py create mode 100644 vendor/stored_messages/settings.py create mode 100644 vendor/stored_messages/static/js/stored_messages.js create mode 100644 vendor/stored_messages/storage.py create mode 100644 vendor/stored_messages/templates/stored_messages/base.html create mode 100644 vendor/stored_messages/templates/stored_messages/stored_messages_list.html create mode 100644 vendor/stored_messages/templatetags/__init__.py create mode 100644 vendor/stored_messages/templatetags/stored_messages_tags.py create mode 100644 vendor/stored_messages/tests/__init__.py create mode 100644 vendor/stored_messages/tests/base.py create mode 100644 vendor/stored_messages/tests/settings.py create mode 100644 vendor/stored_messages/tests/test_api.py create mode 100644 vendor/stored_messages/tests/test_default_backend.py create mode 100644 vendor/stored_messages/tests/test_models.py create mode 100644 vendor/stored_messages/tests/test_redis_backend.py create mode 100644 vendor/stored_messages/tests/test_restapi.py create mode 100644 vendor/stored_messages/tests/test_storage.py create mode 100644 vendor/stored_messages/tests/test_templatetags.py create mode 100644 vendor/stored_messages/tests/urls.py create mode 100644 vendor/stored_messages/tests/views.py create mode 100644 vendor/stored_messages/urls.py create mode 100644 vendor/stored_messages/views.py create mode 100644 vendor/treebeard/__init__.py create mode 100644 vendor/treebeard/admin.py create mode 100644 vendor/treebeard/al_tree.py create mode 100644 vendor/treebeard/exceptions.py create mode 100644 vendor/treebeard/forms.py create mode 100644 vendor/treebeard/locale/de/LC_MESSAGES/django.po create mode 100644 vendor/treebeard/locale/de/LC_MESSAGES/djangojs.po create mode 100644 vendor/treebeard/locale/es/LC_MESSAGES/django.po create mode 100644 vendor/treebeard/locale/es/LC_MESSAGES/djangojs.po create mode 100644 vendor/treebeard/locale/nl/LC_MESSAGES/django.po create mode 100644 vendor/treebeard/locale/nl/LC_MESSAGES/djangojs.po create mode 100644 vendor/treebeard/locale/pl/LC_MESSAGES/django.po create mode 100644 vendor/treebeard/locale/ru/LC_MESSAGES/django.po create mode 100644 vendor/treebeard/locale/ru/LC_MESSAGES/djangojs.po create mode 100644 vendor/treebeard/models.py create mode 100644 vendor/treebeard/mp_tree.py create mode 100644 vendor/treebeard/ns_tree.py create mode 100644 vendor/treebeard/numconv.py create mode 100644 vendor/treebeard/static/treebeard/expand-collapse.png create mode 100644 vendor/treebeard/static/treebeard/jquery-ui-1.8.5.custom.min.js create mode 100644 vendor/treebeard/static/treebeard/treebeard-admin.css create mode 100644 vendor/treebeard/static/treebeard/treebeard-admin.js create mode 100644 vendor/treebeard/templates/admin/tree_change_list.html create mode 100644 vendor/treebeard/templates/admin/tree_change_list_results.html create mode 100644 vendor/treebeard/templates/admin/tree_list.html create mode 100644 vendor/treebeard/templates/admin/tree_list_results.html create mode 100644 vendor/treebeard/templatetags/__init__.py create mode 100644 vendor/treebeard/templatetags/admin_tree.py create mode 100644 vendor/treebeard/templatetags/admin_tree_list.py create mode 100644 vendor/treebeard/tests/__init__.py create mode 100644 vendor/treebeard/tests/admin.py create mode 100644 vendor/treebeard/tests/conftest.py create mode 100644 vendor/treebeard/tests/jenkins/rm_workspace_coverage.py create mode 100644 vendor/treebeard/tests/jenkins/toxhelper.py create mode 100644 vendor/treebeard/tests/models.py create mode 100644 vendor/treebeard/tests/settings.py create mode 100644 vendor/treebeard/tests/test_treebeard.py create mode 100644 vendor/treebeard/tests/urls.py 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 %} + +{% 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 0000000000000000000000000000000000000000..2f9da7e11e33192808a43b35fe1387f4e62b6be0 GIT binary patch literal 1020 zcmVPx#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iglB z6gdw)b05?mS_hntQ>#-J;ctJsN-8deP;qdTqCMoa5RV zy(K8&949E~XM)mhw`an#7L@;M1VIq|S8(u4Bq2EbK18wz{t!vHD434OZ}4VL7SH^a zJ=Y6C5CnoC#AZKg3xY6jFM>aiD-eV!GD;HNe7f759R<&hAP9o6uy8J(>mGr_&y78BvHUI#_Fwn!p!zN?w6UO-K=H})@tybg9)+UyL$K$cm@Ar%Cb~~L) zrHCks?EL(EQm@y8pAr~`fh@~XZf|dk&1O@%xw%Q0rkT_6?hU-ENvOrgP3W48uULa$pz+avVpYl>SZ#VV>t@P1D?HwOXiBsqkDb z_aw`*Ln+0CkiWCp>|>Y{6o%dF^@@!~L-RZ@tLwU}s;ZAM231vkUDsXD^RkUbL+kZ= zMa#03FcQ&moXlV_C^VZ*g;I)4(@b;DQ?_j<8Dn7Eb`k)#ZQD-v`+Z?F8Y$IkwFm$M z!!RDj=jZ27olYlxcXx*X09vhD-c3>xm%P3a@M^VM>~_29 zTrT%R*L4>FwlT&jRaGZDJ3AwUP^MTc{#GiLz84CGfu?DXwOWk}(fenN>9Q=-xLfc% zk6?_4A$YEo!#WrahdYf%L+khZ3Df>)#MUPtmfT6-BL9;|QTA zS(b;TQt7)a%NzhemSsM*hl}7>rChJqgVWQ~i71Lp(=@_4mktgN=Fa@?ZnP{*p_FbD qLPl}mSb~tm_kBr{BtH%e0Prt6*uKrY@;nOw0000= 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 ? '" : ""; + e = e ? '
    ' + (c ? h : "") + (this._isInRange(a, r) ? '" : "") + (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 += '
    ' + (/all|left/.test(t) && C == 0 ? c ? + f : n : "") + (/all|right/.test(t) && C == 0 ? c ? n : f : "") + this._generateMonthYearHeader(a, g, m, j, o, C > 0 || D > 0, z, v) + '
    '; + var A = k ? '" : ""; + 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 ? "" : '"; + 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 += '"; + q.setDate(q.getDate() + 1); + q = this._daylightSavingAdjust(q) + } + x += P + "" + } + g++; + if (g > 11) { + g = 0; + m++ + } + x += "
    ' + this._get(a, "weekHeader") + "
    ' + this._get(a, "calculateWeek")(q) + "" + (B && !w ? " " : K ? '' + q.getDate() + + "" : '' + q.getDate() + "") + "
    " + (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 += '" + } + 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 += '" + } + 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') % ( + 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 ' + '' + '' + '') + 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), +]