django-treebeard-2.0b1/0000755000076500000240000000000012151313146015040 5ustar tabostaff00000000000000django-treebeard-2.0b1/CHANGES0000644000076500000240000001042612151312464016040 0ustar tabostaff00000000000000 Release 2.0b1 (May 29, 2013) ---------------------------- This is a beta release. * Added support for Django 1.5 and Python 3.X * Updated docs: the library supports python 2.5+ and Django 1.4+. Dropped support for older versions * Revamped admin interface for MP and NS trees, supporting drag&drop to reorder nodes. Work on this patch was sponsored by the `Oregon Center for Applied Science`_, inspired by `FeinCMS`_ developed by `Jesús del Carpio`_ with tests from `Fernando Gutierrez`_. Thanks ORCAS! * Updated setup.py to use distribute/setuptools instead of distutils * Now using pytest for testing * Small optimization to ns_tree.is_root * Moved treebeard.tests to it's own directory (instead of tests.py) * Added the runtests.py test runner * Added tox support * Fixed drag&drop bug in the admin * Fixed a bug when moving MP_Nodes * Using .pk instead of .id when accessing nodes. * Removed the Benchmark (tbbench) and example (tbexample) apps. * Fixed url parts join issues in the admin. * Fixed: Now installing the static resources * Fixed ManyToMany form field save handling * In the admin, the node is now saved when moving so it can trigger handlers and/or signals. * Improved translation files, including javascript. * Renamed Node.get_database_engine() to Node.get_database_vendor(). As the name implies, it returns the database vendor instead of the engine used. Treebeard will get the value from Django, but you can subclass the method if needed. Release 1.61 (Jul 24, 2010) --------------------------- * Added admin i18n. Included translations: es, ru * Fixed a bug when trying to introspect the database engine used in Django 1.2+ while using new style db settings (DATABASES). Added Node.get_database_engine to deal with this. Release 1.60 (Apr 18, 2010) --------------------------- * Added get_annotated_list * Complete revamp of the documentation. It's now divided in sections for easier reading, and the package includes .rst files instead of the html build. * Added raw id fields support in the admin * Fixed setup.py to make it work in 2.4 again * The correct ordering in NS/MP trees is now enforced in the queryset. * Cleaned up code, removed some unnecessary statements. * Tests refactoring, to make it easier to spot the model being tested. * Fixed support of trees using proxied models. It was broken due to a bug in Django. * Fixed a bug in add_child when adding nodes to a non-leaf in sorted MP. * There are now 648 unit tests. Test coverage is 96% * This will be the last version compatible with Django 1.0. There will be a a 1.6.X branch maintained for urgent bug fixes, but the main development will focus on recent Django versions. Release 1.52 (Dec 18, 2009) --------------------------- * Really fixed the installation of templates. Release 1.51 (Dec 16, 2009) --------------------------- * Forgot to include treebeard/tempates/\*.html in MANIFEST.in Release 1.5 (Dec 15, 2009) -------------------------- New features added ~~~~~~~~~~~~~~~~~~ * Forms - Added MoveNodeForm * Django Admin - Added TreeAdmin * MP_Node - Added 2 new checks in MP_Node.find_problems(): 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 - Added a new (safer and faster but less comprehensive) MP_Node.fix_tree() approach. * Documentation - Added warnings in the documentation when subclassing MP_Node or NS_Node and adding a new Meta. - HTML documentation is now included in the package. - CHANGES file and section in the docs. * Other changes: - script to build documentation - updated numconv.py Bugs fixed ~~~~~~~~~~ * Added table quoting to all the sql queries that bypass the ORM. Solves bug in postgres when the table isn't created by syncdb. * Removing unused method NS_Node._find_next_node * Fixed MP_Node.get_tree to include the given parent when given a leaf node Release 1.1 (Nov 20, 2008) -------------------------- Bugs fixed ~~~~~~~~~~ * Added exceptions.py Release 1.0 (Nov 19, 2008) -------------------------- * First public release. .. _Oregon Center for Applied Science: http://www.orcasinc.com/ .. _FeinCMS: http://www.feinheit.ch/media/labs/feincms/admin.html .. _Jesús del Carpio: http://www.isgeek.net .. _Fernando Gutierrez: http://xbito.pe django-treebeard-2.0b1/django_treebeard.egg-info/0000755000076500000240000000000012151313146022011 5ustar tabostaff00000000000000django-treebeard-2.0b1/django_treebeard.egg-info/dependency_links.txt0000644000076500000240000000000112151313140026051 0ustar tabostaff00000000000000 django-treebeard-2.0b1/django_treebeard.egg-info/PKG-INFO0000644000076500000240000000412712151313140023104 0ustar tabostaff00000000000000Metadata-Version: 1.1 Name: django-treebeard Version: 2.0b1 Summary: Efficient tree implementations for Django 1.4+ Home-page: https://tabo.pe/projects/django-treebeard/ Author: Gustavo Picon Author-email: tabo@tabo.pe License: Apache License 2.0 Description: django-treebeard ================ django-treebeard is a library that implements efficient tree implementations for the Django Web Framework 1.4+, written by Gustavo Picón and licensed under the Apache License 2.0. django-treebeard is: - **Flexible**: Includes 3 different tree implementations with the same API: 1. Adjacency List 2. Materialized Path 3. Nested Sets - **Fast**: Optimized non-naive tree operations (see Benchmarks). - **Easy**: Uses Django Model Inheritance with abstract classes to define your own models. - **Clean**: Testable and well tested code base. Code/branch test coverage is above 96%. Tests are available in Jenkins: - Test suite running on different versions of Python, Django and database engine: https://tabo.pe/jenkins/job/django-treebeard/ - Code quality: https://tabo.pe/jenkins/job/django-treebeard-quality/ You can find the documentation in https://tabo.pe/projects/django-treebeard/docs/tip/ Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Environment :: Web Environment Classifier: Framework :: Django Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.5 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: 3.3 Classifier: Operating System :: OS Independent Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Utilities django-treebeard-2.0b1/django_treebeard.egg-info/requires.txt0000644000076500000240000000001312151313140024375 0ustar tabostaff00000000000000Django>=1.4django-treebeard-2.0b1/django_treebeard.egg-info/SOURCES.txt0000644000076500000240000000260012151313146023673 0ustar tabostaff00000000000000CHANGES LICENSE MANIFEST.in NOTICE README.rst UPDATING setup.py django_treebeard.egg-info/PKG-INFO django_treebeard.egg-info/SOURCES.txt django_treebeard.egg-info/dependency_links.txt django_treebeard.egg-info/requires.txt django_treebeard.egg-info/top_level.txt docs/Makefile docs/admin.rst docs/al_tree.rst docs/api.rst docs/benchmarks.rst docs/changes.rst docs/conf.py docs/exceptions.rst docs/forms.rst docs/index.rst docs/intro.rst docs/mp_tree.rst docs/ns_tree.rst docs/tests.rst treebeard/__init__.py treebeard/admin.py treebeard/al_tree.py treebeard/exceptions.py treebeard/forms.py treebeard/models.py treebeard/mp_tree.py treebeard/ns_tree.py treebeard/numconv.py treebeard/static/treebeard/expand-collapse.png treebeard/static/treebeard/jquery-ui-1.8.5.custom.min.js treebeard/static/treebeard/treebeard-admin.css treebeard/static/treebeard/treebeard-admin.js treebeard/templates/admin/tree_change_list.html treebeard/templates/admin/tree_change_list_results.html treebeard/templates/admin/tree_list.html treebeard/templates/admin/tree_list_results.html treebeard/templatetags/__init__.py treebeard/templatetags/admin_tree.py treebeard/templatetags/admin_tree_list.py treebeard/tests/__init__.py treebeard/tests/conftest.py treebeard/tests/models.py treebeard/tests/settings.py treebeard/tests/test_treebeard.py treebeard/tests/jenkins/rm_workspace_coverage.py treebeard/tests/jenkins/toxhelper.pydjango-treebeard-2.0b1/django_treebeard.egg-info/top_level.txt0000644000076500000240000000001212151313140024526 0ustar tabostaff00000000000000treebeard django-treebeard-2.0b1/docs/0000755000076500000240000000000012151313146015770 5ustar tabostaff00000000000000django-treebeard-2.0b1/docs/admin.rst0000644000076500000240000000021112151047564017614 0ustar tabostaff00000000000000Admin ===== .. module:: treebeard.admin .. autoclass:: TreeAdmin :show-inheritance: To be used by Django's admin.site.register django-treebeard-2.0b1/docs/al_tree.rst0000644000076500000240000000574412151047554020156 0ustar tabostaff00000000000000Adjacency List trees ==================== .. module:: treebeard.al_tree This is a simple implementation of the traditional Adjacency List Model for storing trees in relational databases. In the adjacency list model, every node will have a ":attr:`~AL_Node.parent`" key, that will be NULL for root nodes. Since ``django-treebeard`` must return trees ordered in a predictable way, the ordering for models without the :attr:`~AL_Node.node_order_by` attribute will have an extra attribute that will store the relative position of a node between it's siblings: :attr:`~AL_Node.sib_order`. The adjacency list model has the advantage of fast writes at the cost of slow reads. If you read more than you write, use :class:`~treebeard.mp_tree.MP_Node` instead. .. inheritance-diagram:: AL_Node .. autoclass:: AL_Node :show-inheritance: .. attribute:: node_order_by Attribute: a list of model fields that will be used for node ordering. When enabled, all tree operations will assume this ordering. Example:: node_order_by = ['field1', 'field2', 'field3'] .. attribute:: parent ``ForeignKey`` to itself. This attribute **MUST** be defined in the subclass (sadly, this isn't inherited correctly from the ABC in `Django 1.0`). Just copy&paste these lines to your model:: parent = models.ForeignKey('self', related_name='children_set', null=True, db_index=True) .. attribute:: sib_order ``PositiveIntegerField`` used to store the relative position of a node between it's siblings. This attribute is mandatory *ONLY* if you don't set a :attr:`node_order_by` field. You can define it copy&pasting this line in your model:: sib_order = models.PositiveIntegerField() Examples:: class AL_TestNode(AL_Node): parent = models.ForeignKey('self', related_name='children_set', null=True, db_index=True) sib_order = models.PositiveIntegerField() desc = models.CharField(max_length=255) class AL_TestNodeSorted(AL_Node): parent = models.ForeignKey('self', related_name='children_set', null=True, db_index=True) node_order_by = ['val1', 'val2', 'desc'] val1 = models.IntegerField() val2 = models.IntegerField() desc = models.CharField(max_length=255) Read the API reference of :class:`treebeard.Node` for info on methods available in this class, or read the following section for methods with particular arguments or exceptions. .. automethod:: get_depth See: :meth:`treebeard.Node.get_depth` django-treebeard-2.0b1/docs/api.rst0000644000076500000240000001621112151051647017301 0ustar tabostaff00000000000000API === .. module:: treebeard.models .. inheritance-diagram:: Node .. autoclass:: Node :show-inheritance: This is the base class that defines the API of all tree models in this library: - :class:`treebeard.mp_tree.MP_Node` (materialized path) - :class:`treebeard.ns_tree.NS_Node` (nested sets) - :class:`treebeard.al_tree.AL_Node` (adjacency list) .. warning:: Please note that ``django-treebeard`` uses Django raw SQL queries for some write operations, and raw queries don't update the objects in the ORM since it's being bypassed. Because of this, if you have a node in memory and plan to use it after a tree modification (adding/removing/moving nodes), you need to reload it. .. automethod:: Node.add_root Example:: MyNode.add_root(numval=1, strval='abcd') .. automethod:: add_child Example:: node.add_child(numval=1, strval='abcd') .. automethod:: add_sibling Examples:: node.add_sibling('sorted-sibling', numval=1, strval='abc') .. automethod:: delete .. note:: Call our queryset's delete to handle children removal. Subclasses will handle extra maintenance. .. automethod:: get_tree .. automethod:: get_depth Example:: node.get_depth() .. automethod:: get_ancestors Example:: node.get_ancestors() .. automethod:: get_children Example:: node.get_children() .. automethod:: get_children_count Example:: node.get_children_count() .. automethod:: get_descendants Example:: node.get_descendants() .. automethod:: get_descendant_count Example:: node.get_descendant_count() .. automethod:: get_first_child Example:: node.get_first_child() .. automethod:: get_last_child Example:: node.get_last_child() .. automethod:: get_first_sibling Example:: node.get_first_sibling() .. automethod:: get_last_sibling Example:: node.get_last_sibling() .. automethod:: get_prev_sibling Example:: node.get_prev_sibling() .. automethod:: get_next_sibling Example:: node.get_next_sibling() .. automethod:: get_parent Example:: node.get_parent() .. automethod:: get_root Example:: node.get_root() .. automethod:: get_siblings Example:: node.get_siblings() .. automethod:: is_child_of Example:: node.is_child_of(node2) .. automethod:: is_descendant_of Example:: node.is_descendant_of(node2) .. automethod:: is_sibling_of Example:: node.is_sibling_of(node2) .. automethod:: is_root Example:: node.is_root() .. automethod:: is_leaf Example:: node.is_leaf() .. automethod:: move .. note:: The node can be moved under another root node. Examples:: node.move(node2, 'sorted-child') node.move(node2, 'prev-sibling') .. automethod:: save .. automethod:: get_first_root_node Example:: MyNodeModel.get_first_root_node() .. automethod:: get_last_root_node Example:: MyNodeModel.get_last_root_node() .. automethod:: get_root_nodes Example:: MyNodeModel.get_root_nodes() .. automethod:: load_bulk .. note:: Any internal data that you may have stored in your nodes' data (:attr:`path`, :attr:`depth`) will be ignored. .. note:: If your node model has a ForeignKey this method will try to load the related object before loading the data. If the related object doesn't exist it won't load anything and will raise a DoesNotExist exception. This is done because the dump_data method uses integers to dump related objects. .. note:: If your node model has :attr:`node_order_by` enabled, it will take precedence over the order in the structure. Example:: 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'}}, ]}, ] # parent = None MyNodeModel.load_data(data, None) Will create: .. digraph:: load_bulk_digraph "1"; "2"; "2" -> "21"; "2" -> "22"; "2" -> "23" -> "231"; "2" -> "24"; "3"; "4"; "4" -> "41"; .. automethod:: dump_bulk Example:: tree = MyNodeModel.dump_bulk() branch = MyNodeModel.dump_bulk(node_obj) .. automethod:: find_problems .. automethod:: fix_tree .. automethod:: get_descendants_group_count Example:: # get a list of the root nodes root_nodes = MyModel.get_descendants_group_count() for node in root_nodes: print '%s by %s (%d replies)' % (node.comment, node.author, node.descendants_count) .. automethod:: get_annotated_list Example:: annotated_list = get_annotated_list() With data: .. digraph:: get_annotated_list_digraph "a"; "a" -> "ab"; "ab" -> "aba"; "ab" -> "abb"; "ab" -> "abc"; "a" -> "ac"; Will return:: [ (a, {'open':True, 'close':[], 'level': 0}) (ab, {'open':True, 'close':[], 'level': 1}) (aba, {'open':True, 'close':[], 'level': 2}) (abb, {'open':False, 'close':[], 'level': 2}) (abc, {'open':False, 'close':[0,1], 'level': 2}) (ac, {'open':False, 'close':[0], 'level': 1}) ] This can be used with a template like:: {% for item, info in annotated_list %} {% if info.open %} {% endfor %} {% endfor %} .. note:: This method was contributed originally by `Alexey Kinyov `_, using an idea borrowed from `django-mptt`. .. versionadded:: 1.55 .. automethod:: get_database_vendor Example:: @classmethod def get_database_engine(cls): return "mysql" .. versionadded:: 1.61 django-treebeard-2.0b1/docs/benchmarks.rst0000644000076500000240000002612712150510123020640 0ustar tabostaff00000000000000Benchmarks ========== ``tbbench`` is a django app that isn't installed by default. I wrote it to find spots that could be optimized, and it may help you to tweak your database settings. To run the benchmarks: 1. Add ``tbbench`` to your Python path 2. Add ``'tbbench'`` to the ``INSTALLED_APPS`` section in your django settings file. 3. Run :command:`python manage.py syncdb` 4. In the ``tbbench`` dir, run :command:`python run.py` .. note:: If the `django-mptt`_ package is also installed, both libraries will be tested with the exact same data and operations. Currently, the available tests are: 1. Inserts: adds 1000 nodes to a tree, in different places: root nodes, normal nodes, leaf nodes 2. Descendants: retrieves the full branch under every node several times. 3. Move: moves nodes several times. This operation can be expensive because involves reodrering and data maintenance. 4. Delete: Removes groups of nodes. For every available library (treebeard and mptt), two models are tested: a vanilla model, and a model with a "tree order by" attribute enabled (:attr:`~treebeard.MP_Node.node_order_by` in treebeard, ``order_insertion_by`` in mptt). Also, every test will be tested with and without database transactions (``tx``). The output of the script is a reST table, with the time for every test in milliseconds (so small numbers are better). By default, these tests use the default tables created by ``syncdb``. Even when the results of ``treebeard`` are good, they can be improved *a lot* with better indexing. The Materialized Path Tree approach used by ``treebeard`` is *very* sensitive to database indexing, so you'll probably want to ``EXPLAIN`` your most common queries involving the :attr:`~treebeard.MP_Node.path` field and add proper indexes. .. note:: Tests results in Ubuntu 8.04.1 on a Thinkpad T61 with 4GB of ram. .. warning:: These results shouldn't be taken as *"X is faster than Y"*, but as *"both X and Y are very fast"*. Databases tested: - MySQL InnoDB 5.0.51a, default settings - MySQL MyISAM 5.0.51a, default settings - PostgreSQL 8.2.7, default settings, mounted on RAM - PostgreSQL 8.3.3, default settings, mounted on RAM - SQLite3, mounted on RAM +-------------+--------------+-------------------+-------------------+-------------------+-------------------+-------------------+ | Test | Model | innodb | myisam | pg82 | pg83 | sqlite | | | +---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | | | no tx | tx | no tx | tx | no tx | tx | no tx | tx | no tx | tx | +=============+==============+=========+=========+=========+=========+=========+=========+=========+=========+=========+=========+ | Inserts | TB MP | 3220 | 2660 | 3181 | 2766 | 2859 | 2542 | 2540 | 2309 | 2205 | 1934 | | +--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | | TB AL | 1963 | 1905 | 1998 | 1936 | 1937 | 1775 | 1736 | 1631 | 1583 | 1457 | | +--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | | TB NS | 3386 | 3438 | 3359 | 3420 | 4061 | 7242 | 3536 | 4401 | 2794 | 2554 | | +--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | | MPTT | 7559 | 9280 | 7525 | 9028 | 5202 | 14969 | 4764 | 6022 | 3781 | 3484 | | +--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | | TB MP Sorted | 4732 | 5627 | 5038 | 5215 | 4022 | 4808 | 3415 | 3942 | 3250 | 3045 | | +--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | | TB AL Sorted | 1096 | 1052 | 1092 | 1033 | 1239 | 999 | 1049 | 896 | 860 | 705 | | +--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | | TB NS Sorted | 6637 | 6373 | 6283 | 6313 | 7548 | 10053 | 6717 | 10941 | 5907 | 5461 | | +--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | | MPTT Sorted | 8564 | 10729 | 7947 | 10221 | 6077 | 7567 | 5490 | 6894 | 4842 | 4284 | +-------------+--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | Descendants | TB MP | 6298 | N/A | 6460 | N/A | 7643 | N/A | 7132 | N/A | 10415 | N/A | | +--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | | TB AL | 56850 | N/A | 116550 | N/A | 54249 | N/A | 50682 | N/A | 50521 | N/A | | +--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | | TB NS | 5595 | N/A | 5824 | N/A | 10080 | N/A | 5840 | N/A | 5965 | N/A | | +--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | | MPTT | 5268 | N/A | 5306 | N/A | 9394 | N/A | 8745 | N/A | 5197 | N/A | | +--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | | TB MP Sorted | 6698 | N/A | 6408 | N/A | 8248 | N/A | 7265 | N/A | 10513 | N/A | | +--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | | TB AL Sorted | 59817 | N/A | 59718 | N/A | 56767 | N/A | 52574 | N/A | 53458 | N/A | | +--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | | TB NS Sorted | 5631 | N/A | 5858 | N/A | 9980 | N/A | 9210 | N/A | 6026 | N/A | | +--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | | MPTT Sorted | 5186 | N/A | 5453 | N/A | 9723 | N/A | 8912 | N/A | 5333 | N/A | +-------------+--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | Move | TB MP | 837 | 1156 | 992 | 1211 | 745 | 1040 | 603 | 740 | 497 | 468 | | +--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | | TB AL | 8708 | 8684 | 9798 | 8890 | 7243 | 7213 | 6721 | 6757 | 7051 | 6863 | | +--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | | TB NS | 683 | 658 | 660 | 679 | 1266 | 2000 | 650 | 907 | 672 | 637 | | +--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | | MPTT | 6449 | 7793 | 6356 | 7003 | 4993 | 20743 | 4445 | 8977 | 921 | 896 | | +--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | | TB MP Sorted | 6730 | 7036 | 6743 | 7023 | 6410 | 19294 | 3622 | 12380 | 2622 | 2487 | | +--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | | TB AL Sorted | 3866 | 3731 | 3873 | 3717 | 3587 | 3599 | 3394 | 3371 | 3491 | 3416 | | +--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | | TB NS Sorted | 2017 | 2017 | 1958 | 2078 | 4397 | 7981 | 3892 | 8110 | 1543 | 1496 | | +--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | | MPTT Sorted | 6563 | 10540 | 6427 | 9358 | 5132 | 20426 | 4601 | 9428 | 957 | 955 | +-------------+--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | Delete | TB MP | 714 | 651 | 733 | 686 | 699 | 689 | 595 | 561 | 636 | 557 | | +--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | | TB AL | 975 | 1093 | 2199 | 991 | 758 | 847 | 714 | 804 | 843 | 921 | | +--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | | TB NS | 745 | 745 | 742 | 763 | 555 | 698 | 430 | 506 | 530 | 513 | | +--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | | MPTT | 2928 | 4473 | 2914 | 4814 | 69385 | 167777 | 18186 | 26270 | 1617 | 1635 | | +--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | | TB MP Sorted | 811 | 751 | 808 | 737 | 798 | 1180 | 648 | 1101 | 612 | 565 | | +--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | | TB AL Sorted | 1030 | 1030 | 1055 | 987 | 797 | 1023 | 760 | 969 | 884 | 859 | | +--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | | TB NS Sorted | 756 | 750 | 728 | 758 | 807 | 847 | 576 | 748 | 501 | 490 | | +--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ | | MPTT Sorted | 3729 | 5108 | 3833 | 4776 | 86545 | 148596 | 34059 | 127125 | 2024 | 1787 | +-------------+--------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ .. _`django-mptt`: http://code.google.com/p/django-mptt/ django-treebeard-2.0b1/docs/changes.rst0000644000076500000240000000005512150510123020123 0ustar tabostaff00000000000000Changelog ========= .. include:: ../CHANGES django-treebeard-2.0b1/docs/conf.py0000644000076500000240000000160012151310452017261 0ustar tabostaff00000000000000# -*- coding: utf-8 -*- """ Configuration for the Sphinx documentation generator. Reference: http://sphinx.pocoo.org/config.html """ import os import sys sys.path.insert(0, os.path.abspath('..')) os.environ['DJANGO_SETTINGS_MODULE'] = 'treebeard.tests.settings' extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinx.ext.graphviz', 'sphinx.ext.inheritance_diagram', 'sphinx.ext.todo'] templates_path = ['_templates'] source_suffix = '.rst' master_doc = 'index' project = 'django-treebeard' copyright = '2008-2013, Gustavo Picon' version = '2.0b1' release = '2.0b1' exclude_trees = ['_build'] pygments_style = 'sphinx' html_theme = 'default' html_static_path = ['_static'] htmlhelp_basename = 'django-treebearddoc' latex_documents = [( 'index', 'django-treebeard.tex', 'django-treebeard Documentation', 'Gustavo Picon', 'manual')] django-treebeard-2.0b1/docs/exceptions.rst0000644000076500000240000000032012151047545020705 0ustar tabostaff00000000000000Exceptions ========== .. module:: treebeard.exceptions .. autoexception:: InvalidPosition .. autoexception:: InvalidMoveToDescendant .. autoexception:: PathOverflow .. autoexception:: MissingNodeOrderBy django-treebeard-2.0b1/docs/forms.rst0000644000076500000240000000042512151047537017661 0ustar tabostaff00000000000000Forms ===== .. module:: treebeard.forms .. autoclass:: MoveNodeForm :show-inheritance: Read the `Django Form objects documentation`_ for reference. .. _`Django Form objects documentation`: http://docs.djangoproject.com/en/dev/topics/forms/#form-objects django-treebeard-2.0b1/docs/index.rst0000644000076500000240000000276212150510123017631 0ustar tabostaff00000000000000django-treebeard ================ `django-treebeard `_ is a library that implements efficient tree implementations for the `Django Web Framework 1.4+ `_, written by `Gustavo Picón `_ and licensed under the Apache License 2.0. ``django-treebeard`` is: - **Flexible**: Includes 3 different tree implementations with the same API: 1. :doc:`Adjacency List ` 2. :doc:`Materialized Path ` 3. :doc:`Nested Sets ` - **Fast**: Optimized non-naive tree operations (see :doc:`benchmarks`). - **Easy**: Uses `Django Model Inheritance with abstract classes`_ to define your own models. - **Clean**: Testable and well tested code base. Code/branch test coverage is above 96%. Tests are available in Jenkins: - `Tests running on different versions of Python, Django and DB engines`_ - `Code Quality`_ Contents -------- .. toctree:: :maxdepth: 2 intro api mp_tree ns_tree al_tree admin forms exceptions benchmarks tests changes .. _`Django Model Inheritance with abstract classes`: https://docs.djangoproject.com/en/1.4/topics/db/models/#abstract-base-classes .. _`Tests running on different versions of Python, Django and DB engines`: https://tabo.pe/jenkins/job/django-treebeard/ .. _`Code Quality`: https://tabo.pe/jenkins/job/django-treebeard-quality/ Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` django-treebeard-2.0b1/docs/intro.rst0000644000076500000240000001211012150510123017641 0ustar tabostaff00000000000000Introduction ============ Everything you need to get working quickly. Prerequisites ------------- ``django-treebeard`` needs at least **Python 2.5** to run, and **Django 1.4 or better**. Installation ------------ You have several ways to install ``django-treebeard``. If you're not sure, `just use pip `_ pip (or easy_install) ~~~~~~~~~~~~~~~~~~~~~ You can install the release versions from `django-treebeard's PyPI page`_ using ``pip``:: pip install django-treebeard or if for some reason you can't use ``pip``, you can try ``easy_install``, (at your own risk):: easy_install --always-unzip django-treebeard setup.py ~~~~~~~~ Download a release from the `treebeard download page`_ and unpack it, then run:: python setup.py install .deb packages ~~~~~~~~~~~~~ Both Debian and Ubuntu include ``django-treebeard`` as a package, so you can just use:: apt-get install python-django-treebeard or:: aptitude install python-django-treebeard Remember that the packages included in linux distributions are usually not the most recent versions. Configuration ------------- Add ``'treebeard'`` to the `INSTALLED_APPS`_ section in your django settings file. .. note:: If you are going to use the :class:`Treeadmin ` class, you need to add the path to treebeard's templates in `TEMPLATE_DIRS`_. Also you need to enable `django.core.context_processors.request`_ in the `TEMPLATE_CONTEXT_PROCESSORS`_ setting in your django settings file. Basic Usage ----------- Create a basic model for your tree. In this example we'll use a Materialized Path tree:: from django.db import models from treebeard.mp_tree import MP_Node class Category(MP_Node): name = models.CharField(max_length=30) node_order_by = ['name'] def __unicode__(self): return 'Category: %s' % self.name Run syncdb:: python manage.py syncdb Let's create some nodes:: >>> get = lambda node_id: Category.objects.get(pk=node_id) >>> root = Category.add_root(name='Computer Hardware') >>> node = get(root.id).add_child(name='Memory') >>> get(node.id).add_sibling(name='Hard Drives') >>> get(node.id).add_sibling(name='SSD') >>> get(node.id).add_child(name='Desktop Memory') >>> get(node.id).add_child(name='Laptop Memory') >>> get(node.id).add_child(name='Server Memory') .. note:: Why retrieving every node again after the first operation? Because ``django-treebeard`` uses raw queries for most write operations, and raw queries don't update the django objects of the db entries they modify. We just created this tree: .. digraph:: introduction_digraph "Computer Hardware"; "Computer Hardware" -> "Hard Drives"; "Computer Hardware" -> "Memory"; "Memory" -> "Desktop Memory"; "Memory" -> "Laptop Memory"; "Memory" -> "Server Memory"; "Computer Hardware" -> "SSD"; You can see the tree structure with code:: >>> Category.dump_bulk() [{'id': 1, 'data': {'name': u'Computer Hardware'}, 'children': [ {'id': 3, 'data': {'name': u'Hard Drives'}}, {'id': 2, 'data': {'name': u'Memory'}, 'children': [ {'id': 5, 'data': {'name': u'Desktop Memory'}}, {'id': 6, 'data': {'name': u'Laptop Memory'}}, {'id': 7, 'data': {'name': u'Server Memory'}}]}, {'id': 4, 'data': {'name': u'SSD'}}]}] >>> Category.get_annotated_list() [(, {'close': [], 'level': 0, 'open': True}), (, {'close': [], 'level': 1, 'open': True}), (, {'close': [], 'level': 1, 'open': False}), (, {'close': [], 'level': 2, 'open': True}), (, {'close': [], 'level': 2, 'open': False}), (, {'close': [0], 'level': 2, 'open': False}), (, {'close': [0, 1], 'level': 1, 'open': False})] Read the :class:`treebeard.models.Node` API reference for detailed info. .. _`django-treebeard's PyPI page`: http://pypi.python.org/pypi/django-treebeard .. _`treebeard download page`: https://tabo.pe/projects/django-treebeard/download/ .. _`treebeard mercurial repository`: http://code.tabo.pe/django-treebeard .. _`latest treebeard version from PyPi`: http://pypi.python.org/pypi/django-treebeard/ .. _`django.core.context_processors.request`: http://docs.djangoproject.com/en/dev/ref/templates/api/#django-core-context-processors-request .. _`INSTALLED_APPS`: http://docs.djangoproject.com/en/dev/ref/settings/#installed-apps .. _`TEMPLATE_DIRS`: http://docs.djangoproject.com/en/dev/ref/settings/#template-dirs .. _`TEMPLATE_CONTEXT_PROCESSORS`: http://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors django-treebeard-2.0b1/docs/Makefile0000644000076500000240000000640712150510123017430 0ustar tabostaff00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage Coverage" clean: -rm -rf $(BUILDDIR)/* html: mkdir -p _static $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-treebeard.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-treebeard.qhc" latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Coverage, " \ "results in $(BUILDDIR)/coverage" django-treebeard-2.0b1/docs/mp_tree.rst0000644000076500000240000001766012151050631020164 0ustar tabostaff00000000000000Materialized Path trees ======================= .. module:: treebeard.mp_tree This is an efficient implementation of Materialized Path trees for Django 1.4+, as described by `Vadim Tropashko`_ in `SQL Design Patterns`_. Materialized Path is probably the fastest way of working with trees in SQL without the need of extra work in the database, like Oracle's ``CONNECT BY`` or sprocs and triggers for nested intervals. In a materialized path approach, every node in the tree will have a :attr:`~MP_Node.path` attribute, where the full path from the root to the node will be stored. This has the advantage of needing very simple and fast queries, at the risk of inconsistency because of the denormalization of ``parent``/``child`` foreign keys. This can be prevented with transactions. ``django-treebeard`` uses a particular approach: every step in the path has a fixed width and has no separators. This makes queries predictable and faster at the cost of using more characters to store a step. To address this problem, every step number is encoded. Also, two extra fields are stored in every node: :attr:`~MP_Node.depth` and :attr:`~MP_Node.numchild`. This makes the read operations faster, at the cost of a little more maintenance on tree updates/inserts/deletes. Don't worry, even with these extra steps, materialized path is more efficient than other approaches. .. note:: The materialized path approach makes heavy use of ``LIKE`` in your database, with clauses like ``WHERE path LIKE '002003%'``. If you think that ``LIKE`` is too slow, you're right, but in this case the :attr:`~MP_Node.path` field is indexed in the database, and all ``LIKE`` clauses that don't **start** with a ``%`` character will use the index. This is what makes the materialized path approach so fast. .. _`Vadim Tropashko`: http://vadimtropashko.wordpress.com/ .. _`Sql Design Patterns`: http://www.rampant-books.com/book_2006_1_sql_coding_styles.htm .. inheritance-diagram:: MP_Node .. autoclass:: MP_Node :show-inheritance: .. warning:: Do not change the values of :attr:`path`, :attr:`depth` or :attr:`numchild` directly: use one of the included methods instead. Consider these values *read-only*. .. warning:: Do not change the values of the :attr:`steplen`, :attr:`alphabet` or :attr:`node_order_by` after saving your first model. Doing so will corrupt the tree. If you *must* do it: 1. Backup the tree with :meth:`dump_bulk` 2. Empty your model's table 3. Change :attr:`depth`, :attr:`alphabet` and/or :attr:`node_order_by` in your model 4. Restore your backup using :meth:`load_bulk` with ``keep_ids=True`` to keep the same primary keys you had. Example:: class SortedNode(MP_Node): node_order_by = ['numval', 'strval'] numval = models.IntegerField() strval = models.CharField(max_length=255) Read the API reference of :class:`treebeard.Node` for info on methods available in this class, or read the following section for methods with particular arguments or exceptions. .. attribute:: steplen Attribute that defines the length of each step in the :attr:`path` of a node. The default value of *4* allows a maximum of *1679615* children per node. Increase this value if you plan to store large trees (a ``steplen`` of *5* allows more than *60M* children per node). Note that increasing this value, while increasing the number of children per node, will decrease the max :attr:`depth` of the tree (by default: *63*). To increase the max :attr:`depth`, increase the max_length attribute of the :attr:`path` field in your model. .. attribute:: alphabet Attribute: the alphabet that will be used in base conversions when encoding the path steps into strings. The default value, ``0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ`` is the most optimal possible value that is portable between the supported databases (which means: their default collation will order the :attr:`path` field correctly). .. note:: In case you know what you are doing, there is a test that is disabled by default that can tell you the optimal default alphabet in your enviroment. To run the test you must enable the :envvar:`TREEBEARD_TEST_ALPHABET` enviroment variable:: $ TREEBEARD_TEST_ALPHABET=1 python manage.py test treebeard.TestTreeAlphabet On my Mountain Lion system, these are the optimal values for the three supported databases in their *default* configuration: ================ ================ Database Optimal Alphabet ================ ================ MySQL 5.6.10 0-9A-Z PostgreSQL 9.2.4 0-9A-Z Sqlite3 0-9A-Z ================ ================ .. attribute:: node_order_by Attribute: a list of model fields that will be used for node ordering. When enabled, all tree operations will assume this ordering. Example:: node_order_by = ['field1', 'field2', 'field3'] .. attribute:: path ``CharField``, stores the full materialized path for each node. The default value of it's max_length, *255*, is the max efficient and portable value for a ``varchar``. Increase it to allow deeper trees (max depth by default: *63*) .. note:: `django-treebeard` uses Django's abstract model inheritance, so: 1. To change the max_length value of the path in your model, you can't just define it since you'd get a django exception, you have to modify the already defined attribute:: class MyNodeModel(MP_Node): pass MyNodeModel._meta.get_field('path').max_length = 1024 2. You can't rely on Django's `auto_now` properties in date fields for sorting, you'll have to manually set the value before creating a node:: class TestNodeSortedAutoNow(MP_Node): desc = models.CharField(max_length=255) created = models.DateTimeField(auto_now_add=True) node_order_by = ['created'] TestNodeSortedAutoNow.add_root(desc='foo', created=datetime.datetime.now()) .. note:: For performance, and if your database allows it, you can safely define the path column as ASCII (not utf-8/unicode/iso8859-1/etc) to keep the index smaller (and faster). Also note that some databases (mysql) have a small index size limit. InnoDB for instance has a limit of 765 bytes per index, so that would be the limit if your path is ASCII encoded. If your path column in InnoDB is using unicode, the index limit will be 255 characters since in MySQL's indexes, unicode means 3 bytes. .. note:: treebeard uses **numconv** for path encoding: https://tabo.pe/projects/numconv/ .. attribute:: depth ``PositiveIntegerField``, depth of a node in the tree. A root node has a depth of *1*. .. attribute:: numchild ``PositiveIntegerField``, the number of children of the node. .. automethod:: add_root See: :meth:`treebeard.Node.add_root` .. automethod:: add_child See: :meth:`treebeard.Node.add_child` .. automethod:: add_sibling See: :meth:`treebeard.Node.add_sibling` .. automethod:: move See: :meth:`treebeard.Node.move` .. automethod:: get_tree See: :meth:`treebeard.Node.get_tree` .. note:: This metod returns a queryset. .. automethod:: find_problems .. note:: A node won't appear in more than one list, even when it exhibits more than one problem. This method stops checking a node when it finds a problem and continues to the next node. .. note:: Problems 1, 2 and 3 can't be solved automatically. Example:: MyNodeModel.find_problems() .. automethod:: fix_tree Example:: MyNodeModel.fix_tree() django-treebeard-2.0b1/docs/ns_tree.rst0000644000076500000240000000227212151047526020172 0ustar tabostaff00000000000000Nested Sets trees ================= .. module:: treebeard.ns_tree An implementation of Nested Sets trees for Django 1.4+, as described by `Joe Celko`_ in `Trees and Hierarchies in SQL for Smarties`_. Nested sets have very efficient reads at the cost of high maintenance on write/delete operations. .. _`Joe Celko`: http://en.wikipedia.org/wiki/Joe_Celko .. _`Trees and Hierarchies in SQL for Smarties`: http://www.elsevier.com/wps/product/cws_home/702605 .. inheritance-diagram:: NS_Node .. autoclass:: NS_Node :show-inheritance: .. attribute:: node_order_by Attribute: a list of model fields that will be used for node ordering. When enabled, all tree operations will assume this ordering. Example:: node_order_by = ['field1', 'field2', 'field3'] .. attribute:: depth ``PositiveIntegerField``, depth of a node in the tree. A root node has a depth of *1*. .. attribute:: lft ``PositiveIntegerField`` .. attribute:: rgt ``PositiveIntegerField`` .. attribute:: tree_id ``PositiveIntegerField`` .. automethod:: get_tree See: :meth:`treebeard.Node.get_tree` .. note:: This metod returns a queryset. django-treebeard-2.0b1/docs/tests.rst0000644000076500000240000000364212150510123017662 0ustar tabostaff00000000000000Running the Test Suite ====================== ``django-treebeard`` includes a comprehensive test suite. It is highly recommended that you run and update the test suite when you send patches. py.test ------- You will need `pytest`_ to run the test suite. To run the test suite:: $ py.test You can use all the features and plugins of pytest this way. By default the test suite will run using a sqlite3 database in RAM, but you can change this setting environment variables: .. option:: DATABASE_ENGINE .. option:: DATABASE_NAME .. option:: DATABASE_USER .. option:: DATABASE_PASSWORD .. option:: DATABASE_HOST .. option:: DATABASE_PORT Sets the database settings to be used by the test suite. Useful if you want to test the same database engine/version you use in production. tox --- ``django-treebeard`` uses `tox`_ to run the test suite in all the supported environments: - py24-dj12-sqlite - py24-dj12-mysql - py24-dj12-postgres - py24-dj13-sqlite - py24-dj13-mysql - py24-dj13-postgres - py25-dj12-sqlite - py25-dj12-mysql - py25-dj12-postgres - py25-dj13-sqlite - py25-dj13-mysql - py25-dj13-postgres - py26-dj12-sqlite - py26-dj12-mysql - py26-dj12-postgres - py26-dj13-sqlite - py26-dj13-mysql - py26-dj13-postgres - py27-dj12-sqlite - py27-dj12-mysql - py27-dj12-postgres - py27-dj13-sqlite - py27-dj13-mysql - py27-dj13-postgres This means that the test suite will run 24 times to test every environment supported by ``django-treebeard``. This takes a long time. If you want to test only one or a few environments, please use the `-e` option in `tox`_, like:: $ tox -e py27-dj13-postgres .. _verbosity level: .. _pytest: http://pytest.org/ http://docs.djangoproject.com/en/dev/ref/django-admin/#django-admin-option---verbosity .. _coverage: http://nedbatchelder.com/code/coverage/ .. _tox: http://codespeak.net/tox/ django-treebeard-2.0b1/LICENSE0000644000076500000240000002613712150510123016047 0ustar tabostaff00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. django-treebeard-2.0b1/MANIFEST.in0000644000076500000240000000023512151306605016600 0ustar tabostaff00000000000000include CHANGES LICENSE NOTICE README.rst UPDATING MANIFEST.in recursive-include docs Makefile README.rst *.py *.rst recursive-include treebeard *.py *.html django-treebeard-2.0b1/NOTICE0000644000076500000240000000000012150510123015723 0ustar tabostaff00000000000000django-treebeard-2.0b1/PKG-INFO0000644000076500000240000000412712151313146016141 0ustar tabostaff00000000000000Metadata-Version: 1.1 Name: django-treebeard Version: 2.0b1 Summary: Efficient tree implementations for Django 1.4+ Home-page: https://tabo.pe/projects/django-treebeard/ Author: Gustavo Picon Author-email: tabo@tabo.pe License: Apache License 2.0 Description: django-treebeard ================ django-treebeard is a library that implements efficient tree implementations for the Django Web Framework 1.4+, written by Gustavo Picón and licensed under the Apache License 2.0. django-treebeard is: - **Flexible**: Includes 3 different tree implementations with the same API: 1. Adjacency List 2. Materialized Path 3. Nested Sets - **Fast**: Optimized non-naive tree operations (see Benchmarks). - **Easy**: Uses Django Model Inheritance with abstract classes to define your own models. - **Clean**: Testable and well tested code base. Code/branch test coverage is above 96%. Tests are available in Jenkins: - Test suite running on different versions of Python, Django and database engine: https://tabo.pe/jenkins/job/django-treebeard/ - Code quality: https://tabo.pe/jenkins/job/django-treebeard-quality/ You can find the documentation in https://tabo.pe/projects/django-treebeard/docs/tip/ Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Environment :: Web Environment Classifier: Framework :: Django Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.5 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: 3.3 Classifier: Operating System :: OS Independent Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Utilities django-treebeard-2.0b1/README.rst0000644000076500000240000000170012151311067016525 0ustar tabostaff00000000000000 django-treebeard ================ django-treebeard is a library that implements efficient tree implementations for the Django Web Framework 1.4+, written by Gustavo Picón and licensed under the Apache License 2.0. django-treebeard is: - **Flexible**: Includes 3 different tree implementations with the same API: 1. Adjacency List 2. Materialized Path 3. Nested Sets - **Fast**: Optimized non-naive tree operations (see Benchmarks). - **Easy**: Uses Django Model Inheritance with abstract classes to define your own models. - **Clean**: Testable and well tested code base. Code/branch test coverage is above 96%. Tests are available in Jenkins: - Test suite running on different versions of Python, Django and database engine: https://tabo.pe/jenkins/job/django-treebeard/ - Code quality: https://tabo.pe/jenkins/job/django-treebeard-quality/ You can find the documentation in https://tabo.pe/projects/django-treebeard/docs/tip/ django-treebeard-2.0b1/setup.cfg0000644000076500000240000000007312151313146016661 0ustar tabostaff00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 django-treebeard-2.0b1/setup.py0000644000076500000240000000337512151311076016562 0ustar tabostaff00000000000000#!/usr/bin/env python import os from setuptools import setup from setuptools.command.test import test def root_dir(): rd = os.path.dirname(__file__) if rd: return rd return '.' class pytest_test(test): def finalize_options(self): test.finalize_options(self) self.test_args = [] self.test_suite = True def run_tests(self): import pytest pytest.main([]) setup_args = dict( name='django-treebeard', version='2.0b1', url='https://tabo.pe/projects/django-treebeard/', author='Gustavo Picon', author_email='tabo@tabo.pe', license='Apache License 2.0', packages=['treebeard', 'treebeard.templatetags'], package_dir={'treebeard': 'treebeard'}, package_data={ 'treebeard': ['templates/admin/*.html', 'static/treebeard/*']}, description='Efficient tree implementations for Django 1.4+', long_description=open(root_dir() + '/README.rst').read(), cmdclass={'test': pytest_test}, install_requires=['Django>=1.4'], tests_require=['pytest'], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', 'Environment :: Web Environment', 'Framework :: Django', 'Programming Language :: Python', 'Programming Language :: Python :: 2.5', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Operating System :: OS Independent', 'Topic :: Software Development :: Libraries', 'Topic :: Utilities']) if __name__ == '__main__': setup(**setup_args) django-treebeard-2.0b1/treebeard/0000755000076500000240000000000012151313146016775 5ustar tabostaff00000000000000django-treebeard-2.0b1/treebeard/__init__.py0000644000076500000240000000002612151310464021104 0ustar tabostaff00000000000000__version__ = '2.0b1' django-treebeard-2.0b1/treebeard/admin.py0000644000076500000240000001002312150510123020424 0ustar tabostaff00000000000000"""Django admin support for treebeard""" import sys from django.conf.urls import patterns, url from django.contrib import admin, messages from django.http import HttpResponse, HttpResponseBadRequest from django.utils.translation import ugettext_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 class TreeAdmin(admin.ModelAdmin): """Django Admin class for treebeard""" change_list_template = 'admin/tree_change_list.html' def 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 .queryset cause we will use # the old admin return super(TreeAdmin, self).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' 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() new_urls = patterns( '', url('^move/$', self.admin_site.admin_view(self.move_node), ), url(r'^jsi18n/$', 'django.views.i18n.javascript_catalog', {'packages': ('treebeard',)}), ) return new_urls + urls def get_node(self, node_id): return self.model.objects.get(pk=node_id) 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 = node.node_order_by is not None pos = { (True, True): 'sorted-child', (True, False): 'last-child', (False, True): 'sorted-sibling', (False, False): 'left', }[as_child, is_sorted] 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 # 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 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)) django-treebeard-2.0b1/treebeard/al_tree.py0000644000076500000240000002530212150510123020755 0ustar tabostaff00000000000000"""Adjacency List""" from django.core import serializers from django.db import connection, models, transaction from django.utils.translation import ugettext_noop as _ from treebeard.exceptions import InvalidMoveToDescendant from treebeard.models import Node class AL_NodeManager(models.Manager): """Custom manager for nodes.""" def get_query_set(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_query_set().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.""" newobj = cls(**kwargs) newobj._cached_depth = 1 if not cls.node_order_by: try: max = 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() transaction.commit_unless_managed() return newobj @classmethod def get_root_nodes(cls): """:returns: A queryset containing the root nodes in the tree.""" return 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 self.__class__.objects.filter(parent=self) def get_parent(self, update=False): """:returns: the parent node of the current node object.""" 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 = [] 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.""" newobj = self.__class__(**kwargs) try: newobj._cached_depth = self._cached_depth + 1 except AttributeError: pass if not self.__class__.node_order_by: try: max = self.__class__.objects.filter(parent=self).reverse( )[0].sib_order except IndexError: max = 0 newobj.sib_order = max + 1 newobj.parent = self newobj.save() transaction.commit_unless_managed() 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 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) newobj = 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() transaction.commit_unless_managed() 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 = 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() transaction.commit_unless_managed() class Meta: """Abstract model.""" abstract = True django-treebeard-2.0b1/treebeard/exceptions.py0000644000076500000240000000116512150510123021524 0ustar tabostaff00000000000000"""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 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) """ django-treebeard-2.0b1/treebeard/forms.py0000644000076500000240000001654412150510123020500 0ustar tabostaff00000000000000"""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.safestring import mark_safe from django.utils.translation import ugettext_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. """ __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): opts = self._meta if instance is None: if opts.model is None: raise ValueError('MoveNodeForm has no model class specified.') else: opts.model = type(instance) 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 if instance is None: # if we didn't get an instance, instantiate a new one instance = opts.model() object_data = {} choices_for_node = None else: object_data = model_to_dict(instance, opts.fields, opts.exclude) object_data.update(self._get_position_ref_node(instance)) choices_for_node = instance choices = self.mk_dropdown_tree(opts.model, for_node=choices_for_node) self.declared_fields['_ref_node_id'].choices = choices self.instance = instance # if initial was provided, it should override the values from instance if initial is not None: object_data.update(initial) super(BaseModelForm, self).__init__(data, files, auto_id, prefix, object_data, error_class, label_suffix, empty_permitted) 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()) + str(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 model: :return: A MoveNodeForm subclass Example of a generated class:: class AL_TestNodeForm(MoveNodeForm): class Meta: model = models.AL_TestNode exclude = ('sib_order', 'parent') """ _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 django-treebeard-2.0b1/treebeard/models.py0000644000076500000240000005023412151051647020643 0ustar tabostaff00000000000000"""Models and base API""" import sys import operator if sys.version_info >= (3, 0): from functools import reduce 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 :returns: the created node object. It will be save()d by this method. """ 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': 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 analize 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 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: 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] ]) transaction.commit_unless_managed() 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 :returns: The created node object. It will be save()d by this method. """ 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 :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 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 de 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(id=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(cls, parent=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. """ result, info = [], {} start_depth, prev_depth = (None, None) for node in cls.get_tree(parent): 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_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): if cls._db_connection is None: cls._db_connection = { 'read': connections[router.db_for_read(cls)], 'write': connections[router.db_for_write(cls)] } return cls._db_connection[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 django-treebeard-2.0b1/treebeard/mp_tree.py0000644000076500000240000011032212150701650021001 0ustar tabostaff00000000000000"""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 ugettext_noop as _ from treebeard.numconv import NumConv from treebeard.models import Node from treebeard.exceptions import InvalidMoveToDescendant, PathOverflow class MP_NodeQuerySet(models.query.QuerySet): """ Custom queryset for the tree node manager. Needed only for the customized 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 not node.is_leaf(): toremove.append(Q(path__startswith=node.path)) else: toremove.append(Q(path=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 = self.model.objects.filter(reduce(operator.or_, toremove)) super(MP_NodeQuerySet, qset).delete() transaction.commit_unless_managed() class MP_NodeManager(models.Manager): """Custom manager for nodes.""" def get_query_set(self): """Sets the custom queryset as the default.""" return MP_NodeQuerySet(self.model).order_by('path') 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) 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. :raise PathOverflow: when no more root objects can be added """ # 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 newpath = cls._inc_path(last_root.path) else: # adding the first root node newpath = cls._get_path(None, 1, 1) # creating the new object newobj = cls(**kwargs) newobj.depth = 1 newobj.path = newpath # saving the instance before returning it newobj.save() transaction.commit_unless_managed() return newobj @classmethod def dump_bulk(cls, parent=None, keep_ids=True): """Dumps a tree branch to a python data structure.""" # 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 """ 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=['LENGTH(path)/%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 attemped. 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). """ 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=LENGTH(path)/%%s "\ "WHERE depth!=LENGTH(path)/%%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 "\ "CONCAT(tbn1.path, %%s)) 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 tbn1.path||%%s)" 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) transaction.commit_unless_managed() @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. """ if parent is None: # return the entire tree return cls.objects.all() if not parent.is_leaf(): return cls.objects.filter(path__startswith=parent.path, depth__gte=parent.depth) return cls.objects.filter(pk=parent.pk) @classmethod def get_root_nodes(cls): """:returns: A queryset containing the root nodes in the tree.""" return cls.objects.filter(depth=1) @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. #~ 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 = '' sql = 'SELECT * FROM %(table)s AS t1 INNER JOIN '\ ' (SELECT '\ ' SUBSTR(path, 1, %(subpathlen)s) 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) transaction.commit_unless_managed() 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 = self.__class__.objects.filter(depth=self.depth) 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 self.__class__.objects.none() return self.__class__.objects.filter( depth=self.depth + 1, path__range=self._get_children_path_interval(self.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. :raise PathOverflow: when no more child nodes can be added """ if not self.is_leaf() and self.node_order_by: # there are child nodes and node_order_by has been set # delegate sorted insertion to add_sibling self.numchild += 1 return self.get_last_child().add_sibling('sorted-sibling', **kwargs) # creating a new object newobj = self.__class__(**kwargs) newobj.depth = self.depth + 1 if not self.is_leaf(): # adding the new child as the last one newobj.path = self._inc_path(self.get_last_child().path) else: # the node had no children, adding the first child newobj.path = self._get_path(self.path, newobj.depth, 1) max_length = newobj.__class__._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')) # saving the instance before returning it newobj.save() newobj._cached_parent_obj = self self.__class__.objects.filter(path=self.path).update(numchild=F( 'numchild')+1) # we increase the numchild value of the object in memory self.numchild += 1 transaction.commit_unless_managed() return newobj def add_sibling(self, pos=None, **kwargs): """ Adds a new node as a sibling to the current node object. :raise PathOverflow: when the library can't make room for the node's new position """ pos = self._prepare_pos_var_for_add_sibling(pos) # creating a new object newobj = self.__class__(**kwargs) newobj.depth = self.depth if pos == 'sorted-sibling': siblings = self.get_sorted_pos_queryset( self.get_siblings(), newobj) try: newpos = self._get_lastpos_in_path(siblings.all()[0].path) except IndexError: newpos = None if newpos is None: pos = 'last-sibling' else: newpos, siblings = None, [] stmts = [] _, newpath = self._move_add_sibling_aux(pos, newpos, self.depth, self, siblings, stmts, None, False) parentpath = self._get_basepath(newpath, self.depth - 1) if parentpath: stmts.append(self._get_sql_update_numchild(parentpath, 'inc')) cursor = self._get_database_cursor('write') for sql, vals in stmts: cursor.execute(sql, vals) # saving the instance before returning it newobj.path = newpath newobj.save() transaction.commit_unless_managed() return newobj def get_root(self): """:returns: the root node for the current node object.""" return self.__class__.objects.get(path=self.path[0:self.steplen]) 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. """ paths = [self.path[0:pos] for pos in range(0, len(self.path), self.steplen)[1:]] return 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 = 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 """ pos = self._prepare_pos_var_for_move(pos) oldpath = self.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) (pos, target, newdepth, siblings, newpos) = ( self._fix_move_to_child(pos, target) ) if target.is_descendant_of(self): raise InvalidMoveToDescendant( _("Can't move node to a descendant.")) if oldpath == target.path and ( (pos == 'left') or (pos in ('right', 'last-sibling') and target.path == target.get_last_sibling().path) or (pos == 'first-sibling' and target.path == target.get_first_sibling().path)): # special cases, not actually moving the node so no need to UPDATE return if pos == 'sorted-sibling': siblings = self.get_sorted_pos_queryset( target.get_siblings(), self) try: newpos = self._get_lastpos_in_path(siblings.all()[0].path) except IndexError: newpos = None if newpos is None: pos = 'last-sibling' stmts = [] # generate the sql that will do the actual moving of nodes oldpath, newpath = self._move_add_sibling_aux(pos, newpos, newdepth, target, siblings, stmts, oldpath, True) # updates needed for mysql and children count in parents self._updates_after_move(oldpath, newpath, stmts) cursor = self._get_database_cursor('write') for sql, vals in stmts: cursor.execute(sql, vals) transaction.commit_unless_managed() @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 '%s%s%s' % (parentpath, '0' * (cls.steplen - len(key)), key) @classmethod def _inc_path(cls, path): """:returns: The path of the next sibling of a given node path.""" newpos = cls._str2int(path[-cls.steplen:]) + 1 key = cls._int2str(newpos) if len(key) > cls.steplen: raise PathOverflow(_("Path Overflow from: '%s'" % (path, ))) return '%s%s%s' % (path[:-cls.steplen], '0' * (cls.steplen - len(key)), key) @classmethod def _get_lastpos_in_path(cls, path): """:returns: The integer value of the last step in a path.""" return cls._str2int(path[-cls.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) @classmethod def _move_add_sibling_aux(cls, pos, newpos, newdepth, target, siblings, stmts, 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 = cls._inc_path(last.path) if movebranch: stmts.append(cls._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 = cls._get_lastpos_in_path(target.path) newpos = {'first-sibling': 1, 'left': basenum, 'right': basenum + 1}[pos] newpath = 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 = cls._get_basepath( oldpath, int(len(oldpath) / cls.steplen) - 1 ) parentnewpath = cls._get_basepath(newpath, newdepth - 1) if ( parentoldpath == parentnewpath and siblings and newpath < oldpath ): last = target.get_last_sibling() basenum = cls._get_lastpos_in_path(last.path) tempnewpath = cls._get_path(newpath, newdepth, basenum + 2) stmts.append(cls._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 = cls._inc_path(node.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 = cls._get_sql_newpath_in_branches(node.path, cls._inc_path( node.path)) 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: stmts.append(cls._get_sql_newpath_in_branches(tempnewpath, newpath)) else: stmts.append(cls._get_sql_newpath_in_branches(oldpath, newpath)) return oldpath, newpath def _fix_move_to_child(self, pos, target): """Update preliminar vars in :meth:`move` when moving to a child""" newdepth = target.depth newpos = None siblings = [] if pos in ('first-child', 'last-child', 'sorted-child'): # moving to a child parent = target newdepth += 1 if target.is_leaf(): # moving as a target's first child newpos = 1 pos = 'first-sibling' siblings = self.__class__.objects.none() else: target = target.get_last_child() pos = {'first-child': 'first-sibling', 'last-child': 'last-sibling', 'sorted-child': 'sorted-sibling'}[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 pos, target, newdepth, siblings, newpos @classmethod def _updates_after_move(cls, oldpath, newpath, stmts): """ 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 ( 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 stmts.append(cls._get_sql_update_depth_in_branch(newpath)) oldparentpath = cls._get_parent_path_from_path(oldpath) newparentpath = 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: stmts.append(cls._get_sql_update_numchild(oldparentpath, 'dec')) if newparentpath: stmts.append(cls._get_sql_update_numchild(newparentpath, 'inc')) @classmethod def _get_sql_newpath_in_branches(cls, 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 = cls.get_database_vendor('write') sql1 = "UPDATE %s SET" % ( connection.ops.quote_name(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 = "%s||SUBSTR(path, %s)" 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=LENGTH(%s)/%%s" % (sqlpath, )) vals.extend([newpath, len(oldpath) + 1, cls.steplen]) sql3 = "WHERE path LIKE %s" vals.extend([oldpath + '%']) sql = '%s %s %s' % (sql1, ', '.join(sql2), sql3) return sql, vals @classmethod def _get_sql_update_depth_in_branch(cls, path): """ :returns: The sql needed to update the depth of all the nodes in a branch. """ # Right now this is only used by *sigh* mysql. sql = "UPDATE %s SET depth=LENGTH(path)/%%s"\ " WHERE path LIKE %%s" % ( connection.ops.quote_name(cls._meta.db_table), ) vals = [cls.steplen, path + '%'] return sql, vals @classmethod def _get_sql_update_numchild(cls, 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(cls._meta.db_table), {'inc': '+', 'dec': '-'}[incdec]) vals = [path] return sql, vals class Meta: """Abstract model.""" abstract = True django-treebeard-2.0b1/treebeard/ns_tree.py0000644000076500000240000005332712151051647021025 0ustar tabostaff00000000000000"""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 ugettext_noop as _ from treebeard.exceptions import InvalidMoveToDescendant from treebeard.models import Node 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`` """ 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 = self.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 = self.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: self.model.objects.filter( reduce(operator.or_, toremove) ).delete(removed_ranges=ranges) transaction.commit_unless_managed() class NS_NodeManager(models.Manager): """ Custom manager for nodes. """ def get_query_set(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 # creating the new object newobj = cls(**kwargs) newobj.depth = 1 newobj.tree_id = newtree_id newobj.lft = 1 newobj.rgt = 2 # saving the instance before returning it newobj.save() transaction.commit_unless_managed() 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(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(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) # creating a new object newobj = 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() transaction.commit_unless_managed() 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) # creating a new object newobj = 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() transaction.commit_unless_managed() 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 = 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) transaction.commit_unless_managed() @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(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.""" # 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] ]) transaction.commit_unless_managed() 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 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. """ 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 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 self.__class__.objects.none() return 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 cls.objects.filter(lft=1) class Meta: """Abstract model.""" abstract = True django-treebeard-2.0b1/treebeard/numconv.py0000644000076500000240000000751312150510123021033 0ustar tabostaff00000000000000"""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) django-treebeard-2.0b1/treebeard/static/0000755000076500000240000000000012151313146020264 5ustar tabostaff00000000000000django-treebeard-2.0b1/treebeard/static/treebeard/0000755000076500000240000000000012151313146022221 5ustar tabostaff00000000000000django-treebeard-2.0b1/treebeard/static/treebeard/expand-collapse.png0000644000076500000240000000177412150510123026010 0ustar tabostaff00000000000000PNG  IHDR@}sRGBbKGD pHYs  tIME 9=s|IDATXUnH.76t{6$keF@2!Qwq\2]p%%qwp di/dU_踺4}a2@8E$IH$c<___/$IxDI(ofN= A f?j 0o/?+)2J|ۜώ p.VR+ĕJ60&cCV&v;J(|R׃,RnoofSI4$IvvvE ƘvӞ雈n1s7 @r`rGNaeMhZR\.$˲C)ńcFCW\׃Ӊ(L49n,KnBFqX}I$? /;ؾͼ< IENDB`django-treebeard-2.0b1/treebeard/static/treebeard/jquery-ui-1.8.5.custom.min.js0000644000076500000240000026437312150510123027263 0ustar tabostaff00000000000000/*! * jQuery UI 1.8.5 * * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) * Dual licensed under the MIT or GPL Version 2 licenses. * http://jquery.org/license * * http://docs.jquery.com/UI */ (function (c, j) { function k(a) { return!c(a).parents().andSelf().filter(function () { return c.curCSS(this, "visibility") === "hidden" || c.expr.filters.hidden(this) }).length } c.ui = c.ui || {}; if (!c.ui.version) { c.extend(c.ui, {version: "1.8.5", keyCode: {ALT: 18, BACKSPACE: 8, CAPS_LOCK: 20, COMMA: 188, COMMAND: 91, COMMAND_LEFT: 91, COMMAND_RIGHT: 93, CONTROL: 17, DELETE: 46, DOWN: 40, END: 35, ENTER: 13, ESCAPE: 27, HOME: 36, INSERT: 45, LEFT: 37, MENU: 93, NUMPAD_ADD: 107, NUMPAD_DECIMAL: 110, NUMPAD_DIVIDE: 111, NUMPAD_ENTER: 108, NUMPAD_MULTIPLY: 106, NUMPAD_SUBTRACT: 109, PAGE_DOWN: 34, PAGE_UP: 33, PERIOD: 190, RIGHT: 39, SHIFT: 16, SPACE: 32, TAB: 9, UP: 38, WINDOWS: 91}}); c.fn.extend({_focus: c.fn.focus, focus: function (a, b) { return typeof a === "number" ? this.each(function () { var d = this; setTimeout(function () { c(d).focus(); b && b.call(d) }, a) }) : this._focus.apply(this, arguments) }, scrollParent: function () { var a; a = c.browser.msie && /(static|relative)/.test(this.css("position")) || /absolute/.test(this.css("position")) ? this.parents().filter(function () { return/(relative|absolute|fixed)/.test(c.curCSS(this, "position", 1)) && /(auto|scroll)/.test(c.curCSS(this, "overflow", 1) + c.curCSS(this, "overflow-y", 1) + c.curCSS(this, "overflow-x", 1)) }).eq(0) : this.parents().filter(function () { return/(auto|scroll)/.test(c.curCSS(this, "overflow", 1) + c.curCSS(this, "overflow-y", 1) + c.curCSS(this, "overflow-x", 1)) }).eq(0); return/fixed/.test(this.css("position")) || !a.length ? c(document) : a }, zIndex: function (a) { if (a !== j)return this.css("zIndex", a); if (this.length) { a = c(this[0]); for (var b; a.length && a[0] !== document;) { b = a.css("position"); if (b === "absolute" || b === "relative" || b === "fixed") { b = parseInt(a.css("zIndex")); if (!isNaN(b) && b != 0)return b } a = a.parent() } } return 0 }, disableSelection: function () { return this.bind("mousedown.ui-disableSelection selectstart.ui-disableSelection", function (a) { a.preventDefault() }) }, enableSelection: function () { return this.unbind(".ui-disableSelection") }}); c.each(["Width", "Height"], function (a, b) { function d(f, g, l, m) { c.each(e, function () { g -= parseFloat(c.curCSS(f, "padding" + this, true)) || 0; if (l)g -= parseFloat(c.curCSS(f, "border" + this + "Width", true)) || 0; if (m)g -= parseFloat(c.curCSS(f, "margin" + this, true)) || 0 }); return g } var e = b === "Width" ? ["Left", "Right"] : ["Top", "Bottom"], h = b.toLowerCase(), i = {innerWidth: c.fn.innerWidth, innerHeight: c.fn.innerHeight, outerWidth: c.fn.outerWidth, outerHeight: c.fn.outerHeight}; c.fn["inner" + b] = function (f) { if (f === j)return i["inner" + b].call(this); return this.each(function () { c.style(this, h, d(this, f) + "px") }) }; c.fn["outer" + b] = function (f, g) { if (typeof f !== "number")return i["outer" + b].call(this, f); return this.each(function () { c.style(this, h, d(this, f, true, g) + "px") }) } }); c.extend(c.expr[":"], {data: function (a, b, d) { return!!c.data(a, d[3]) }, focusable: function (a) { var b = a.nodeName.toLowerCase(), d = c.attr(a, "tabindex"); if ("area" === b) { b = a.parentNode; d = b.name; if (!a.href || !d || b.nodeName.toLowerCase() !== "map")return false; a = c("img[usemap=#" + d + "]")[0]; return!!a && k(a) } return(/input|select|textarea|button|object/.test(b) ? !a.disabled : "a" == b ? a.href || !isNaN(d) : !isNaN(d)) && k(a) }, tabbable: function (a) { var b = c.attr(a, "tabindex"); return(isNaN(b) || b >= 0) && c(a).is(":focusable") }}); c(function () { var a = document.createElement("div"), b = document.body; c.extend(a.style, {minHeight: "100px", height: "auto", padding: 0, borderWidth: 0}); c.support.minHeight = b.appendChild(a).offsetHeight === 100; b.removeChild(a).style.display = "none" }); c.extend(c.ui, {plugin: {add: function (a, b, d) { a = c.ui[a].prototype; for (var e in d) { a.plugins[e] = a.plugins[e] || []; a.plugins[e].push([b, d[e]]) } }, call: function (a, b, d) { if ((b = a.plugins[b]) && a.element[0].parentNode)for (var e = 0; e < b.length; e++)a.options[b[e][0]] && b[e][1].apply(a.element, d) }}, contains: function (a, b) { return document.compareDocumentPosition ? a.compareDocumentPosition(b) & 16 : a !== b && a.contains(b) }, hasScroll: function (a, b) { if (c(a).css("overflow") === "hidden")return false; b = b && b === "left" ? "scrollLeft" : "scrollTop"; var d = false; if (a[b] > 0)return true; a[b] = 1; d = a[b] > 0; a[b] = 0; return d }, isOverAxis: function (a, b, d) { return a > b && a < b + d }, isOver: function (a, b, d, e, h, i) { return c.ui.isOverAxis(a, d, h) && c.ui.isOverAxis(b, e, i) }}) } })(jQuery); ; /*! * jQuery UI Widget 1.8.5 * * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) * Dual licensed under the MIT or GPL Version 2 licenses. * http://jquery.org/license * * http://docs.jquery.com/UI/Widget */ (function (b, j) { if (b.cleanData) { var k = b.cleanData; b.cleanData = function (a) { for (var c = 0, d; (d = a[c]) != null; c++)b(d).triggerHandler("remove"); k(a) } } else { var l = b.fn.remove; b.fn.remove = function (a, c) { return this.each(function () { if (!c)if (!a || b.filter(a, [this]).length)b("*", this).add([this]).each(function () { b(this).triggerHandler("remove") }); return l.call(b(this), a, c) }) } } b.widget = function (a, c, d) { var e = a.split(".")[0], f; a = a.split(".")[1]; f = e + "-" + a; if (!d) { d = c; c = b.Widget } b.expr[":"][f] = function (h) { return!!b.data(h, a) }; b[e] = b[e] || {}; b[e][a] = function (h, g) { arguments.length && this._createWidget(h, g) }; c = new c; c.options = b.extend(true, {}, c.options); b[e][a].prototype = b.extend(true, c, {namespace: e, widgetName: a, widgetEventPrefix: b[e][a].prototype.widgetEventPrefix || a, widgetBaseClass: f}, d); b.widget.bridge(a, b[e][a]) }; b.widget.bridge = function (a, c) { b.fn[a] = function (d) { var e = typeof d === "string", f = Array.prototype.slice.call(arguments, 1), h = this; d = !e && f.length ? b.extend.apply(null, [true, d].concat(f)) : d; if (e && d.substring(0, 1) === "_")return h; e ? this.each(function () { var g = b.data(this, a); if (!g)throw"cannot call methods on " + a + " prior to initialization; attempted to call method '" + d + "'"; if (!b.isFunction(g[d]))throw"no such method '" + d + "' for " + a + " widget instance"; var i = g[d].apply(g, f); if (i !== g && i !== j) { h = i; return false } }) : this.each(function () { var g = b.data(this, a); g ? g.option(d || {})._init() : b.data(this, a, new c(d, this)) }); return h } }; b.Widget = function (a, c) { arguments.length && this._createWidget(a, c) }; b.Widget.prototype = {widgetName: "widget", widgetEventPrefix: "", options: {disabled: false}, _createWidget: function (a, c) { b.data(c, this.widgetName, this); this.element = b(c); this.options = b.extend(true, {}, this.options, b.metadata && b.metadata.get(c)[this.widgetName], a); var d = this; this.element.bind("remove." + this.widgetName, function () { d.destroy() }); this._create(); this._init() }, _create: function () { }, _init: function () { }, destroy: function () { this.element.unbind("." + this.widgetName).removeData(this.widgetName); this.widget().unbind("." + this.widgetName).removeAttr("aria-disabled").removeClass(this.widgetBaseClass + "-disabled ui-state-disabled") }, widget: function () { return this.element }, option: function (a, c) { var d = a, e = this; if (arguments.length === 0)return b.extend({}, e.options); if (typeof a === "string") { if (c === j)return this.options[a]; d = {}; d[a] = c } b.each(d, function (f, h) { e._setOption(f, h) }); return e }, _setOption: function (a, c) { this.options[a] = c; if (a === "disabled")this.widget()[c ? "addClass" : "removeClass"](this.widgetBaseClass + "-disabled ui-state-disabled").attr("aria-disabled", c); return this }, enable: function () { return this._setOption("disabled", false) }, disable: function () { return this._setOption("disabled", true) }, _trigger: function (a, c, d) { var e = this.options[a]; c = b.Event(c); c.type = (a === this.widgetEventPrefix ? a : this.widgetEventPrefix + a).toLowerCase(); d = d || {}; if (c.originalEvent) { a = b.event.props.length; for (var f; a;) { f = b.event.props[--a]; c[f] = c.originalEvent[f] } } this.element.trigger(c, d); return!(b.isFunction(e) && e.call(this.element[0], c, d) === false || c.isDefaultPrevented()) }} })(jQuery); ; /*! * jQuery UI Mouse 1.8.5 * * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) * Dual licensed under the MIT or GPL Version 2 licenses. * http://jquery.org/license * * http://docs.jquery.com/UI/Mouse * * Depends: * jquery.ui.widget.js */ (function (c) { c.widget("ui.mouse", {options: {cancel: ":input,option", distance: 1, delay: 0}, _mouseInit: function () { var a = this; this.element.bind("mousedown." + this.widgetName,function (b) { return a._mouseDown(b) }).bind("click." + this.widgetName, function (b) { if (a._preventClickEvent) { a._preventClickEvent = false; b.stopImmediatePropagation(); return false } }); this.started = false }, _mouseDestroy: function () { this.element.unbind("." + this.widgetName) }, _mouseDown: function (a) { a.originalEvent = a.originalEvent || {}; if (!a.originalEvent.mouseHandled) { this._mouseStarted && this._mouseUp(a); this._mouseDownEvent = a; var b = this, e = a.which == 1, f = typeof this.options.cancel == "string" ? c(a.target).parents().add(a.target).filter(this.options.cancel).length : false; if (!e || f || !this._mouseCapture(a))return true; this.mouseDelayMet = !this.options.delay; if (!this.mouseDelayMet)this._mouseDelayTimer = setTimeout(function () { b.mouseDelayMet = true }, this.options.delay); if (this._mouseDistanceMet(a) && this._mouseDelayMet(a)) { this._mouseStarted = this._mouseStart(a) !== false; if (!this._mouseStarted) { a.preventDefault(); return true } } this._mouseMoveDelegate = function (d) { return b._mouseMove(d) }; this._mouseUpDelegate = function (d) { return b._mouseUp(d) }; c(document).bind("mousemove." + this.widgetName, this._mouseMoveDelegate).bind("mouseup." + this.widgetName, this._mouseUpDelegate); c.browser.safari || a.preventDefault(); return a.originalEvent.mouseHandled = true } }, _mouseMove: function (a) { if (c.browser.msie && !a.button)return this._mouseUp(a); if (this._mouseStarted) { this._mouseDrag(a); return a.preventDefault() } if (this._mouseDistanceMet(a) && this._mouseDelayMet(a))(this._mouseStarted = this._mouseStart(this._mouseDownEvent, a) !== false) ? this._mouseDrag(a) : this._mouseUp(a); return!this._mouseStarted }, _mouseUp: function (a) { c(document).unbind("mousemove." + this.widgetName, this._mouseMoveDelegate).unbind("mouseup." + this.widgetName, this._mouseUpDelegate); if (this._mouseStarted) { this._mouseStarted = false; this._preventClickEvent = a.target == this._mouseDownEvent.target; this._mouseStop(a) } return false }, _mouseDistanceMet: function (a) { return Math.max(Math.abs(this._mouseDownEvent.pageX - a.pageX), Math.abs(this._mouseDownEvent.pageY - a.pageY)) >= this.options.distance }, _mouseDelayMet: function () { return this.mouseDelayMet }, _mouseStart: function () { }, _mouseDrag: function () { }, _mouseStop: function () { }, _mouseCapture: function () { return true }}) })(jQuery); ; /* * jQuery UI Position 1.8.5 * * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) * Dual licensed under the MIT or GPL Version 2 licenses. * http://jquery.org/license * * http://docs.jquery.com/UI/Position */ (function (c) { c.ui = c.ui || {}; var n = /left|center|right/, o = /top|center|bottom/, t = c.fn.position, u = c.fn.offset; c.fn.position = function (b) { if (!b || !b.of)return t.apply(this, arguments); b = c.extend({}, b); var a = c(b.of), d = a[0], g = (b.collision || "flip").split(" "), e = b.offset ? b.offset.split(" ") : [0, 0], h, k, j; if (d.nodeType === 9) { h = a.width(); k = a.height(); j = {top: 0, left: 0} } else if (d.scrollTo && d.document) { h = a.width(); k = a.height(); j = {top: a.scrollTop(), left: a.scrollLeft()} } else if (d.preventDefault) { b.at = "left top"; h = k = 0; j = {top: b.of.pageY, left: b.of.pageX} } else { h = a.outerWidth(); k = a.outerHeight(); j = a.offset() } c.each(["my", "at"], function () { var f = (b[this] || "").split(" "); if (f.length === 1)f = n.test(f[0]) ? f.concat(["center"]) : o.test(f[0]) ? ["center"].concat(f) : ["center", "center"]; f[0] = n.test(f[0]) ? f[0] : "center"; f[1] = o.test(f[1]) ? f[1] : "center"; b[this] = f }); if (g.length === 1)g[1] = g[0]; e[0] = parseInt(e[0], 10) || 0; if (e.length === 1)e[1] = e[0]; e[1] = parseInt(e[1], 10) || 0; if (b.at[0] === "right")j.left += h; else if (b.at[0] === "center")j.left += h / 2; if (b.at[1] === "bottom")j.top += k; else if (b.at[1] === "center")j.top += k / 2; j.left += e[0]; j.top += e[1]; return this.each(function () { var f = c(this), l = f.outerWidth(), m = f.outerHeight(), p = parseInt(c.curCSS(this, "marginLeft", true)) || 0, q = parseInt(c.curCSS(this, "marginTop", true)) || 0, v = l + p + parseInt(c.curCSS(this, "marginRight", true)) || 0, w = m + q + parseInt(c.curCSS(this, "marginBottom", true)) || 0, i = c.extend({}, j), r; if (b.my[0] === "right")i.left -= l; else if (b.my[0] === "center")i.left -= l / 2; if (b.my[1] === "bottom")i.top -= m; else if (b.my[1] === "center")i.top -= m / 2; i.left = parseInt(i.left); i.top = parseInt(i.top); r = {left: i.left - p, top: i.top - q}; c.each(["left", "top"], function (s, x) { c.ui.position[g[s]] && c.ui.position[g[s]][x](i, {targetWidth: h, targetHeight: k, elemWidth: l, elemHeight: m, collisionPosition: r, collisionWidth: v, collisionHeight: w, offset: e, my: b.my, at: b.at}) }); c.fn.bgiframe && f.bgiframe(); f.offset(c.extend(i, {using: b.using})) }) }; c.ui.position = {fit: {left: function (b, a) { var d = c(window); d = a.collisionPosition.left + a.collisionWidth - d.width() - d.scrollLeft(); b.left = d > 0 ? b.left - d : Math.max(b.left - a.collisionPosition.left, b.left) }, top: function (b, a) { var d = c(window); d = a.collisionPosition.top + a.collisionHeight - d.height() - d.scrollTop(); b.top = d > 0 ? b.top - d : Math.max(b.top - a.collisionPosition.top, b.top) }}, flip: {left: function (b, a) { if (a.at[0] !== "center") { var d = c(window); d = a.collisionPosition.left + a.collisionWidth - d.width() - d.scrollLeft(); var g = a.my[0] === "left" ? -a.elemWidth : a.my[0] === "right" ? a.elemWidth : 0, e = a.at[0] === "left" ? a.targetWidth : -a.targetWidth, h = -2 * a.offset[0]; b.left += a.collisionPosition.left < 0 ? g + e + h : d > 0 ? g + e + h : 0 } }, top: function (b, a) { if (a.at[1] !== "center") { var d = c(window); d = a.collisionPosition.top + a.collisionHeight - d.height() - d.scrollTop(); var g = a.my[1] === "top" ? -a.elemHeight : a.my[1] === "bottom" ? a.elemHeight : 0, e = a.at[1] === "top" ? a.targetHeight : -a.targetHeight, h = -2 * a.offset[1]; b.top += a.collisionPosition.top < 0 ? g + e + h : d > 0 ? g + e + h : 0 } }}}; if (!c.offset.setOffset) { c.offset.setOffset = function (b, a) { if (/static/.test(c.curCSS(b, "position")))b.style.position = "relative"; var d = c(b), g = d.offset(), e = parseInt(c.curCSS(b, "top", true), 10) || 0, h = parseInt(c.curCSS(b, "left", true), 10) || 0; g = {top: a.top - g.top + e, left: a.left - g.left + h}; "using"in a ? a.using.call(b, g) : d.css(g) }; c.fn.offset = function (b) { var a = this[0]; if (!a || !a.ownerDocument)return null; if (b)return this.each(function () { c.offset.setOffset(this, b) }); return u.call(this) } } })(jQuery); ; /* * jQuery UI Slider 1.8.5 * * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) * Dual licensed under the MIT or GPL Version 2 licenses. * http://jquery.org/license * * http://docs.jquery.com/UI/Slider * * Depends: * jquery.ui.core.js * jquery.ui.mouse.js * jquery.ui.widget.js */ (function (d) { d.widget("ui.slider", d.ui.mouse, {widgetEventPrefix: "slide", options: {animate: false, distance: 0, max: 100, min: 0, orientation: "horizontal", range: false, step: 1, value: 0, values: null}, _create: function () { var a = this, b = this.options; this._mouseSliding = this._keySliding = false; this._animateOff = true; this._handleIndex = null; this._detectOrientation(); this._mouseInit(); this.element.addClass("ui-slider ui-slider-" + this.orientation + " ui-widget ui-widget-content ui-corner-all"); b.disabled && this.element.addClass("ui-slider-disabled ui-disabled"); this.range = d([]); if (b.range) { if (b.range === true) { this.range = d("
"); if (!b.values)b.values = [this._valueMin(), this._valueMin()]; if (b.values.length && b.values.length !== 2)b.values = [b.values[0], b.values[0]] } else this.range = d("
"); this.range.appendTo(this.element).addClass("ui-slider-range"); if (b.range === "min" || b.range === "max")this.range.addClass("ui-slider-range-" + b.range); this.range.addClass("ui-widget-header") } d(".ui-slider-handle", this.element).length === 0 && d("").appendTo(this.element).addClass("ui-slider-handle"); if (b.values && b.values.length)for (; d(".ui-slider-handle", this.element).length < b.values.length;)d("").appendTo(this.element).addClass("ui-slider-handle"); this.handles = d(".ui-slider-handle", this.element).addClass("ui-state-default ui-corner-all"); this.handle = this.handles.eq(0); this.handles.add(this.range).filter("a").click(function (c) { c.preventDefault() }).hover(function () { b.disabled || d(this).addClass("ui-state-hover") },function () { d(this).removeClass("ui-state-hover") }).focus(function () { if (b.disabled)d(this).blur(); else { d(".ui-slider .ui-state-focus").removeClass("ui-state-focus"); d(this).addClass("ui-state-focus") } }).blur(function () { d(this).removeClass("ui-state-focus") }); this.handles.each(function (c) { d(this).data("index.ui-slider-handle", c) }); this.handles.keydown(function (c) { var e = true, f = d(this).data("index.ui-slider-handle"), h, g, i; if (!a.options.disabled) { switch (c.keyCode) { case d.ui.keyCode.HOME: case d.ui.keyCode.END: case d.ui.keyCode.PAGE_UP: case d.ui.keyCode.PAGE_DOWN: case d.ui.keyCode.UP: case d.ui.keyCode.RIGHT: case d.ui.keyCode.DOWN: case d.ui.keyCode.LEFT: e = false; if (!a._keySliding) { a._keySliding = true; d(this).addClass("ui-state-active"); h = a._start(c, f); if (h === false)return } break } i = a.options.step; h = a.options.values && a.options.values.length ? (g = a.values(f)) : (g = a.value()); switch (c.keyCode) { case d.ui.keyCode.HOME: g = a._valueMin(); break; case d.ui.keyCode.END: g = a._valueMax(); break; case d.ui.keyCode.PAGE_UP: g = a._trimAlignValue(h + (a._valueMax() - a._valueMin()) / 5); break; case d.ui.keyCode.PAGE_DOWN: g = a._trimAlignValue(h - (a._valueMax() - a._valueMin()) / 5); break; case d.ui.keyCode.UP: case d.ui.keyCode.RIGHT: if (h === a._valueMax())return; g = a._trimAlignValue(h + i); break; case d.ui.keyCode.DOWN: case d.ui.keyCode.LEFT: if (h === a._valueMin())return; g = a._trimAlignValue(h - i); break } a._slide(c, f, g); return e } }).keyup(function (c) { var e = d(this).data("index.ui-slider-handle"); if (a._keySliding) { a._keySliding = false; a._stop(c, e); a._change(c, e); d(this).removeClass("ui-state-active") } }); this._refreshValue(); this._animateOff = false }, destroy: function () { this.handles.remove(); this.range.remove(); this.element.removeClass("ui-slider ui-slider-horizontal ui-slider-vertical ui-slider-disabled ui-widget ui-widget-content ui-corner-all").removeData("slider").unbind(".slider"); this._mouseDestroy(); return this }, _mouseCapture: function (a) { var b = this.options, c, e, f, h, g; if (b.disabled)return false; this.elementSize = {width: this.element.outerWidth(), height: this.element.outerHeight()}; this.elementOffset = this.element.offset(); c = this._normValueFromMouse({x: a.pageX, y: a.pageY}); e = this._valueMax() - this._valueMin() + 1; h = this; this.handles.each(function (i) { var j = Math.abs(c - h.values(i)); if (e > j) { e = j; f = d(this); g = i } }); if (b.range === true && this.values(1) === b.min) { g += 1; f = d(this.handles[g]) } if (this._start(a, g) === false)return false; this._mouseSliding = true; h._handleIndex = g; f.addClass("ui-state-active").focus(); b = f.offset(); this._clickOffset = !d(a.target).parents().andSelf().is(".ui-slider-handle") ? {left: 0, top: 0} : {left: a.pageX - b.left - f.width() / 2, top: a.pageY - b.top - f.height() / 2 - (parseInt(f.css("borderTopWidth"), 10) || 0) - (parseInt(f.css("borderBottomWidth"), 10) || 0) + (parseInt(f.css("marginTop"), 10) || 0)}; this._slide(a, g, c); return this._animateOff = true }, _mouseStart: function () { return true }, _mouseDrag: function (a) { var b = this._normValueFromMouse({x: a.pageX, y: a.pageY}); this._slide(a, this._handleIndex, b); return false }, _mouseStop: function (a) { this.handles.removeClass("ui-state-active"); this._mouseSliding = false; this._stop(a, this._handleIndex); this._change(a, this._handleIndex); this._clickOffset = this._handleIndex = null; return this._animateOff = false }, _detectOrientation: function () { this.orientation = this.options.orientation === "vertical" ? "vertical" : "horizontal" }, _normValueFromMouse: function (a) { var b; if (this.orientation === "horizontal") { b = this.elementSize.width; a = a.x - this.elementOffset.left - (this._clickOffset ? this._clickOffset.left : 0) } else { b = this.elementSize.height; a = a.y - this.elementOffset.top - (this._clickOffset ? this._clickOffset.top : 0) } b = a / b; if (b > 1)b = 1; if (b < 0)b = 0; if (this.orientation === "vertical")b = 1 - b; a = this._valueMax() - this._valueMin(); return this._trimAlignValue(this._valueMin() + b * a) }, _start: function (a, b) { var c = {handle: this.handles[b], value: this.value()}; if (this.options.values && this.options.values.length) { c.value = this.values(b); c.values = this.values() } return this._trigger("start", a, c) }, _slide: function (a, b, c) { var e; if (this.options.values && this.options.values.length) { e = this.values(b ? 0 : 1); if (this.options.values.length === 2 && this.options.range === true && (b === 0 && c > e || b === 1 && c < e))c = e; if (c !== this.values(b)) { e = this.values(); e[b] = c; a = this._trigger("slide", a, {handle: this.handles[b], value: c, values: e}); this.values(b ? 0 : 1); a !== false && this.values(b, c, true) } } else if (c !== this.value()) { a = this._trigger("slide", a, {handle: this.handles[b], value: c}); a !== false && this.value(c) } }, _stop: function (a, b) { var c = {handle: this.handles[b], value: this.value()}; if (this.options.values && this.options.values.length) { c.value = this.values(b); c.values = this.values() } this._trigger("stop", a, c) }, _change: function (a, b) { if (!this._keySliding && !this._mouseSliding) { var c = {handle: this.handles[b], value: this.value()}; if (this.options.values && this.options.values.length) { c.value = this.values(b); c.values = this.values() } this._trigger("change", a, c) } }, value: function (a) { if (arguments.length) { this.options.value = this._trimAlignValue(a); this._refreshValue(); this._change(null, 0) } return this._value() }, values: function (a, b) { var c, e, f; if (arguments.length > 1) { this.options.values[a] = this._trimAlignValue(b); this._refreshValue(); this._change(null, a) } if (arguments.length)if (d.isArray(arguments[0])) { c = this.options.values; e = arguments[0]; for (f = 0; f < c.length; f += 1) { c[f] = this._trimAlignValue(e[f]); this._change(null, f) } this._refreshValue() } else return this.options.values && this.options.values.length ? this._values(a) : this.value(); else return this._values() }, _setOption: function (a, b) { var c, e = 0; if (d.isArray(this.options.values))e = this.options.values.length; d.Widget.prototype._setOption.apply(this, arguments); switch (a) { case "disabled": if (b) { this.handles.filter(".ui-state-focus").blur(); this.handles.removeClass("ui-state-hover"); this.handles.attr("disabled", "disabled"); this.element.addClass("ui-disabled") } else { this.handles.removeAttr("disabled"); this.element.removeClass("ui-disabled") } break; case "orientation": this._detectOrientation(); this.element.removeClass("ui-slider-horizontal ui-slider-vertical").addClass("ui-slider-" + this.orientation); this._refreshValue(); break; case "value": this._animateOff = true; this._refreshValue(); this._change(null, 0); this._animateOff = false; break; case "values": this._animateOff = true; this._refreshValue(); for (c = 0; c < e; c += 1)this._change(null, c); this._animateOff = false; break } }, _value: function () { var a = this.options.value; return a = this._trimAlignValue(a) }, _values: function (a) { var b, c; if (arguments.length) { b = this.options.values[a]; return b = this._trimAlignValue(b) } else { b = this.options.values.slice(); for (c = 0; c < b.length; c += 1)b[c] = this._trimAlignValue(b[c]); return b } }, _trimAlignValue: function (a) { if (a < this._valueMin())return this._valueMin(); if (a > this._valueMax())return this._valueMax(); var b = this.options.step > 0 ? this.options.step : 1, c = a % b; a = a - c; if (Math.abs(c) * 2 >= b)a += c > 0 ? b : -b; return parseFloat(a.toFixed(5)) }, _valueMin: function () { return this.options.min }, _valueMax: function () { return this.options.max }, _refreshValue: function () { var a = this.options.range, b = this.options, c = this, e = !this._animateOff ? b.animate : false, f, h = {}, g, i, j, l; if (this.options.values && this.options.values.length)this.handles.each(function (k) { f = (c.values(k) - c._valueMin()) / (c._valueMax() - c._valueMin()) * 100; h[c.orientation === "horizontal" ? "left" : "bottom"] = f + "%"; d(this).stop(1, 1)[e ? "animate" : "css"](h, b.animate); if (c.options.range === true)if (c.orientation === "horizontal") { if (k === 0)c.range.stop(1, 1)[e ? "animate" : "css"]({left: f + "%"}, b.animate); if (k === 1)c.range[e ? "animate" : "css"]({width: f - g + "%"}, {queue: false, duration: b.animate}) } else { if (k === 0)c.range.stop(1, 1)[e ? "animate" : "css"]({bottom: f + "%"}, b.animate); if (k === 1)c.range[e ? "animate" : "css"]({height: f - g + "%"}, {queue: false, duration: b.animate}) } g = f }); else { i = this.value(); j = this._valueMin(); l = this._valueMax(); f = l !== j ? (i - j) / (l - j) * 100 : 0; h[c.orientation === "horizontal" ? "left" : "bottom"] = f + "%"; this.handle.stop(1, 1)[e ? "animate" : "css"](h, b.animate); if (a === "min" && this.orientation === "horizontal")this.range.stop(1, 1)[e ? "animate" : "css"]({width: f + "%"}, b.animate); if (a === "max" && this.orientation === "horizontal")this.range[e ? "animate" : "css"]({width: 100 - f + "%"}, {queue: false, duration: b.animate}); if (a === "min" && this.orientation === "vertical")this.range.stop(1, 1)[e ? "animate" : "css"]({height: f + "%"}, b.animate); if (a === "max" && this.orientation === "vertical")this.range[e ? "animate" : "css"]({height: 100 - f + "%"}, {queue: false, duration: b.animate}) } }}); d.extend(d.ui.slider, {version: "1.8.5"}) })(jQuery); ; /* * jQuery UI Datepicker 1.8.5 * * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) * Dual licensed under the MIT or GPL Version 2 licenses. * http://jquery.org/license * * http://docs.jquery.com/UI/Datepicker * * Depends: * jquery.ui.core.js */ (function (d, G) { function L() { this.debug = false; this._curInst = null; this._keyEvent = false; this._disabledInputs = []; this._inDialog = this._datepickerShowing = false; this._mainDivId = "ui-datepicker-div"; this._inlineClass = "ui-datepicker-inline"; this._appendClass = "ui-datepicker-append"; this._triggerClass = "ui-datepicker-trigger"; this._dialogClass = "ui-datepicker-dialog"; this._disableClass = "ui-datepicker-disabled"; this._unselectableClass = "ui-datepicker-unselectable"; this._currentClass = "ui-datepicker-current-day"; this._dayOverClass = "ui-datepicker-days-cell-over"; this.regional = []; this.regional[""] = {closeText: "Done", prevText: "Prev", nextText: "Next", currentText: "Today", monthNames: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], monthNamesShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], dayNames: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], dayNamesShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], dayNamesMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"], weekHeader: "Wk", dateFormat: "mm/dd/yy", firstDay: 0, isRTL: false, showMonthAfterYear: false, yearSuffix: ""}; this._defaults = {showOn: "focus", showAnim: "fadeIn", showOptions: {}, defaultDate: null, appendText: "", buttonText: "...", buttonImage: "", buttonImageOnly: false, hideIfNoPrevNext: false, navigationAsDateFormat: false, gotoCurrent: false, changeMonth: false, changeYear: false, yearRange: "c-10:c+10", showOtherMonths: false, selectOtherMonths: false, showWeek: false, calculateWeek: this.iso8601Week, shortYearCutoff: "+10", minDate: null, maxDate: null, duration: "fast", beforeShowDay: null, beforeShow: null, onSelect: null, onChangeMonthYear: null, onClose: null, numberOfMonths: 1, showCurrentAtPos: 0, stepMonths: 1, stepBigMonths: 12, altField: "", altFormat: "", constrainInput: true, showButtonPanel: false, autoSize: false}; d.extend(this._defaults, this.regional[""]); this.dpDiv = d('
') } function E(a, b) { d.extend(a, b); for (var c in b)if (b[c] == null || b[c] == G)a[c] = b[c]; return a } d.extend(d.ui, {datepicker: {version: "1.8.5"}}); var y = (new Date).getTime(); d.extend(L.prototype, {markerClassName: "hasDatepicker", log: function () { this.debug && console.log.apply("", arguments) }, _widgetDatepicker: function () { return this.dpDiv }, setDefaults: function (a) { E(this._defaults, a || {}); return this }, _attachDatepicker: function (a, b) { var c = null; for (var e in this._defaults) { var f = a.getAttribute("date:" + e); if (f) { c = c || {}; try { c[e] = eval(f) } catch (h) { c[e] = f } } } e = a.nodeName.toLowerCase(); f = e == "div" || e == "span"; if (!a.id) { this.uuid += 1; a.id = "dp" + this.uuid } var i = this._newInst(d(a), f); i.settings = d.extend({}, b || {}, c || {}); if (e == "input")this._connectDatepicker(a, i); else f && this._inlineDatepicker(a, i) }, _newInst: function (a, b) { return{id: a[0].id.replace(/([^A-Za-z0-9_])/g, "\\\\$1"), input: a, selectedDay: 0, selectedMonth: 0, selectedYear: 0, drawMonth: 0, drawYear: 0, inline: b, dpDiv: !b ? this.dpDiv : d('
')} }, _connectDatepicker: function (a, b) { var c = d(a); b.append = d([]); b.trigger = d([]); if (!c.hasClass(this.markerClassName)) { this._attachments(c, b); c.addClass(this.markerClassName).keydown(this._doKeyDown).keypress(this._doKeyPress).keyup(this._doKeyUp).bind("setData.datepicker",function (e, f, h) { b.settings[f] = h }).bind("getData.datepicker", function (e, f) { return this._get(b, f) }); this._autoSize(b); d.data(a, "datepicker", b) } }, _attachments: function (a, b) { var c = this._get(b, "appendText"), e = this._get(b, "isRTL"); b.append && b.append.remove(); if (c) { b.append = d('' + c + ""); a[e ? "before" : "after"](b.append) } a.unbind("focus", this._showDatepicker); b.trigger && b.trigger.remove(); c = this._get(b, "showOn"); if (c == "focus" || c == "both")a.focus(this._showDatepicker); if (c == "button" || c == "both") { c = this._get(b, "buttonText"); var f = this._get(b, "buttonImage"); b.trigger = d(this._get(b, "buttonImageOnly") ? d("").addClass(this._triggerClass).attr({src: f, alt: c, title: c}) : d('').addClass(this._triggerClass).html(f == "" ? c : d("").attr({src: f, alt: c, title: c}))); a[e ? "before" : "after"](b.trigger); b.trigger.click(function () { d.datepicker._datepickerShowing && d.datepicker._lastInput == a[0] ? d.datepicker._hideDatepicker() : d.datepicker._showDatepicker(a[0]); return false }) } }, _autoSize: function (a) { if (this._get(a, "autoSize") && !a.inline) { var b = new Date(2009, 11, 20), c = this._get(a, "dateFormat"); if (c.match(/[DM]/)) { var e = function (f) { for (var h = 0, i = 0, g = 0; g < f.length; g++)if (f[g].length > h) { h = f[g].length; i = g } return i }; b.setMonth(e(this._get(a, c.match(/MM/) ? "monthNames" : "monthNamesShort"))); b.setDate(e(this._get(a, c.match(/DD/) ? "dayNames" : "dayNamesShort")) + 20 - b.getDay()) } a.input.attr("size", this._formatDate(a, b).length) } }, _inlineDatepicker: function (a, b) { var c = d(a); if (!c.hasClass(this.markerClassName)) { c.addClass(this.markerClassName).append(b.dpDiv).bind("setData.datepicker",function (e, f, h) { b.settings[f] = h }).bind("getData.datepicker", function (e, f) { return this._get(b, f) }); d.data(a, "datepicker", b); this._setDate(b, this._getDefaultDate(b), true); this._updateDatepicker(b); this._updateAlternate(b) } }, _dialogDatepicker: function (a, b, c, e, f) { a = this._dialogInst; if (!a) { this.uuid += 1; this._dialogInput = d(''); this._dialogInput.keydown(this._doKeyDown); d("body").append(this._dialogInput); a = this._dialogInst = this._newInst(this._dialogInput, false); a.settings = {}; d.data(this._dialogInput[0], "datepicker", a) } E(a.settings, e || {}); b = b && b.constructor == Date ? this._formatDate(a, b) : b; this._dialogInput.val(b); this._pos = f ? f.length ? f : [f.pageX, f.pageY] : null; if (!this._pos)this._pos = [document.documentElement.clientWidth / 2 - 100 + (document.documentElement.scrollLeft || document.body.scrollLeft), document.documentElement.clientHeight / 2 - 150 + (document.documentElement.scrollTop || document.body.scrollTop)]; this._dialogInput.css("left", this._pos[0] + 20 + "px").css("top", this._pos[1] + "px"); a.settings.onSelect = c; this._inDialog = true; this.dpDiv.addClass(this._dialogClass); this._showDatepicker(this._dialogInput[0]); d.blockUI && d.blockUI(this.dpDiv); d.data(this._dialogInput[0], "datepicker", a); return this }, _destroyDatepicker: function (a) { var b = d(a), c = d.data(a, "datepicker"); if (b.hasClass(this.markerClassName)) { var e = a.nodeName.toLowerCase(); d.removeData(a, "datepicker"); if (e == "input") { c.append.remove(); c.trigger.remove(); b.removeClass(this.markerClassName).unbind("focus", this._showDatepicker).unbind("keydown", this._doKeyDown).unbind("keypress", this._doKeyPress).unbind("keyup", this._doKeyUp) } else if (e == "div" || e == "span")b.removeClass(this.markerClassName).empty() } }, _enableDatepicker: function (a) { var b = d(a), c = d.data(a, "datepicker"); if (b.hasClass(this.markerClassName)) { var e = a.nodeName.toLowerCase(); if (e == "input") { a.disabled = false; c.trigger.filter("button").each(function () { this.disabled = false }).end().filter("img").css({opacity: "1.0", cursor: ""}) } else if (e == "div" || e == "span")b.children("." + this._inlineClass).children().removeClass("ui-state-disabled"); this._disabledInputs = d.map(this._disabledInputs, function (f) { return f == a ? null : f }) } }, _disableDatepicker: function (a) { var b = d(a), c = d.data(a, "datepicker"); if (b.hasClass(this.markerClassName)) { var e = a.nodeName.toLowerCase(); if (e == "input") { a.disabled = true; c.trigger.filter("button").each(function () { this.disabled = true }).end().filter("img").css({opacity: "0.5", cursor: "default"}) } else if (e == "div" || e == "span")b.children("." + this._inlineClass).children().addClass("ui-state-disabled"); this._disabledInputs = d.map(this._disabledInputs, function (f) { return f == a ? null : f }); this._disabledInputs[this._disabledInputs.length] = a } }, _isDisabledDatepicker: function (a) { if (!a)return false; for (var b = 0; b < this._disabledInputs.length; b++)if (this._disabledInputs[b] == a)return true; return false }, _getInst: function (a) { try { return d.data(a, "datepicker") } catch (b) { throw"Missing instance data for this datepicker"; } }, _optionDatepicker: function (a, b, c) { var e = this._getInst(a); if (arguments.length == 2 && typeof b == "string")return b == "defaults" ? d.extend({}, d.datepicker._defaults) : e ? b == "all" ? d.extend({}, e.settings) : this._get(e, b) : null; var f = b || {}; if (typeof b == "string") { f = {}; f[b] = c } if (e) { this._curInst == e && this._hideDatepicker(); var h = this._getDateDatepicker(a, true); E(e.settings, f); this._attachments(d(a), e); this._autoSize(e); this._setDateDatepicker(a, h); this._updateDatepicker(e) } }, _changeDatepicker: function (a, b, c) { this._optionDatepicker(a, b, c) }, _refreshDatepicker: function (a) { (a = this._getInst(a)) && this._updateDatepicker(a) }, _setDateDatepicker: function (a, b) { if (a = this._getInst(a)) { this._setDate(a, b); this._updateDatepicker(a); this._updateAlternate(a) } }, _getDateDatepicker: function (a, b) { (a = this._getInst(a)) && !a.inline && this._setDateFromField(a, b); return a ? this._getDate(a) : null }, _doKeyDown: function (a) { var b = d.datepicker._getInst(a.target), c = true, e = b.dpDiv.is(".ui-datepicker-rtl"); b._keyEvent = true; if (d.datepicker._datepickerShowing)switch (a.keyCode) { case 9: d.datepicker._hideDatepicker(); c = false; break; case 13: c = d("td." + d.datepicker._dayOverClass, b.dpDiv).add(d("td." + d.datepicker._currentClass, b.dpDiv)); c[0] ? d.datepicker._selectDay(a.target, b.selectedMonth, b.selectedYear, c[0]) : d.datepicker._hideDatepicker(); return false; case 27: d.datepicker._hideDatepicker(); break; case 33: d.datepicker._adjustDate(a.target, a.ctrlKey ? -d.datepicker._get(b, "stepBigMonths") : -d.datepicker._get(b, "stepMonths"), "M"); break; case 34: d.datepicker._adjustDate(a.target, a.ctrlKey ? +d.datepicker._get(b, "stepBigMonths") : +d.datepicker._get(b, "stepMonths"), "M"); break; case 35: if (a.ctrlKey || a.metaKey)d.datepicker._clearDate(a.target); c = a.ctrlKey || a.metaKey; break; case 36: if (a.ctrlKey || a.metaKey)d.datepicker._gotoToday(a.target); c = a.ctrlKey || a.metaKey; break; case 37: if (a.ctrlKey || a.metaKey)d.datepicker._adjustDate(a.target, e ? +1 : -1, "D"); c = a.ctrlKey || a.metaKey; if (a.originalEvent.altKey)d.datepicker._adjustDate(a.target, a.ctrlKey ? -d.datepicker._get(b, "stepBigMonths") : -d.datepicker._get(b, "stepMonths"), "M"); break; case 38: if (a.ctrlKey || a.metaKey)d.datepicker._adjustDate(a.target, -7, "D"); c = a.ctrlKey || a.metaKey; break; case 39: if (a.ctrlKey || a.metaKey)d.datepicker._adjustDate(a.target, e ? -1 : +1, "D"); c = a.ctrlKey || a.metaKey; if (a.originalEvent.altKey)d.datepicker._adjustDate(a.target, a.ctrlKey ? +d.datepicker._get(b, "stepBigMonths") : +d.datepicker._get(b, "stepMonths"), "M"); break; case 40: if (a.ctrlKey || a.metaKey)d.datepicker._adjustDate(a.target, +7, "D"); c = a.ctrlKey || a.metaKey; break; default: c = false } else if (a.keyCode == 36 && a.ctrlKey)d.datepicker._showDatepicker(this); else c = false; if (c) { a.preventDefault(); a.stopPropagation() } }, _doKeyPress: function (a) { var b = d.datepicker._getInst(a.target); if (d.datepicker._get(b, "constrainInput")) { b = d.datepicker._possibleChars(d.datepicker._get(b, "dateFormat")); var c = String.fromCharCode(a.charCode == G ? a.keyCode : a.charCode); return a.ctrlKey || c < " " || !b || b.indexOf(c) > -1 } }, _doKeyUp: function (a) { a = d.datepicker._getInst(a.target); if (a.input.val() != a.lastVal)try { if (d.datepicker.parseDate(d.datepicker._get(a, "dateFormat"), a.input ? a.input.val() : null, d.datepicker._getFormatConfig(a))) { d.datepicker._setDateFromField(a); d.datepicker._updateAlternate(a); d.datepicker._updateDatepicker(a) } } catch (b) { d.datepicker.log(b) } return true }, _showDatepicker: function (a) { a = a.target || a; if (a.nodeName.toLowerCase() != "input")a = d("input", a.parentNode)[0]; if (!(d.datepicker._isDisabledDatepicker(a) || d.datepicker._lastInput == a)) { var b = d.datepicker._getInst(a); d.datepicker._curInst && d.datepicker._curInst != b && d.datepicker._curInst.dpDiv.stop(true, true); var c = d.datepicker._get(b, "beforeShow"); E(b.settings, c ? c.apply(a, [a, b]) : {}); b.lastVal = null; d.datepicker._lastInput = a; d.datepicker._setDateFromField(b); if (d.datepicker._inDialog)a.value = ""; if (!d.datepicker._pos) { d.datepicker._pos = d.datepicker._findPos(a); d.datepicker._pos[1] += a.offsetHeight } var e = false; d(a).parents().each(function () { e |= d(this).css("position") == "fixed"; return!e }); if (e && d.browser.opera) { d.datepicker._pos[0] -= document.documentElement.scrollLeft; d.datepicker._pos[1] -= document.documentElement.scrollTop } c = {left: d.datepicker._pos[0], top: d.datepicker._pos[1]}; d.datepicker._pos = null; b.dpDiv.css({position: "absolute", display: "block", top: "-1000px"}); d.datepicker._updateDatepicker(b); c = d.datepicker._checkOffset(b, c, e); b.dpDiv.css({position: d.datepicker._inDialog && d.blockUI ? "static" : e ? "fixed" : "absolute", display: "none", left: c.left + "px", top: c.top + "px"}); if (!b.inline) { c = d.datepicker._get(b, "showAnim"); var f = d.datepicker._get(b, "duration"), h = function () { d.datepicker._datepickerShowing = true; var i = d.datepicker._getBorders(b.dpDiv); b.dpDiv.find("iframe.ui-datepicker-cover").css({left: -i[0], top: -i[1], width: b.dpDiv.outerWidth(), height: b.dpDiv.outerHeight()}) }; b.dpDiv.zIndex(d(a).zIndex() + 1); d.effects && d.effects[c] ? b.dpDiv.show(c, d.datepicker._get(b, "showOptions"), f, h) : b.dpDiv[c || "show"](c ? f : null, h); if (!c || !f)h(); b.input.is(":visible") && !b.input.is(":disabled") && b.input.focus(); d.datepicker._curInst = b } } }, _updateDatepicker: function (a) { var b = this, c = d.datepicker._getBorders(a.dpDiv); a.dpDiv.empty().append(this._generateHTML(a)).find("iframe.ui-datepicker-cover").css({left: -c[0], top: -c[1], width: a.dpDiv.outerWidth(), height: a.dpDiv.outerHeight()}).end().find("button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a").bind("mouseout",function () { d(this).removeClass("ui-state-hover"); this.className.indexOf("ui-datepicker-prev") != -1 && d(this).removeClass("ui-datepicker-prev-hover"); this.className.indexOf("ui-datepicker-next") != -1 && d(this).removeClass("ui-datepicker-next-hover") }).bind("mouseover",function () { if (!b._isDisabledDatepicker(a.inline ? a.dpDiv.parent()[0] : a.input[0])) { d(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"); d(this).addClass("ui-state-hover"); this.className.indexOf("ui-datepicker-prev") != -1 && d(this).addClass("ui-datepicker-prev-hover"); this.className.indexOf("ui-datepicker-next") != -1 && d(this).addClass("ui-datepicker-next-hover") } }).end().find("." + this._dayOverClass + " a").trigger("mouseover").end(); c = this._getNumberOfMonths(a); var e = c[1]; e > 1 ? a.dpDiv.addClass("ui-datepicker-multi-" + e).css("width", 17 * e + "em") : a.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width(""); a.dpDiv[(c[0] != 1 || c[1] != 1 ? "add" : "remove") + "Class"]("ui-datepicker-multi"); a.dpDiv[(this._get(a, "isRTL") ? "add" : "remove") + "Class"]("ui-datepicker-rtl"); a == d.datepicker._curInst && d.datepicker._datepickerShowing && a.input && a.input.is(":visible") && !a.input.is(":disabled") && a.input.focus() }, _getBorders: function (a) { var b = function (c) { return{thin: 1, medium: 2, thick: 3}[c] || c }; return[parseFloat(b(a.css("border-left-width"))), parseFloat(b(a.css("border-top-width")))] }, _checkOffset: function (a, b, c) { var e = a.dpDiv.outerWidth(), f = a.dpDiv.outerHeight(), h = a.input ? a.input.outerWidth() : 0, i = a.input ? a.input.outerHeight() : 0, g = document.documentElement.clientWidth + d(document).scrollLeft(), k = document.documentElement.clientHeight + d(document).scrollTop(); b.left -= this._get(a, "isRTL") ? e - h : 0; b.left -= c && b.left == a.input.offset().left ? d(document).scrollLeft() : 0; b.top -= c && b.top == a.input.offset().top + i ? d(document).scrollTop() : 0; b.left -= Math.min(b.left, b.left + e > g && g > e ? Math.abs(b.left + e - g) : 0); b.top -= Math.min(b.top, b.top + f > k && k > f ? Math.abs(f + i) : 0); return b }, _findPos: function (a) { for (var b = this._get(this._getInst(a), "isRTL"); a && (a.type == "hidden" || a.nodeType != 1);)a = a[b ? "previousSibling" : "nextSibling"]; a = d(a).offset(); return[a.left, a.top] }, _hideDatepicker: function (a) { var b = this._curInst; if (!(!b || a && b != d.data(a, "datepicker")))if (this._datepickerShowing) { a = this._get(b, "showAnim"); var c = this._get(b, "duration"), e = function () { d.datepicker._tidyDialog(b); this._curInst = null }; d.effects && d.effects[a] ? b.dpDiv.hide(a, d.datepicker._get(b, "showOptions"), c, e) : b.dpDiv[a == "slideDown" ? "slideUp" : a == "fadeIn" ? "fadeOut" : "hide"](a ? c : null, e); a || e(); if (a = this._get(b, "onClose"))a.apply(b.input ? b.input[0] : null, [b.input ? b.input.val() : "", b]); this._datepickerShowing = false; this._lastInput = null; if (this._inDialog) { this._dialogInput.css({position: "absolute", left: "0", top: "-100px"}); if (d.blockUI) { d.unblockUI(); d("body").append(this.dpDiv) } } this._inDialog = false } }, _tidyDialog: function (a) { a.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar") }, _checkExternalClick: function (a) { if (d.datepicker._curInst) { a = d(a.target); a[0].id != d.datepicker._mainDivId && a.parents("#" + d.datepicker._mainDivId).length == 0 && !a.hasClass(d.datepicker.markerClassName) && !a.hasClass(d.datepicker._triggerClass) && d.datepicker._datepickerShowing && !(d.datepicker._inDialog && d.blockUI) && d.datepicker._hideDatepicker() } }, _adjustDate: function (a, b, c) { a = d(a); var e = this._getInst(a[0]); if (!this._isDisabledDatepicker(a[0])) { this._adjustInstDate(e, b + (c == "M" ? this._get(e, "showCurrentAtPos") : 0), c); this._updateDatepicker(e) } }, _gotoToday: function (a) { a = d(a); var b = this._getInst(a[0]); if (this._get(b, "gotoCurrent") && b.currentDay) { b.selectedDay = b.currentDay; b.drawMonth = b.selectedMonth = b.currentMonth; b.drawYear = b.selectedYear = b.currentYear } else { var c = new Date; b.selectedDay = c.getDate(); b.drawMonth = b.selectedMonth = c.getMonth(); b.drawYear = b.selectedYear = c.getFullYear() } this._notifyChange(b); this._adjustDate(a) }, _selectMonthYear: function (a, b, c) { a = d(a); var e = this._getInst(a[0]); e._selectingMonthYear = false; e["selected" + (c == "M" ? "Month" : "Year")] = e["draw" + (c == "M" ? "Month" : "Year")] = parseInt(b.options[b.selectedIndex].value, 10); this._notifyChange(e); this._adjustDate(a) }, _clickMonthYear: function (a) { var b = this._getInst(d(a)[0]); b.input && b._selectingMonthYear && setTimeout(function () { b.input.focus() }, 0); b._selectingMonthYear = !b._selectingMonthYear }, _selectDay: function (a, b, c, e) { var f = d(a); if (!(d(e).hasClass(this._unselectableClass) || this._isDisabledDatepicker(f[0]))) { f = this._getInst(f[0]); f.selectedDay = f.currentDay = d("a", e).html(); f.selectedMonth = f.currentMonth = b; f.selectedYear = f.currentYear = c; this._selectDate(a, this._formatDate(f, f.currentDay, f.currentMonth, f.currentYear)) } }, _clearDate: function (a) { a = d(a); this._getInst(a[0]); this._selectDate(a, "") }, _selectDate: function (a, b) { a = this._getInst(d(a)[0]); b = b != null ? b : this._formatDate(a); a.input && a.input.val(b); this._updateAlternate(a); var c = this._get(a, "onSelect"); if (c)c.apply(a.input ? a.input[0] : null, [b, a]); else a.input && a.input.trigger("change"); if (a.inline)this._updateDatepicker(a); else { this._hideDatepicker(); this._lastInput = a.input[0]; typeof a.input[0] != "object" && a.input.focus(); this._lastInput = null } }, _updateAlternate: function (a) { var b = this._get(a, "altField"); if (b) { var c = this._get(a, "altFormat") || this._get(a, "dateFormat"), e = this._getDate(a), f = this.formatDate(c, e, this._getFormatConfig(a)); d(b).each(function () { d(this).val(f) }) } }, noWeekends: function (a) { a = a.getDay(); return[a > 0 && a < 6, ""] }, iso8601Week: function (a) { a = new Date(a.getTime()); a.setDate(a.getDate() + 4 - (a.getDay() || 7)); var b = a.getTime(); a.setMonth(0); a.setDate(1); return Math.floor(Math.round((b - a) / 864E5) / 7) + 1 }, parseDate: function (a, b, c) { if (a == null || b == null)throw"Invalid arguments"; b = typeof b == "object" ? b.toString() : b + ""; if (b == "")return null; for (var e = (c ? c.shortYearCutoff : null) || this._defaults.shortYearCutoff, f = (c ? c.dayNamesShort : null) || this._defaults.dayNamesShort, h = (c ? c.dayNames : null) || this._defaults.dayNames, i = (c ? c.monthNamesShort : null) || this._defaults.monthNamesShort, g = (c ? c.monthNames : null) || this._defaults.monthNames, k = c = -1, l = -1, u = -1, j = false, o = function (p) { (p = z + 1 < a.length && a.charAt(z + 1) == p) && z++; return p }, m = function (p) { o(p); p = new RegExp("^\\d{1," + (p == "@" ? 14 : p == "!" ? 20 : p == "y" ? 4 : p == "o" ? 3 : 2) + "}"); p = b.substring(s).match(p); if (!p)throw"Missing number at position " + s; s += p[0].length; return parseInt(p[0], 10) }, n = function (p, w, H) { p = o(p) ? H : w; for (w = 0; w < p.length; w++)if (b.substr(s, p[w].length).toLowerCase() == p[w].toLowerCase()) { s += p[w].length; return w + 1 } throw"Unknown name at position " + s; }, r = function () { if (b.charAt(s) != a.charAt(z))throw"Unexpected literal at position " + s; s++ }, s = 0, z = 0; z < a.length; z++)if (j)if (a.charAt(z) == "'" && !o("'"))j = false; else r(); else switch (a.charAt(z)) { case "d": l = m("d"); break; case "D": n("D", f, h); break; case "o": u = m("o"); break; case "m": k = m("m"); break; case "M": k = n("M", i, g); break; case "y": c = m("y"); break; case "@": var v = new Date(m("@")); c = v.getFullYear(); k = v.getMonth() + 1; l = v.getDate(); break; case "!": v = new Date((m("!") - this._ticksTo1970) / 1E4); c = v.getFullYear(); k = v.getMonth() + 1; l = v.getDate(); break; case "'": if (o("'"))r(); else j = true; break; default: r() } if (c == -1)c = (new Date).getFullYear(); else if (c < 100)c += (new Date).getFullYear() - (new Date).getFullYear() % 100 + (c <= e ? 0 : -100); if (u > -1) { k = 1; l = u; do { e = this._getDaysInMonth(c, k - 1); if (l <= e)break; k++; l -= e } while (1) } v = this._daylightSavingAdjust(new Date(c, k - 1, l)); if (v.getFullYear() != c || v.getMonth() + 1 != k || v.getDate() != l)throw"Invalid date"; return v }, ATOM: "yy-mm-dd", COOKIE: "D, dd M yy", ISO_8601: "yy-mm-dd", RFC_822: "D, d M y", RFC_850: "DD, dd-M-y", RFC_1036: "D, d M y", RFC_1123: "D, d M yy", RFC_2822: "D, d M yy", RSS: "D, d M y", TICKS: "!", TIMESTAMP: "@", W3C: "yy-mm-dd", _ticksTo1970: (718685 + Math.floor(492.5) - Math.floor(19.7) + Math.floor(4.925)) * 24 * 60 * 60 * 1E7, formatDate: function (a, b, c) { if (!b)return""; var e = (c ? c.dayNamesShort : null) || this._defaults.dayNamesShort, f = (c ? c.dayNames : null) || this._defaults.dayNames, h = (c ? c.monthNamesShort : null) || this._defaults.monthNamesShort; c = (c ? c.monthNames : null) || this._defaults.monthNames; var i = function (o) { (o = j + 1 < a.length && a.charAt(j + 1) == o) && j++; return o }, g = function (o, m, n) { m = "" + m; if (i(o))for (; m.length < n;)m = "0" + m; return m }, k = function (o, m, n, r) { return i(o) ? r[m] : n[m] }, l = "", u = false; if (b)for (var j = 0; j < a.length; j++)if (u)if (a.charAt(j) == "'" && !i("'"))u = false; else l += a.charAt(j); else switch (a.charAt(j)) { case "d": l += g("d", b.getDate(), 2); break; case "D": l += k("D", b.getDay(), e, f); break; case "o": l += g("o", (b.getTime() - (new Date(b.getFullYear(), 0, 0)).getTime()) / 864E5, 3); break; case "m": l += g("m", b.getMonth() + 1, 2); break; case "M": l += k("M", b.getMonth(), h, c); break; case "y": l += i("y") ? b.getFullYear() : (b.getYear() % 100 < 10 ? "0" : "") + b.getYear() % 100; break; case "@": l += b.getTime(); break; case "!": l += b.getTime() * 1E4 + this._ticksTo1970; break; case "'": if (i("'"))l += "'"; else u = true; break; default: l += a.charAt(j) } return l }, _possibleChars: function (a) { for (var b = "", c = false, e = function (h) { (h = f + 1 < a.length && a.charAt(f + 1) == h) && f++; return h }, f = 0; f < a.length; f++)if (c)if (a.charAt(f) == "'" && !e("'"))c = false; else b += a.charAt(f); else switch (a.charAt(f)) { case "d": case "m": case "y": case "@": b += "0123456789"; break; case "D": case "M": return null; case "'": if (e("'"))b += "'"; else c = true; break; default: b += a.charAt(f) } return b }, _get: function (a, b) { return a.settings[b] !== G ? a.settings[b] : this._defaults[b] }, _setDateFromField: function (a, b) { if (a.input.val() != a.lastVal) { var c = this._get(a, "dateFormat"), e = a.lastVal = a.input ? a.input.val() : null, f, h; f = h = this._getDefaultDate(a); var i = this._getFormatConfig(a); try { f = this.parseDate(c, e, i) || h } catch (g) { this.log(g); e = b ? "" : e } a.selectedDay = f.getDate(); a.drawMonth = a.selectedMonth = f.getMonth(); a.drawYear = a.selectedYear = f.getFullYear(); a.currentDay = e ? f.getDate() : 0; a.currentMonth = e ? f.getMonth() : 0; a.currentYear = e ? f.getFullYear() : 0; this._adjustInstDate(a) } }, _getDefaultDate: function (a) { return this._restrictMinMax(a, this._determineDate(a, this._get(a, "defaultDate"), new Date)) }, _determineDate: function (a, b, c) { var e = function (h) { var i = new Date; i.setDate(i.getDate() + h); return i }, f = function (h) { try { return d.datepicker.parseDate(d.datepicker._get(a, "dateFormat"), h, d.datepicker._getFormatConfig(a)) } catch (i) { } var g = (h.toLowerCase().match(/^c/) ? d.datepicker._getDate(a) : null) || new Date, k = g.getFullYear(), l = g.getMonth(); g = g.getDate(); for (var u = /([+-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g, j = u.exec(h); j;) { switch (j[2] || "d") { case "d": case "D": g += parseInt(j[1], 10); break; case "w": case "W": g += parseInt(j[1], 10) * 7; break; case "m": case "M": l += parseInt(j[1], 10); g = Math.min(g, d.datepicker._getDaysInMonth(k, l)); break; case "y": case "Y": k += parseInt(j[1], 10); g = Math.min(g, d.datepicker._getDaysInMonth(k, l)); break } j = u.exec(h) } return new Date(k, l, g) }; if (b = (b = b == null ? c : typeof b == "string" ? f(b) : typeof b == "number" ? isNaN(b) ? c : e(b) : b) && b.toString() == "Invalid Date" ? c : b) { b.setHours(0); b.setMinutes(0); b.setSeconds(0); b.setMilliseconds(0) } return this._daylightSavingAdjust(b) }, _daylightSavingAdjust: function (a) { if (!a)return null; a.setHours(a.getHours() > 12 ? a.getHours() + 2 : 0); return a }, _setDate: function (a, b, c) { var e = !b, f = a.selectedMonth, h = a.selectedYear; b = this._restrictMinMax(a, this._determineDate(a, b, new Date)); a.selectedDay = a.currentDay = b.getDate(); a.drawMonth = a.selectedMonth = a.currentMonth = b.getMonth(); a.drawYear = a.selectedYear = a.currentYear = b.getFullYear(); if ((f != a.selectedMonth || h != a.selectedYear) && !c)this._notifyChange(a); this._adjustInstDate(a); if (a.input)a.input.val(e ? "" : this._formatDate(a)) }, _getDate: function (a) { return!a.currentYear || a.input && a.input.val() == "" ? null : this._daylightSavingAdjust(new Date(a.currentYear, a.currentMonth, a.currentDay)) }, _generateHTML: function (a) { var b = new Date; b = this._daylightSavingAdjust(new Date(b.getFullYear(), b.getMonth(), b.getDate())); var c = this._get(a, "isRTL"), e = this._get(a, "showButtonPanel"), f = this._get(a, "hideIfNoPrevNext"), h = this._get(a, "navigationAsDateFormat"), i = this._getNumberOfMonths(a), g = this._get(a, "showCurrentAtPos"), k = this._get(a, "stepMonths"), l = i[0] != 1 || i[1] != 1, u = this._daylightSavingAdjust(!a.currentDay ? new Date(9999, 9, 9) : new Date(a.currentYear, a.currentMonth, a.currentDay)), j = this._getMinMaxDate(a, "min"), o = this._getMinMaxDate(a, "max"); g = a.drawMonth - g; var m = a.drawYear; if (g < 0) { g += 12; m-- } if (o) { var n = this._daylightSavingAdjust(new Date(o.getFullYear(), o.getMonth() - i[0] * i[1] + 1, o.getDate())); for (n = j && n < j ? j : n; this._daylightSavingAdjust(new Date(m, g, 1)) > n;) { g--; if (g < 0) { g = 11; m-- } } } a.drawMonth = g; a.drawYear = m; n = this._get(a, "prevText"); n = !h ? n : this.formatDate(n, this._daylightSavingAdjust(new Date(m, g - k, 1)), this._getFormatConfig(a)); n = this._canAdjustMonth(a, -1, m, g) ? '' + n + "" : f ? "" : '' + n + ""; var r = this._get(a, "nextText"); r = !h ? r : this.formatDate(r, this._daylightSavingAdjust(new Date(m, g + k, 1)), this._getFormatConfig(a)); f = this._canAdjustMonth(a, +1, m, g) ? '' + r + "" : f ? "" : '' + r + ""; k = this._get(a, "currentText"); r = this._get(a, "gotoCurrent") && a.currentDay ? u : b; k = !h ? k : this.formatDate(k, r, this._getFormatConfig(a)); h = !a.inline ? '" : ""; 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); ;django-treebeard-2.0b1/treebeard/static/treebeard/treebeard-admin.css0000644000076500000240000000261012150510123025746 0ustar tabostaff00000000000000/* 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; }*/ django-treebeard-2.0b1/treebeard/static/treebeard/treebeard-admin.js0000644000076500000240000003467112150510123025606 0ustar tabostaff00000000000000(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 $('html').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.curCSS(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); django-treebeard-2.0b1/treebeard/templates/0000755000076500000240000000000012151313146020773 5ustar tabostaff00000000000000django-treebeard-2.0b1/treebeard/templates/admin/0000755000076500000240000000000012151313146022063 5ustar tabostaff00000000000000django-treebeard-2.0b1/treebeard/templates/admin/tree_change_list.html0000644000076500000240000000110212150510123026233 0ustar tabostaff00000000000000{# 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 %} django-treebeard-2.0b1/treebeard/templates/admin/tree_change_list_results.html0000644000076500000240000000300412150510123030017 0ustar tabostaff00000000000000{% 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, has_children, result in results %} {% for item in result %} {% if forloop.counter == 1 %} {% for spacer in item.depth %}  {% endfor %} {% endif %} {{ item }} {% endfor %} {% endfor %}
{% endif %} django-treebeard-2.0b1/treebeard/templates/admin/tree_list.html0000644000076500000240000000102112150510123024726 0ustar tabostaff00000000000000{# 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 %} django-treebeard-2.0b1/treebeard/templates/admin/tree_list_results.html0000644000076500000240000000025512150510123026517 0ustar tabostaff00000000000000{% if results %}
    {% for result in results %}
  • {{ result }}
  • {% endfor %}
{% endif %} django-treebeard-2.0b1/treebeard/templatetags/0000755000076500000240000000000012151313146021467 5ustar tabostaff00000000000000django-treebeard-2.0b1/treebeard/templatetags/__init__.py0000644000076500000240000000025212150777026023611 0ustar tabostaff00000000000000from django.template import Variable action_form_var = Variable('action_form') def needs_checkboxes(context): return action_form_var.resolve(context) is not None django-treebeard-2.0b1/treebeard/templatetags/admin_tree.py0000644000076500000240000002117112150777261024165 0ustar tabostaff00000000000000# -*- coding: utf-8 -*- """ Templatetags for django-treebeard to add drag and drop capabilities to the nodes change list - @jjdelc """ import sys from django.db import models from django.conf import settings from django.contrib.admin.templatetags.admin_list import ( _boolean_icon, result_headers, result_hidden_fields) from django.contrib.admin.util import display_for_field, lookup_field from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE from django.core.exceptions import ObjectDoesNotExist from django.template import Library from django.utils.html import conditional_escape, escape from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ register = Library() if sys.version_info >= (3, 0): from django.utils.encoding import force_str, smart_str from urllib.parse import urljoin else: from django.utils.encoding import force_unicode as force_str from django.utils.encoding import smart_unicode as smart_str from urlparse import urljoin from treebeard.templatetags import needs_checkboxes 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: row_class = '' try: f, attr, value = lookup_field(field_name, result, cl.model_admin) except (AttributeError, ObjectDoesNotExist): result_repr = EMPTY_CHANGELIST_VALUE else: if f is None: if getattr(attr, 'boolean', False): allow_tags = True result_repr = _boolean_icon(value) else: allow_tags = getattr(attr, 'allow_tags', False) result_repr = smart_str(value) if allow_tags: result_repr = mark_safe(result_repr) else: # strip HTML tags result_repr = escape(result_repr) else: if isinstance(f.rel, models.ManyToOneRel): result_repr = escape(getattr(result, f.name)) else: result_repr = display_for_field(value, f) if isinstance(f, (models.DateField, models.TimeField)): row_class = ' class="nowrap"' if force_str(result_repr) == '': result_repr = mark_safe(' ') # 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 if first: spacer = ' ' * ( result.get_depth() - 1) else: spacer = '' # This shows a collapse or expand link for nodes with childs if result.get_children_count(): collapse = ('' '-') else: collapse = ' ' # Add a before the first col to show the drag handler drag_handler = '' if first: drag_handler = ('' ' ') 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 = repr(force_str(value))[1:] onclickstr = ( ' onclick="opener.dismissRelatedLookupPopup(window, %s);' ' return false;"') yield mark_safe( '%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: bf = form[field_name] result_repr = mark_safe( force_str(bf.errors) + force_str(bf)) else: result_repr = conditional_escape(result_repr) yield mark_safe('%s' % (row_class, result_repr)) if form and not form[cl.model._meta.pk.name].is_hidden: yield mark_safe( '%s' % 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 = '/' if not path.endswith('/'): path += '/' return path @register.simple_tag def treebeard_css(): """ Template tag to print out the proper tag to include a custom .css """ LINK_HTML = """""" css_file = urljoin(get_static_url(), 'treebeard/treebeard-admin.css') return LINK_HTML % css_file @register.simple_tag def treebeard_js(): """ Template tag to print out the proper """ js_file = '/'.join([path.rstrip('/'), 'treebeard', 'treebeard-admin.js']) # Jquery UI is needed to call disableSelection() on drag and drop so # text selections arent marked while dragging a table row # http://www.lokkju.com/blog/archives/143 JQUERY_UI = """ """ jquery_ui = urljoin(path, 'treebeard/jquery-ui-1.8.5.custom.min.js') scripts = [SCRIPT_HTML % 'jsi18n', SCRIPT_HTML % js_file, JQUERY_UI % jquery_ui] return ''.join(scripts) django-treebeard-2.0b1/treebeard/templatetags/admin_tree_list.py0000644000076500000240000000227412150777054025223 0ustar tabostaff00000000000000# -*- coding: utf-8 -*- from django.template import Library from treebeard.templatetags import needs_checkboxes register = Library() CHECKBOX_TMPL = ('') def _line(context, node, request): if 't' in request.GET and request.GET['t'] == 'id': raw_id_fields = """ onclick="opener.dismissRelatedLookupPopup(window, '%d'); return false;" """ % (node.pk,) else: raw_id_fields = '' output = '' if needs_checkboxes(context): output += CHECKBOX_TMPL % node.pk return output + '%s' % ( node.pk, raw_id_fields, str(node)) def _subtree(context, node, request): tree = '' for subnode in node.get_children(): tree += '
  • %s
  • ' % _subtree(context, subnode, request) if tree: tree = '
      %s
    ' % 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 += '
  • %s
  • ' % _subtree(context, root_node, request) return "
      %s
    " % tree django-treebeard-2.0b1/treebeard/tests/0000755000076500000240000000000012151313146020137 5ustar tabostaff00000000000000django-treebeard-2.0b1/treebeard/tests/__init__.py0000644000076500000240000000000012150510123022227 0ustar tabostaff00000000000000django-treebeard-2.0b1/treebeard/tests/conftest.py0000644000076500000240000000452612150510123022336 0ustar tabostaff00000000000000import os import sys import time os.environ['DJANGO_SETTINGS_MODULE'] = 'treebeard.tests.settings' import django from django.conf import settings from django.test.utils import (setup_test_environment, teardown_test_environment) from django.test.client import Client from django.core.management import call_command from django.core import mail from django.db import connection from django.db.models.base import ModelBase from _pytest import python as _pytest_python def idmaker(argnames, argvalues): idlist = [] for valindex, valset in enumerate(argvalues): this_id = [] for nameindex, val in enumerate(valset): argname = argnames[nameindex] if isinstance(val, (float, int, str)): this_id.append(str(val)) elif isinstance(val, ModelBase): this_id.append(val.__name__) else: this_id.append("{0}-{1}={2!s}".format(argname, valindex)) idlist.append("][".join(this_id)) return idlist _pytest_python.idmaker = idmaker def pytest_report_header(config): return 'Django: ' + django.get_version() def pytest_configure(config): setup_test_environment() connection.creation.create_test_db(verbosity=2, autoclobber=True) def pytest_unconfigure(config): dbsettings = settings.DATABASES['default'] dbtestname = dbsettings['TEST_NAME'] connection.close() if dbsettings['ENGINE'].split('.')[-1] == 'postgresql_psycopg2': connection.connection = None connection.settings_dict['NAME'] = dbtestname.split('_')[1] cursor = connection.cursor() connection.autocommit = True connection._set_isolation_level(0) time.sleep(1) sys.stdout.write( "Destroying test database for alias '%s' (%s)...\n" % ( connection.alias, dbtestname) ) sys.stdout.flush() cursor.execute( 'DROP DATABASE %s' % connection.ops.quote_name(dbtestname)) else: connection.creation.destroy_test_db(dbtestname, verbosity=2) teardown_test_environment() def pytest_funcarg__client(request): def setup(): mail.outbox = [] return Client() def teardown(client): call_command('flush', verbosity=0, interactive=False) return request.cached_setup(setup, teardown, 'function') django-treebeard-2.0b1/treebeard/tests/jenkins/0000755000076500000240000000000012151313146021600 5ustar tabostaff00000000000000django-treebeard-2.0b1/treebeard/tests/jenkins/rm_workspace_coverage.py0000644000076500000240000000103712150510123026513 0ustar tabostaff00000000000000"""Remove `.coverage.$HOST.$ID` files from previous runs. In Python because of portability with Windows. """ import sys import os def main(): workspace = os.environ['WORKSPACE'] for filename in os.listdir(workspace): 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() django-treebeard-2.0b1/treebeard/tests/jenkins/toxhelper.py0000644000076500000240000000155112150510123024157 0ustar tabostaff00000000000000#!/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 pytest from coverage import coverage def run_the_tests(): 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()) django-treebeard-2.0b1/treebeard/tests/models.py0000644000076500000240000001275512151053251022004 0ustar tabostaff00000000000000from django.db import models, connection 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_TestNodeSomeDep(models.Model): node = models.ForeignKey(MP_TestNode) 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) def __str__(self): # pragma: no cover return 'Node %d' % self.pk class NS_TestNode(NS_Node): desc = models.CharField(max_length=255) def __str__(self): # pragma: no cover return 'Node %d' % self.pk class NS_TestNodeSomeDep(models.Model): node = models.ForeignKey(NS_TestNode) 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) def __str__(self): # pragma: no cover return 'Node %d' % self.pk class AL_TestNode(AL_Node): parent = models.ForeignKey('self', related_name='children_set', null=True, db_index=True) sib_order = models.PositiveIntegerField() desc = models.CharField(max_length=255) def __str__(self): # pragma: no cover return 'Node %d' % self.pk class AL_TestNodeSomeDep(models.Model): node = models.ForeignKey(AL_TestNode) 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) sib_order = models.PositiveIntegerField() desc = models.CharField(max_length=255) related = models.ForeignKey(RelatedModel) def __str__(self): # pragma: no cover return 'Node %d' % self.pk 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) 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 def empty_models_tables(models): for model in models: model.objects.all().delete() django-treebeard-2.0b1/treebeard/tests/settings.py0000644000076500000240000000346612150510123022353 0ustar tabostaff00000000000000"""Django settings for testing treebeard""" import random import string import os def get_db_conf(): conf, options = {}, {} for name in ('ENGINE', 'NAME', 'USER', 'PASSWORD', 'HOST', 'PORT'): conf[name] = os.environ.get('DATABASE_' + name, '') engine = conf['ENGINE'] if engine == '': engine = 'sqlite3' elif engine in ('pgsql', 'postgres', 'postgresql', 'psycopg2'): engine = 'postgresql_psycopg2' if '.' not in engine: engine = 'django.db.backends.' + engine conf['ENGINE'] = engine if engine == 'django.db.backends.sqlite3': conf['TEST_NAME'] = conf['NAME'] = ':memory:' elif engine in ('django.db.backends.mysql', 'django.db.backends.postgresql_psycopg2'): if not conf['NAME']: conf['NAME'] = 'treebeard' # randomizing the test db name, # so we can safely run multiple # tests at the same time conf['TEST_NAME'] = "test_%s_%s" % ( conf['NAME'], ''.join(random.choice(string.ascii_letters) for _ in range(15)) ) if conf['USER'] == '': conf['USER'] = { 'django.db.backends.mysql': 'root', 'django.db.backends.postgresql_psycopg2': 'postgres' }[engine] if engine == 'django.db.backends.mysql': conf['OPTIONS'] = { 'init_command': 'SET storage_engine=INNODB,' 'character_set_connection=utf8,' 'collation_connection=utf8_unicode_ci'} return conf DATABASES = {'default': get_db_conf()} SECRET_KEY = '7r33b34rd' INSTALLED_APPS = [ 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.admin', 'treebeard', 'treebeard.tests'] django-treebeard-2.0b1/treebeard/tests/test_treebeard.py0000644000076500000240000021720212151051647023516 0ustar tabostaff00000000000000"""Unit/Functional tests""" from __future__ import with_statement import datetime import os import sys from django.contrib.admin.sites import AdminSite from django.test import TestCase from django.contrib.auth.models import User from django.db.models import Q import pytest from treebeard import numconv from treebeard.exceptions import InvalidPosition, InvalidMoveToDescendant,\ PathOverflow, MissingNodeOrderBy from treebeard.forms import movenodeform_factory from treebeard.admin import admin_factory from treebeard.tests import models 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 @pytest.fixture(scope='function', params=models.BASE_MODELS + models.PROXY_MODELS) def model(request): return _prepare_db_test(request) @pytest.fixture(scope='function', params=models.BASE_MODELS) def model_without_proxy(request): return _prepare_db_test(request) @pytest.fixture(scope='function', params=models.SORTED_MODELS) def sorted_model(request): return _prepare_db_test(request) @pytest.fixture(scope='function', params=models.RELATED_MODELS) def related_model(request): return _prepare_db_test(request) @pytest.fixture(scope='function', params=models.MP_SHORTPATH_MODELS) def mpshort_model(request): return _prepare_db_test(request) @pytest.fixture(scope='function', params=[models.MP_TestNodeShortPath]) def mpshortnotsorted_model(request): return _prepare_db_test(request) @pytest.fixture(scope='function', params=[models.MP_TestNodeAlphabet]) def mpalphabet_model(request): return _prepare_db_test(request) @pytest.fixture(scope='function', params=[models.MP_TestNodeSortedAutoNow]) 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): got = [ (obj[0].desc, obj[1]['open'], obj[1]['close'], obj[1]['level']) for obj in model.get_annotated_list(parent) ] assert expected == got 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): got = [(o.desc, o.get_depth(), o.get_children_count()) for o in model.get_tree()] assert got == UNCHANGED 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) got = [(o.desc, o.get_depth(), o.get_children_count()) for o in model.get_tree(node)] 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 def test_get_tree_leaf(self, model): node = model.objects.get(desc='1') assert 0 == node.get_children_count() got = [(o.desc, o.get_depth(), o.get_children_count()) for o in model.get_tree(node)] expected = [('1', 1, 0)] assert got == expected 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 def test_get_first_root_node(self, model): got = model.get_first_root_node() assert got.desc == '1' def test_get_last_root_node(self, model): got = model.get_last_root_node() assert got.desc == '4' def test_add_root(self, model): obj = model.add_root(desc='5') assert obj.get_depth() == 1 assert model.get_last_root_node().desc == '5' 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 TestMP_TreeAlphabet(TestTreeBase): def test_alphabet(self, mpalphabet_model): if not os.getenv('TREEBEARD_TEST_ALPHABET', False): # run this test only if the enviroment variable is set return basealpha = numconv.BASE85 got_err = False last_good = None for alphabetlen in range(35, len(basealpha) + 1): alphabet = basealpha[0:alphabetlen] 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 # 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: got_err = True last_good = alphabet sys.stdout.write( '\nThe best BASE85 based alphabet for your setup is: %s\n' % ( last_good, ) ) sys.stdout.flush() 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.id, 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_get_position_ref_node(self, model): form_class = movenodeform_factory(model) instance_parent = model.objects.get(desc='1') form = form_class(instance=instance_parent) assert form._get_position_ref_node(instance_parent) == { '_position': 'first-child', '_ref_node_id': '' } instance_child = model.objects.get(desc='21') form = form_class(instance=instance_child) assert form._get_position_ref_node(instance_child) == { '_position': 'first-child', '_ref_node_id': model.objects.get(desc='2').pk } instance_grandchild = model.objects.get(desc='22') form = form_class(instance=instance_grandchild) assert form._get_position_ref_node(instance_grandchild) == { '_position': 'right', '_ref_node_id': model.objects.get(desc='21').pk } instance_grandchild = model.objects.get(desc='231') form = form_class(instance=instance_grandchild) assert form._get_position_ref_node(instance_grandchild) == { '_position': 'first-child', '_ref_node_id': model.objects.get(desc='23').pk } def test_clean_cleaned_data(self, model): instance_parent = model.objects.get(desc='1') _position = 'first-child' _ref_node_id = '' form_class = movenodeform_factory(model) form = form_class( instance=instance_parent, data={ '_position': _position, '_ref_node_id': _ref_node_id, 'desc': instance_parent.desc } ) assert form.is_valid() assert form._clean_cleaned_data() == (_position, _ref_node_id) def test_save_edit(self, model): instance_parent = model.objects.get(desc='1') original_count = len(model.objects.all()) form_class = movenodeform_factory(model) form = form_class( instance=instance_parent, data={ '_position': 'first-child', '_ref_node_id': model.objects.get(desc='2').pk, 'desc': instance_parent.desc } ) assert form.is_valid() saved_instance = form.save() assert original_count == model.objects.all().count() assert saved_instance.get_children_count() == 0 assert saved_instance.get_depth() == 2 assert not saved_instance.is_root() assert saved_instance.is_leaf() # Return to original state form_class = movenodeform_factory(model) form = form_class( instance=saved_instance, data={ '_position': 'first-child', '_ref_node_id': '', 'desc': saved_instance.desc } ) assert form.is_valid() restored_instance = form.save() assert original_count == model.objects.all().count() assert restored_instance.get_children_count() == 0 assert restored_instance.get_depth() == 1 assert restored_instance.is_root() assert restored_instance.is_leaf() def test_save_new(self, model): original_count = model.objects.all().count() assert original_count == 10 _position = 'first-child' form_class = movenodeform_factory(model) form = form_class( data={'_position': _position, 'desc': 'New Form Test'}) assert form.is_valid() assert form.save() is not None assert original_count < model.objects.all().count() django-treebeard-2.0b1/UPDATING0000644000076500000240000000074212150510123016172 0ustar tabostaff00000000000000This file documents problems you may encounter when upgrading django-treebeard (potential backward incompatible changes). 20081117: Cleaned __init__.py, if you need Node you'll have to call it from it's original location (treebeard.models.Node instead of treebeard.Node). Also exceptions have been moved to treebeard.exceptions. 20100316: Queryset ordering in NS/MP trees is now enforced by the library. Previous ordering settings in META no longer work.