Merge remote-tracking branch 'upstream/master' into scheduled-publishing

Conflicts:
	wagtail/wagtailadmin/tests.py
	wagtail/wagtailadmin/views/pages.py
	wagtail/wagtailcore/models.py
This commit is contained in:
Serafeim Papastefanos 2014-05-29 18:59:56 +03:00
commit 18a4a30821
244 changed files with 6298 additions and 12348 deletions

1
.gitignore vendored
View file

@ -5,3 +5,4 @@
/MANIFEST
/wagtail.egg-info/
/docs/_build/
/.tox/

View file

@ -13,7 +13,7 @@ services:
install:
- python setup.py install
- pip install psycopg2 pyelasticsearch elasticutils==0.8.2 wand
- pip install coveralls
- pip install coveralls unittest2
# Pre-test configuration
before_script:
- psql -c 'create database wagtaildemo;' -U postgres

View file

@ -1,24 +1,36 @@
Changelog
=========
0.3 (xx.xx.20xx)
0.3 (28.05.2014)
~~~~~~~~~~~~~~~~
* Added toolbar to allow logged-in users to add and edit pages from the site front-end
* Support for alternative image processing backends such as Wand, via the WAGTAILIMAGES_BACKENDS setting
* Added support for generating static sites using django-medusa
* Added custom Query set for Pages with some handy methods for querying pages
* Added 'wagtailforms' module for creating form pages on a site, and handling form submissions
* Editor's guide documentation
* Expanded developer documentation
* Editor interface now outputs form media CSS / JS, to support custom widgets with assets
* Migrations and user management now correctly handle custom AUTH_USER_MODEL settings
* Added 'slugurl' template tag to output the URL of a page with a given slug
* MultiFieldPanel definitions now accept a 'classname' attribute, including a special classname of 'collapsible' to allow showing / hiding them on click
* Added 'insert_editor_css' and 'insert_editor_js' hooks for passing in custom CSS / JS to the editor interface
* Made JPEG compression level configurable through the IMAGE_COMPRESSION_QUALITY setting, and increased default to 85
* Added translation for Portuguese Brazil
* Added document_served signal which gets fired when a document is downloaded
* Added translations for Portuguese Brazil and Traditional Chinese (Taiwan).
* Made compatible with Python 2.6
* 'richtext' template filter now wraps output in <div class="rich-text"></div>, to assist in styling
* Embeds now save author_name and provider_name if set by oEmbed provider
* Fix: non-ASCII characters in image filenames are now converted into ASCII equivalents rather than becoming all underscores
* Fix: paths to fonts and images within CSS are no longer hard-coded to /static/
* Fix: Localization files for the JQuery UI datepicker are stored locally and only imported when a localization is known to be available
* Fix: Page slugs are now validated on page edit
* Fix: Filter objects are cached to avoid a database hit every time an {% image %} tag is compiled
* Fix: Moving or changing a site root page no longer causes URLs for subpages to change to 'None'
* Fix: Eliminated raw SQL queries from wagtailcore / wagtailadmin, to ensure cross-database compatibility
* Fix: Snippets menu item is hidden for administrators if no snippet types are defined
* Fix: 'Upload' tab in image chooser now retains focus if submit action returns a form error.
* Fix: Search input now appears on image chooser after form validation error.
0.2 (11.03.2014)
~~~~~~~~~~~~~~~~

View file

@ -24,6 +24,10 @@ Contributors
* v1kku
* Miguel Vieira
* Ben Emery
* David Smith
* Ben Margolis
* Tom Talbot
* Jeffrey Hearn
Translators
===========
@ -31,7 +35,7 @@ Translators
* Basque: Unai Zalakain
* Bulgarian: Lyuboslav Petrov
* Catalan: David Llop
* Chinese: Lihan Li
* Chinese: Lihan Li, tulpar008, wwj718
* French: Sylvain Fankhauser
* Galician: fooflare
* German: Karl Sander, Johannes Spielmann
@ -41,3 +45,4 @@ Translators
* Portuguese Brazil: Gilson Filho
* Romanian: Dan Braghis
* Spanish: Unai Zalakain, fooflare
* Traditional Chinese (Taiwan): wdv4758h

View file

@ -4,7 +4,7 @@
.. image:: https://coveralls.io/repos/torchbox/wagtail/badge.png?branch=master
:target: https://coveralls.io/r/torchbox/wagtail?branch=master
.. image:: https://pypip.in/v/wagtail/badge.png?asdf
.. image:: https://pypip.in/v/wagtail/badge.png?zxcv
:target: https://crate.io/packages/wagtail/
Wagtail CMS
@ -26,7 +26,7 @@ Wagtail is a Django content management system built originally for the `Royal Co
* Fast out of the box. `Varnish <https://www.varnish-cache.org/>`_-friendly if you need it
* Tests! But not enough; we're working hard to improve this
Find out more at `wagtail.io <http://wagtail.io/>`_. Documentation is at `wagtail.readthedocs.org <http://wagtail.readthedocs.org/>`_.
Find out more at `wagtail.io <http://wagtail.io/>`_.
Got a question? Ask it on our `Google Group <https://groups.google.com/forum/#!forum/wagtail>`_.
@ -36,6 +36,15 @@ Getting started
* See the `Getting Started <http://wagtail.readthedocs.org/en/latest/gettingstarted.html#getting-started>`_ docs for installation (with the demo app) on a fresh Debian/Ubuntu box with production-ready dependencies, on OS X and on a Vagrant box.
* `Serafeim Papastefanos <https://github.com/spapas>`_ has written a `tutorial <http://spapas.github.io/2014/02/13/wagtail-tutorial/>`_ with all the steps to build a simple Wagtail site from scratch.
Documentation
~~~~~~~~~~~~~
Available at `wagtail.readthedocs.org <http://wagtail.readthedocs.org/>`_. and always being updated.
Compatibility
~~~~~~~~~~~~~
Wagtail supports Django 1.6.2+ on Python 2.6 and 2.7. Django 1.7 and Python 3 support are in progress.
Contributing
~~~~~~~~~~~~
If you're a Python or Django developer, fork the repo and get stuck in! Send us a useful pull request and we'll post you a `t-shirt <https://twitter.com/WagtailCMS/status/432166799464210432/photo/1>`_. Our immediate priorities are better docs, more tests, internationalisation and localisation.

17
docs/advanced_topics.rst Normal file
View file

@ -0,0 +1,17 @@
Advanced Topics
~~~~~~~~~~~~~~~~
.. note::
This documentation is currently being written.
replacing image processing backend
custom image processing tags?
wagtail user bar custom CSS option?
extending hallo editor plugins with editor_js()
injecting any JS into page edit with editor_js()
Custom content module (same level as docs or images)

View file

@ -1,6 +0,0 @@
Building your site
==================
Serafeim Papastefanos has written a comprehensive tutorial on creating a site from scratch in Wagtail; for the time being, this is our recommended resource:
`spapas.github.io/2014/02/13/wagtail-tutorial/ <http://spapas.github.io/2014/02/13/wagtail-tutorial/>`_

View file

@ -0,0 +1,280 @@
For Django developers
=====================
.. note::
This documentation is currently being written.
Wagtail requires a little careful setup to define the types of content that you want to present through your website. The basic unit of content in Wagtail is the ``Page``, and all of your page-level content will inherit basic webpage-related properties from it. But for the most part, you will be defining content yourself, through the construction of Django models using Wagtail's ``Page`` as a base.
Wagtail organizes content created from your models in a tree, which can have any structure and combination of model objects in it. Wagtail doesn't prescribe ways to organize and interrelate your content, but here we've sketched out some strategies for organizing your models.
The presentation of your content, the actual webpages, includes the normal use of the Django template system. We'll cover additional functionality that Wagtail provides at the template level later on.
But first, we'll take a look at the ``Page`` class and model definitions.
The Page Class
~~~~~~~~~~~~~~
``Page`` uses Django's model interface, so you can include any field type and field options that Django allows. Wagtail provides some fields and editing handlers that simplify data entry in the Wagtail admin interface, so you may want to keep those in mind when deciding what properties to add to your models in addition to those already provided by ``Page``.
Built-in Properties of the Page Class
-------------------------------------
Wagtail provides some properties in the ``Page`` class which are common to most webpages. Since you'll be subclassing ``Page``, you don't have to worry about implementing them.
Public Properties
`````````````````
``title`` (string, required)
Human-readable title for the content
``slug`` (string, required)
Machine-readable URL component for this piece of content. The name of the page as it will appear in URLs e.g ``http://domain.com/blog/[my-slug]/``
``seo_title`` (string)
Alternate SEO-crafted title which overrides the normal title for use in the ``<head>`` of a page
``search_description`` (string)
A SEO-crafted description of the content, used in both internal search indexing and for the meta description read by search engines
The ``Page`` class actually has alot more to it, but these are probably the only built-in properties you'll need to worry about when creating templates for your models.
Anatomy of a Wagtail Model
~~~~~~~~~~~~~~~~~~~~~~~~~~
So what does a Wagtail model definition look like? Here's a model representing a typical blog post:
.. code-block:: python
from django.db import models
from wagtail.wagtailcore.models import Page
from wagtail.wagtailcore.fields import RichTextField
from wagtail.wagtailadmin.edit_handlers import FieldPanel
from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
from wagtail.wagtailimages.models import Image
class BlogPage(Page):
body = RichTextField()
date = models.DateField("Post date")
feed_image = models.ForeignKey(
'wagtailimages.Image',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+'
)
BlogPage.content_panels = [
FieldPanel('title', classname="full title"),
FieldPanel('date'),
FieldPanel('body', classname="full"),
]
BlogPage.promote_panels = [
FieldPanel('slug'),
FieldPanel('seo_title'),
FieldPanel('show_in_menus'),
FieldPanel('search_description'),
ImageChooserPanel('feed_image'),
]
To keep track of your ``Page``-derived models, it might be helpful to include "Page" as the last part of your class name. ``BlogPage`` defines three properties: ``body``, ``date``, and ``feed_image``. These are a mix of basic Django models (``DateField``), Wagtail fields (``RichTextField``), and a pointer to a Wagtail model (``Image``).
Next, the ``content_panels`` and ``promote_panels`` lists define the capabilities and layout of the Wagtail admin page edit interface. The lists are filled with "panels" and "choosers", which will provide a fine-grain interface for inputting the model's content. The ``ImageChooserPanel``, for instance, lets one browse the image library, upload new images, and input image metadata. The ``RichTextField`` is the basic field for creating web-ready website rich text, including text formatting and embedded media like images and video. The Wagtail admin offers other choices for fields, Panels, and Choosers, with the option of creating your own to precisely fit your content without workarounds or other compromises.
Your models may be even more complex, with methods overriding the built-in functionality of the ``Page`` to achieve webdev magic. Or, you can keep your models simple and let Wagtail's built-in functionality do the work.
Now that we have a basic idea of how our content is defined, lets look at relationships between pieces of content.
Introduction to Trees
~~~~~~~~~~~~~~~~~~~~~
If you're unfamiliar with trees as an abstract data type, you might want to `review the concepts involved. <http://en.wikipedia.org/wiki/Tree_(data_structure)>`_
As a web developer, though, you probably already have a good understanding of trees as filesystem directories or paths. Wagtail pages can create the same structure, as each page in the tree has its own URL path, like so::
/
people/
nien-nunb/
laura-roslin/
events/
captain-picard-day/
winter-wrap-up/
The Wagtail admin interface uses the tree to organize content for editing, letting you navigate up and down levels in the tree through its Explorer menu. This method of organization is a good place to start in thinking about your own Wagtail models.
Nodes and Leaves
----------------
It might be handy to think of the ``Page``-derived models you want to create as being one of two node types: parents and leaves. Wagtail isn't prescriptive in this approach, but it's a good place to start if you're not experienced in structuring your own content types.
Nodes
`````
Parent nodes on the Wagtail tree probably want to organize and display a browse-able index of their descendants. A blog, for instance, needs a way to show a list of individual posts.
A Parent node could provide its own function returning its descendant objects.
.. code-block:: python
class EventPageIndex(Page):
# ...
def events(self):
# Get list of live event pages that are descendants of this page
events = EventPage.objects.live().descendant_of(self)
# Filter events list to get ones that are either
# running now or start in the future
events = events.filter(date_from__gte=date.today())
# Order by date
events = events.order_by('date_from')
return events
This example makes sure to limit the returned objects to pieces of content which make sense, specifically ones which have been published through Wagtail's admin interface (``live()``) and are children of this node (``descendant_of(self)``). By setting a ``subpage_types`` class property in your model, you can specify which models are allowed to be set as children, but Wagtail will allow any ``Page``-derived model by default. Regardless, it's smart for a parent model to provide an index filtered to make sense.
Leaves
``````
Leaves are the pieces of content itself, a page which is consumable, and might just consist of a bunch of properties. A blog page leaf might have some body text and an image. A person page leaf might have a photo, a name, and an address.
It might be helpful for a leaf to provide a way to back up along the tree to a parent, such as in the case of breadcrumbs navigation. The tree might also be deep enough that a leaf's parent won't be included in general site navigation.
The model for the leaf could provide a function that traverses the tree in the opposite direction and returns an appropriate ancestor:
.. code-block:: python
class EventPage(Page):
# ...
def event_index(self):
# Find closest ancestor which is an event index
return self.get_ancestors().type(EventIndexPage).last()
If defined, ``subpage_types`` will also limit the parent models allowed to contain a leaf. If not, Wagtail will allow any combination of parents and leafs to be associated in the Wagtail tree. Like with index pages, it's a good idea to make sure that the index is actually of the expected model to contain the leaf.
Other Relationships
```````````````````
Your ``Page``-derived models might have other interrelationships which extend the basic Wagtail tree or depart from it entirely. You could provide functions to navigate between siblings, such as a "Next Post" link on a blog page (``post->post->post``). It might make sense for subtrees to interrelate, such as in a discussion forum (``forum->post->replies``) Skipping across the hierarchy might make sense, too, as all objects of a certain model class might interrelate regardless of their ancestors (``events = EventPage.objects.all``). It's largely up to the models to define their interrelations, the possibilities are really endless.
Anatomy of a Wagtail Request
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
For going beyond the basics of model definition and interrelation, it might help to know how Wagtail handles requests and constructs responses. In short, it goes something like:
#. Django gets a request and routes through Wagtail's URL dispatcher definitions
#. Starting from the root content piece, Wagtail traverses the page tree, letting the model for each piece of content along the path decide how to ``route()`` the next step in the path.
#. A model class decides that routing is done and it's now time to ``serve()`` content.
#. ``serve()`` constructs a context using ``get_context()``
#. ``serve()`` finds a template to pass it to using ``get_template()``
#. A response object is returned by ``serve()`` and Django responds to the requester.
You can apply custom behavior to this process by overriding ``Page`` class methods such as ``route()`` and ``serve()`` in your own models. For examples, see :ref:`model_recipes`.
Page Properties and Methods Reference
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In addition to the model fields provided, ``Page`` has many properties and methods that you may wish to reference, use, or override in creating your own models. Those listed here are relatively straightforward to use, but consult the Wagtail source code for a full view of what's possible.
Properties:
* specific
* url
* full_url
* relative_url
* has_unpublished_changes
* status_string
* subpage_types
* indexed_fields
Methods:
* route
* serve
* get_context
* get_template
* is_navigable
* get_other_siblings
* get_ancestors
* get_descendants
* get_siblings
* search
* get_page_modes
* show_as_mode
Page Queryset Methods
~~~~~~~~~~~~~~~~~~~~~
The ``Page`` class uses a custom Django model manager which provides these methods for structuring queries on ``Page`` objects.
get_query_set()
return PageQuerySet(self.model).order_by('path')
live(self):
return self.get_query_set().live()
not_live(self):
return self.get_query_set().not_live()
page(self, other):
return self.get_query_set().page(other)
not_page(self, other):
return self.get_query_set().not_page(other)
descendant_of(self, other, inclusive=False):
return self.get_query_set().descendant_of(other, inclusive)
not_descendant_of(self, other, inclusive=False):
return self.get_query_set().not_descendant_of(other, inclusive)
child_of(self, other):
return self.get_query_set().child_of(other)
not_child_of(self, other):
return self.get_query_set().not_child_of(other)
ancestor_of(self, other, inclusive=False):
return self.get_query_set().ancestor_of(other, inclusive)
not_ancestor_of(self, other, inclusive=False):
return self.get_query_set().not_ancestor_of(other, inclusive)
parent_of(self, other):
return self.get_query_set().parent_of(other)
not_parent_of(self, other):
return self.get_query_set().not_parent_of(other)
sibling_of(self, other, inclusive=False):
return self.get_query_set().sibling_of(other, inclusive)
not_sibling_of(self, other, inclusive=False):
return self.get_query_set().not_sibling_of(other, inclusive)
type(self, model):
return self.get_query_set().type(model)
not_type(self, model):
return self.get_query_set().not_type(model)
Site
~~~~
Django's built-in admin interface provides the way to map a "site" (hostname or domain) to any node in the wagtail tree, using that node as the site's root.
Access this by going to ``/django-admin/`` and then "Home Wagtailcore Sites." To try out a development site, add a single site with the hostname ``localhost`` at port ``8000`` and map it to one of the pieces of content you have created.
Wagtail's developers plan to move the site settings into the Wagtail admin interface.

View file

@ -0,0 +1,186 @@
For Front End developers
========================
.. note::
This documentation is currently being written.
========================
Overview
========================
Wagtail uses Django's templating language. For developers new to Django, start with Django's own template documentation:
https://docs.djangoproject.com/en/dev/topics/templates/
Python programmers new to Django/Wagtail may prefer more technical documentation:
https://docs.djangoproject.com/en/dev/ref/templates/api/
==========================
Displaying Pages
==========================
Template Location
~~~~~~~~~~~~~~~~~
For each of your ``Page``-derived models, Wagtail will look for a template in the following location, relative to your project root::
project/
app/
templates/
app/
blog_index_page.html
models.py
Class names are converted from camel case to underscores. For example, the template for model class ``BlogIndexPage`` would be assumed to be ``blog_index_page.html``. For more information, see the Django documentation for the `application directories template loader`_.
.. _application directories template loader: https://docs.djangoproject.com/en/dev/ref/templates/api/
Self
~~~~
By default, the context passed to a model's template consists of two properties: ``self`` and ``request``. ``self`` is the model object being displayed. ``request`` is the normal Django request object. So, to include the title of a ``Page``, use ``{{ self.title }}``.
========================
Static files (css, js, images)
========================
Images
~~~~~~
Images uploaded to Wagtail go into the image library and from there are added to pages via the :doc:`page editor interface </editor_manual/new_pages/inserting_images>`.
Unlike other CMS, adding images to a page does not involve choosing a "version" of the image to use. Wagtail has no predefined image "formats" or "sizes". Instead the template developer defines image manipulation to occur *on the fly* when the image is requested, via a special syntax within the template.
Images from the library **must** be requested using this syntax, but images in your codebase can be added via conventional means e.g ``img`` tags. Only images from the library can be manipulated on the fly.
Read more about the image manipulation syntax here :ref:`image_tag`.
========================
Template tags & filters
========================
In addition to Django's standard tags and filters, Wagtail provides some of it's own, which can be ``load``-ed `as you would any other <https://docs.djangoproject.com/en/dev/topics/templates/#custom-tag-and-filter-libraries>`_
.. _image_tag:
Images (tag)
~~~~~~~~~~~~
The syntax for displaying/manipulating an image is thus::
{% image [image] [method]-[dimension(s)] %}
For example::
{% image self.photo width-400 %}
<!-- or a square thumbnail: -->
{% image self.photo fill-80x80 %}
The ``image`` is the Django object refering to the image. If your page model defined a field called "photo" then ``image`` would probably be ``self.photo``. The ``method`` defines which resizing algorithm to use and ``dimension(s)`` provides height and/or width values (as ``[width|height]`` or ``[width]x[height]``) to refine that algorithm.
Note that a space separates ``image`` and ``method``, but not ``method`` and ``dimensions``: a hyphen between ``method`` and ``dimensions`` is mandatory. Multiple dimensions must be separated by an ``x``.
The available ``method`` s are:
.. glossary::
``max``
(takes two dimensions)
Fit **within** the given dimensions.
The longest edge will be reduced to the equivalent dimension size defined. e.g A portrait image of width 1000, height 2000, treated with the ``max`` dimensions ``1000x500`` (landscape) would result in the image shrunk so the *height* was 500 pixels and the width 250.
``min``
(takes two dimensions)
**Cover** the given dimensions.
This may result in an image slightly **larger** than the dimensions you specify. e.g A square image of width 2000, height 2000, treated with the ``min`` dimensions ``500x200`` (landscape) would have it's height and width changed to 500, i.e matching the width required, but greater than the height.
``width``
(takes one dimension)
Reduces the width of the image to the dimension specified.
``height``
(takes one dimension)
Resize the height of the image to the dimension specified..
``fill``
(takes two dimensions)
Resize and **crop** to fill the **exact** dimensions.
This can be particularly useful for websites requiring square thumbnails of arbitrary images. e.g A landscape image of width 2000, height 1000, treated with ``fill`` dimensions ``200x200`` would have it's height reduced to 200, then it's width (ordinarily 400) cropped to 200.
**The crop always aligns on the centre of the image.**
.. Note::
Wagtail *does not allow deforming or stretching images*. Image dimension ratios will always be kept. Wagtail also *does not support upscaling*. Small images forced to appear at larger sizes will "max out" at their their native dimensions.
To request the "original" version of an image, it is suggested you rely on the lack of upscaling support by requesting an image much larger than it's maximum dimensions. e.g to insert an image who's dimensions are uncertain/unknown, at it's maximum size, try: ``{% image self.image width-10000 %}``. This assumes the image is unlikely to be larger than 10000px wide.
.. _rich-text-filter:
Rich text (filter)
~~~~~~~~~~~~~~~~~~
This filter is required for use with any ``RichTextField``. It will expand internal shorthand references to embeds and links made in the Wagtail editor into fully-baked HTML ready for display. **Note that the template tag loaded differs from the name of the filter.**
.. code-block:: django
{% load rich_text %}
...
{{ body|richtext }}
Internal links (tag)
~~~~~~~~~~~~~~~~~~~~
**pageurl**
Takes a ``Page``-derived object and returns its URL as relative (``/foo/bar/``) if it's within the same site as the current page, or absolute (``http://example.com/foo/bar/``) if not.
.. code-block:: django
{% load pageurl %}
...
<a href="{% pageurl blog %}">
**slugurl**
Takes a ``slug`` string and returns the URL for the ``Page``-derived object with that slug. Like ``pageurl``, will try to provide a relative link if possible, but will default to an absolute link if on a different site.
.. code-block:: django
{% load slugurl %}
...
<a href="{% slugurl blogslug %}">
Static files (tag)
~~~~~~~~~~~~~~
Misc
~~~~~~~~~~
========================
Wagtail User Bar
========================
This tag provides a Wagtail icon and flyout menu on the top-right of a page for a logged-in user with editing capabilities, with the option of editing the current Page-derived object or adding a new sibling object.
.. code-block:: django
{% load wagtailuserbar %}
...
{% wagtailuserbar %}

View file

@ -0,0 +1,11 @@
Building your site
==================
.. note::
This documentation is currently incomplete.
.. toctree::
:maxdepth: 3
djangodevelopers
frontenddevelopers

322
docs/editing_api.rst Normal file
View file

@ -0,0 +1,322 @@
Editing API
===========
.. note::
This documentation is currently being written.
Wagtail provides a highly-customizable editing interface consisting of several components:
* **Fields** — built-in content types to augment the basic types provided by Django.
* **Panels** — the basic editing blocks for fields, groups of fields, and related object clusters
* **Choosers** — interfaces for finding related objects in a ForeignKey relationship
Configuring your models to use these components will shape the Wagtail editor to your needs. Wagtail also provides an API for injecting custom CSS and Javascript for further customization, including extending the hallo.js rich text editor.
There is also an Edit Handler API for creating your own Wagtail editor components.
Defining Panels
~~~~~~~~~~~~~~~
A "panel" is the basic editing block in Wagtail. Wagtail will automatically pick the appropriate editing widget for most Django field types, you just need to add a panel for each field you want to show in the Wagtail page editor, in the order you want them to appear.
There are three types of panels:
``FieldPanel( field_name, classname=None )``
This is the panel used for basic Django field types. ``field_name`` is the name of the class property used in your model definition. ``classname`` is a string of optional CSS classes given to the panel which are used in formatting and scripted interactivity. By default, panels are formatted as inset fields. The CSS class ``full`` can be used to format the panel so it covers the full width of the Wagtail page editor. The CSS class ``title`` can be used to mark a field as the source for auto-generated slug strings.
``MultiFieldPanel( children, heading="", classname=None )``
This panel condenses several ``FieldPanel`` s or choosers, from a list or tuple, under a single ``heading`` string.
``InlinePanel( base_model, relation_name, panels=None, label='', help_text='' )``
This panel allows for the creation of a "cluster" of related objects over a join to a separate model, such as a list of related links or slides to an image carousel. This is a very powerful, but tricky feature which will take some space to cover, so we'll skip over it for now. For a full explanation on the usage of ``InlinePanel``, see :ref:`inline_panels`.
Wagtail provides a tabbed interface to help organize panels. ``content_panels`` is the main tab, used for the meat of your model content. The other, ``promote_panels``, is suggested for organizing metadata about the content, such as SEO information and other machine-readable information. Since you're writing the panel definitions, you can organize them however you want.
Let's look at an example of a panel definition:
.. code-block:: python
COMMON_PANELS = (
FieldPanel('slug'),
FieldPanel('seo_title'),
FieldPanel('show_in_menus'),
FieldPanel('search_description'),
)
...
class ExamplePage( Page ):
# field definitions omitted
...
ExamplePage.content_panels = [
FieldPanel('title', classname="full title"),
FieldPanel('body', classname="full"),
FieldPanel('date'),
ImageChooserPanel('splash_image'),
DocumentChooserPanel('free_download'),
PageChooserPanel('related_page'),
]
ExamplePage.promote_panels = [
MultiFieldPanel(COMMON_PANELS, "Common page configuration"),
]
After the ``Page``-derived class definition, just add lists of panel definitions to order and organize the Wagtail page editing interface for your model.
Built-in Fields and Choosers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Django's field types are automatically recognized and provided with an appropriate widget for input. Just define that field the normal Django way and pass the field name into ``FieldPanel()`` when defining your panels. Wagtail will take care of the rest.
Here are some Wagtail-specific types that you might include as fields in your models.
Rich Text (HTML)
----------------
Wagtail provides a general-purpose WYSIWYG editor for creating rich text content (HTML) and embedding media such as images, video, and documents. To include this in your models, use the ``RichTextField()`` function when defining a model field:
.. code-block:: python
from wagtail.wagtailcore.fields import RichTextField
from wagtail.wagtailadmin.edit_handlers import FieldPanel
# ...
class BookPage(Page):
book_text = RichTextField()
BookPage.content_panels = [
FieldPanel('body', classname="full"),
# ...
]
``RichTextField`` inherits from Django's basic ``TextField`` field, so you can pass any field parameters into ``RichTextField`` as if using a normal Django field. This field does not need a special panel and can be defined with ``FieldPanel``.
However, template output from ``RichTextField`` is special and need to be filtered to preserve embedded content. See :ref:`rich-text-filter`.
If you're interested in extending the capabilities of the Wagtail WYSIWYG editor (hallo.js), See :ref:`extending_wysiwyg`.
Images
------
One of the features of Wagtail is a unified image library, which you can access in your models through the ``Image`` model and the ``ImageChooserPanel`` chooser. Here's how:
.. code-block:: python
from wagtail.wagtailimages.models import Image
from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
# ...
class BookPage(Page):
cover = models.ForeignKey(
'wagtailimages.Image',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+'
)
BookPage.content_panels = [
ImageChooserPanel('cover'),
# ...
]
Django's default behavior is to "cascade" deletions through a ForeignKey relationship, which is probably not what you want happening. This is why the ``null``, ``blank``, and ``on_delete`` parameters should be set to allow for an empty field. (See `Django model field reference (on_delete)`_ ). ``ImageChooserPanel`` takes only one argument: the name of the field.
.. _Django model field reference (on_delete): https://docs.djangoproject.com/en/dev/ref/models/fields/#django.db.models.ForeignKey.on_delete
Displaying ``Image`` objects in a template requires the use of a template tag. See :ref:`image_tag`.
Documents
---------
For files in other formats, Wagtail provides a generic file store through the ``Document`` model:
.. code-block:: python
from wagtail.wagtaildocs.models import Document
from wagtail.wagtaildocs.edit_handlers import DocumentChooserPanel
# ...
class BookPage(Page):
book_file = models.ForeignKey(
'wagtaildocs.Document',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+'
)
BookPage.content_panels = [
DocumentChooserPanel('book_file'),
# ...
]
As with images, Wagtail documents should also have the appropriate extra parameters to prevent cascade deletions across a ForeignKey relationship. ``DocumentChooserPanel`` takes only one argument: the name of the field.
Documents can be used directly in templates without tags or filters. Its properties are:
.. glossary::
``title``
The title of the document.
``url``
URL to the file.
``created_at``
The date and time the document was created (DateTime).
``filename``
The filename of the file.
``file_extension``
The extension of the file.
``tags``
A ``TaggableManager`` which keeps track of tags associated with the document (uses the ``django-taggit`` module).
Pages and Page-derived Models
-----------------------------
You can explicitly link ``Page``-derived models together using the ``Page`` model and ``PageChooserPanel``.
.. code-block:: python
from wagtail.wagtailcore.models import Page
from wagtail.wagtailadmin.edit_handlers import PageChooserPanel
# ...
class BookPage(Page):
publisher = models.ForeignKey(
'wagtailcore.Page',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
)
BookPage.content_panels = [
PageChooserPanel('related_page', 'demo.PublisherPage'),
# ...
]
``PageChooserPanel`` takes two arguments: a field name and an optional page type. Specifying a page type (in the form of an ``"appname.modelname"`` string) will filter the chooser to display only pages of that type.
Snippets
--------
Snippets are not subclasses, so you must include the model class directly. A chooser is provided which takes the field name snippet class.
.. code-block:: python
from wagtail.wagtailsnippets.edit_handlers import SnippetChooserPanel
# ...
class BookPage(Page):
advert = models.ForeignKey(
'demo.Advert',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+'
)
BookPage.content_panels = [
SnippetChooserPanel('advert', Advert),
# ...
]
See :ref:`snippets` for more information.
Field Customization
~~~~~~~~~~~~~~~~~~~
By adding CSS classnames to your panel definitions or adding extra parameters to your field definitions, you can control much of how your fields will display in the Wagtail page editing interface. Wagtail's page editing interface takes much of its behavior from Django's admin, so you may find many options for customization covered there. (See `Django model field reference`_ ).
.. _Django model field reference:https://docs.djangoproject.com/en/dev/ref/models/fields/
Full-Width Input
----------------
Use ``classname="full"`` to make a field (input element) stretch the full width of the Wagtail page editor. This will not work if the field is encapsulated in a ``MultiFieldPanel``, which places its child fields into a formset.
Required Fields
---------------
To make input or chooser selection manditory for a field, add ``blank=False`` to its model definition. (See `Django model field reference (blank)`_ ).
.. _Django model field reference (blank): https://docs.djangoproject.com/en/dev/ref/models/fields/#django.db.models.Field.blank
Hiding Fields
-------------
Without a panel definition, a default form field (without label) will be used to represent your fields. If you intend to hide a field on the Wagtail page editor, define the field with ``editable=False`` (See `Django model field reference (editable)`_ ).
.. _Django model field reference (editable): https://docs.djangoproject.com/en/dev/ref/models/fields/#editable
MultiFieldPanel
~~~~~~~~~~~~~~~
.. code-block:: python
BOOK_FIELD_COLLECTION = [
ImageChooserPanel('cover'),
DocumentChooserPanel('book_file'),
PageChooserPanel('publisher'),
]
BookPage.content_panels = [
MultiFieldPanel(
BOOK_FIELD_COLLECTION,
heading="Collection of Book Fields",
classname="collapsible collapsed"
),
# ...
]
.. _inline_panels:
Inline Panels and Model Clusters
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``django-modelcluster`` module allows for streamlined relation of extra models to a Wagtail page.
.. _extending_wysiwyg:
Extending the WYSIWYG Editor (hallo.js)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Adding hallo.js plugins:
https://github.com/torchbox/wagtail/commit/1ecc215759142e6cafdacb185bbfd3f8e9cd3185
Edit Handler API
~~~~~~~~~~~~~~~~

View file

@ -1,9 +1,10 @@
Using Wagtail: an Editor's guide
================================
This section of the documentation is written for the users of a Wagtail-powered site. That is, the content editors, moderators and administrators who will be running things on a day-to-day basis.
.. note::
Documentation currently incomplete and in draft status
**NOTE:** This section of the documentation is currently in draft status.
This section of the documentation is written for the users of a Wagtail-powered site. That is, the content editors, moderators and administrators who will be running things on a day-to-day basis.
.. toctree::
:maxdepth: 3

69
docs/form_builder.rst Normal file
View file

@ -0,0 +1,69 @@
Form builder
============
The `wagtailforms` module allows you to set up single-page forms, such as a 'Contact us' form, as pages of a Wagtail site. It provides a set of base models that site implementors can extend to create their own 'Form' page type with their own site-specific templates. Once a page type has been set up in this way, editors can build forms within the usual page editor, consisting of any number of fields. Form submissions are stored for later retrieval through a new 'Forms' section within the Wagtail admin interface; in addition, they can be optionally e-mailed to an address specified by the editor.
Usage
~~~~~
Add 'wagtail.wagtailforms' to your INSTALLED_APPS:
.. code:: python
INSTALLED_APPS = [
...
'wagtail.wagtailforms',
]
Within the models.py of one of your apps, create a model that extends wagtailforms.models.AbstractEmailForm:
.. code:: python
from wagtail.wagtailforms.models import AbstractEmailForm, AbstractFormField
class FormField(AbstractFormField):
page = ParentalKey('FormPage', related_name='form_fields')
class FormPage(AbstractEmailForm):
intro = RichTextField(blank=True)
thank_you_text = RichTextField(blank=True)
FormPage.content_panels = [
FieldPanel('title', classname="full title"),
FieldPanel('intro', classname="full"),
InlinePanel(FormPage, 'form_fields', label="Form fields"),
FieldPanel('thank_you_text', classname="full"),
MultiFieldPanel([
FieldPanel('to_address', classname="full"),
FieldPanel('from_address', classname="full"),
FieldPanel('subject', classname="full"),
], "Email")
]
AbstractEmailForm defines the fields 'to_address', 'from_address' and 'subject', and expects form_fields to be defined. Any additional fields are treated as ordinary page content - note that FormPage is responsible for serving both the form page itself and the landing page after submission, so the model definition should include all necessary content fields for both of those views.
If you do not want your form page type to offer form-to-email functionality, you can inherit from AbstractForm instead of AbstractEmailForm, and omit the 'to_address', 'from_address' and 'subject' fields from the content_panels definition.
You now need to create two templates named form_page.html and form_page_landing.html (where 'form_page' is the underscore-formatted version of the class name). form_page.html differs from a standard Wagtail template in that it is passed a variable 'form', containing a Django form object, in addition to the usual 'self' variable. A very basic template for the form would thus be:
.. code:: html
{% load pageurl rich_text %}
<html>
<head>
<title>{{ self.title }}</title>
</head>
<body>
<h1>{{ self.title }}</h1>
{{ self.intro|richtext }}
<form action="{% pageurl self %}" method="POST">
{% csrf_token %}
{{ form.as_p }}
<input type="submit">
</form>
</body>
</html>
form_page_landing.html is a regular Wagtail template, displayed after the user makes a successful form submission.

View file

@ -4,7 +4,7 @@ Getting Started
On Ubuntu
~~~~~~~~~
If you have a fresh instance of Ubuntu 13.04 or 13.10, you can install Wagtail,
If you have a fresh instance of Ubuntu 13.04 or later, you can install Wagtail,
along with a demonstration site containing a set of standard templates and page
types, in one step. As the root user::
@ -102,12 +102,17 @@ Required dependencies
=====================
- `pip <https://github.com/pypa/pip>`_
- `libjpeg <http://ijg.org/>`_
- `libxml2 <http://xmlsoft.org/>`_
- `libxslt <http://xmlsoft.org/XSLT/>`_
- `zlib <http://www.zlib.net/>`_
Optional dependencies
=====================
- `PostgreSQL`_
- `Elasticsearch`_
- `Redis`_
Installation
============
@ -137,6 +142,7 @@ with a regular Django project.
.. _the Wagtail codebase: https://github.com/torchbox/wagtail
.. _PostgreSQL: http://www.postgresql.org
.. _Elasticsearch: http://www.elasticsearch.org
.. _Redis: http://redis.io/
_`Remove the demo app`
~~~~~~~~~~~~~~~~~~~~~~
@ -160,4 +166,4 @@ Once you've experimented with the demo app and are ready to build your pages via
COMMIT;
EOF
rm -r demo media/images/* media/original_images/*
perl -pi -e"s/('demo',|WAGTAILSEARCH_RESULTS_TEMPLATE)/#\1/" $PROJECT/settingsbase.py
perl -pi -e"s/('demo',|WAGTAILSEARCH_RESULTS_TEMPLATE)/#\1/" $PROJECT/settings/base.py

View file

@ -3,14 +3,22 @@ Welcome to Wagtail's documentation
Wagtail is a modern, flexible CMS, built on Django.
It supports Django 1.6.2+ on Python 2.6 and 2.7. Django 1.7 and Python 3 support are in progress.
.. toctree::
:maxdepth: 3
gettingstarted
building_your_site
building_your_site/index
editing_api
snippets
wagtail_search
form_builder
model_recipes
advanced_topics
deploying
performance
static_site_generation
contributing
support
roadmap

199
docs/model_recipes.rst Normal file
View file

@ -0,0 +1,199 @@
.. _model_recipes:
Model Recipes
=============
Overriding the serve() Method
-----------------------------
Wagtail defaults to serving ``Page``-derived models by passing ``self`` to a Django HTML template matching the model's name, but suppose you wanted to serve something other than HTML? You can override the ``serve()`` method provided by the ``Page`` class and handle the Django request and response more directly.
Consider this example from the Wagtail demo site's ``models.py``, which serves an ``EventPage`` object as an iCal file if the ``format`` variable is set in the request:
.. code-block:: python
class EventPage(Page):
...
def serve(self, request):
if "format" in request.GET:
if request.GET['format'] == 'ical':
# Export to ical format
response = HttpResponse(
export_event(self, 'ical'),
content_type='text/calendar',
)
response['Content-Disposition'] = 'attachment; filename=' + self.slug + '.ics'
return response
else:
# Unrecognised format error
message = 'Could not export event\n\nUnrecognised format: ' + request.GET['format']
return HttpResponse(message, content_type='text/plain')
else:
# Display event page as usual
return super(EventPage, self).serve(request)
``serve()`` takes a Django request object and returns a Django response object. Wagtail returns a ``TemplateResponse`` object with the template and context which it generates, which allows middleware to function as intended, so keep in mind that a simpler response object like a ``HttpResponse`` will not receive these benefits.
With this strategy, you could use Django or Python utilities to render your model in JSON or XML or any other format you'd like.
Adding Endpoints with Custom route() Methods
--------------------------------------------
Wagtail routes requests by iterating over the path components (separated with a forward slash ``/``), finding matching objects based on their slug, and delegating further routing to that object's model class. The Wagtail source is very instructive in figuring out what's happening. This is the default ``route()`` method of the ``Page`` class:
.. code-block:: python
class Page(...):
...
def route(self, request, path_components):
if path_components:
# request is for a child of this page
child_slug = path_components[0]
remaining_components = path_components[1:]
# find a matching child or 404
try:
subpage = self.get_children().get(slug=child_slug)
except Page.DoesNotExist:
raise Http404
# delegate further routing
return subpage.specific.route(request, remaining_components)
else:
# request is for this very page
if self.live:
# use the serve() method to render the request if the page is published
return self.serve(request)
else:
# the page matches the request, but isn't published, so 404
raise Http404
The contract is pretty simple. ``route()`` takes the current object (``self``), the ``request`` object, and a list of the remaining ``path_components`` from the request URL. It either continues delegating routing by calling ``route()`` again on one of its children in the Wagtail tree, or ends the routing process by serving something -- either normally through the ``self.serve()`` method or by raising a 404 error.
By overriding the ``route()`` method, we could create custom endpoints for each object in the Wagtail tree. One use case might be using an alternate template when encountering the ``print/`` endpoint in the path. Another might be a REST API which interacts with the current object. Just to see what's involved, lets make a simple model which prints out all of its child path components.
First, ``models.py``:
.. code-block:: python
from django.shortcuts import render
...
class Echoer(Page):
def route(self, request, path_components):
if path_components:
return render(request, self.template, {
'self': self,
'echo': ' '.join(path_components),
})
else:
if self.live:
return self.serve(request)
else:
raise Http404
Echoer.content_panels = [
FieldPanel('title', classname="full title"),
]
Echoer.promote_panels = [
MultiFieldPanel(COMMON_PANELS, "Common page configuration"),
]
This model, ``Echoer``, doesn't define any properties, but does subclass ``Page`` so objects will be able to have a custom title and slug. The template just has to display our ``{{ echo }}`` property. We're skipping the ``serve()`` method entirely, but you could include your render code there to stay consistent with Wagtail's conventions.
Now, once creating a new ``Echoer`` page in the Wagtail admin titled "Echo Base," requests such as::
http://127.0.0.1:8000/echo-base/tauntaun/kennel/bed/and/breakfast/
Will return::
tauntaun kennel bed and breakfast
Tagging
-------
Wagtail provides tagging capability through the combination of two django modules, ``taggit`` and ``modelcluster``. ``taggit`` provides a model for tags which is extended by ``modelcluster``, which in turn provides some magical database abstraction which makes drafts and revisions possible in Wagtail. It's a tricky recipe, but the net effect is a many-to-many relationship between your model and a tag class reserved for your model.
Using an example from the Wagtail demo site, here's what the tag model and the relationship field looks like in ``models.py``:
.. code-block:: python
from modelcluster.fields import ParentalKey
from modelcluster.tags import ClusterTaggableManager
from taggit.models import Tag, TaggedItemBase
...
class BlogPageTag(TaggedItemBase):
content_object = ParentalKey('demo.BlogPage', related_name='tagged_items')
...
class BlogPage(Page):
...
tags = ClusterTaggableManager(through=BlogPageTag, blank=True)
BlogPage.promote_panels = [
...
FieldPanel('tags'),
]
Wagtail's admin provides a nice interface for inputting tags into your content, with typeahead tag completion and friendly tag icons.
Now that we have the many-to-many tag relationship in place, we can fit in a way to render both sides of the relation. Here's more of the Wagtail demo site ``models.py``, where the index model for ``BlogPage`` is extended with logic for filtering the index by tag:
.. code-block:: python
class BlogIndexPage(Page):
...
def serve(self, request):
# Get blogs
blogs = self.blogs
# Filter by tag
tag = request.GET.get('tag')
if tag:
blogs = blogs.filter(tags__name=tag)
return render(request, self.template, {
'self': self,
'blogs': blogs,
})
Here, ``blogs.filter(tags__name=tag)`` invokes a reverse Django queryset filter on the ``BlogPageTag`` model to optionally limit the ``BlogPage`` objects sent to the template for rendering. Now, lets render both sides of the relation by showing the tags associated with an object and a way of showing all of the objects associated with each tag. This could be added to the ``blog_page.html`` template:
.. code-block:: django
{% for tag in self.tags.all %}
<a href="{% pageurl self.blog_index %}?tag={{ tag }}">{{ tag }}</a>
{% endfor %}
Iterating through ``self.tags.all`` will display each tag associated with ``self``, while the link(s) back to the index make use of the filter option added to the ``BlogIndexPage`` model. A Django query could also use the ``tagged_items`` related name field to get ``BlogPage`` objects associated with a tag.
This is just one possible way of creating a taxonomy for Wagtail objects. With all of the components for a taxonomy available through Wagtail, you should be able to fulfill even the most exotic taxonomic schemes.
Custom Page Contexts by Overriding get_context()
------------------------------------------------
Load Alternate Templates by Overriding get_template()
-----------------------------------------------------
Page Modes
----------
get_page_modes
show_as_mode

View file

@ -10,7 +10,7 @@ https://raw.github.com/torchbox/wagtail/master/CHANGELOG.txt
In summary:
* February 2013: Reduced dependencies, basic documentation, translations, tests
* February 2014: Reduced dependencies, basic documentation, translations, tests
What's next
~~~~~~~~~~~
@ -19,12 +19,10 @@ The `issue list <https://github.com/torchbox/wagtail/issues>`_ gives a detailed
* More and better tests (>80% `coverage <https://coveralls.io/r/torchbox/wagtail>`_)
* Better documentation: simple setup guides for all levels of user, a manual for editors and administrators, in-depth intstructions for Django developers.
* A form builder
* Move site section permissions out of Django admin
* Improved image handling: intelligent cropping, animated gif support
* Block-level editing UI (see `Sir Trevor <http://madebymany.github.io/sir-trevor-js/>`_)
* Site settings management
* Edit bird for logged-in visitors
* Support for an HTML content type
* Simple inline stats

143
docs/snippets.rst Normal file
View file

@ -0,0 +1,143 @@
.. _snippets:
Snippets
========
Snippets are pieces of content which do not necessitate a full webpage to render. They could be used for making secondary content, such as headers, footers, and sidebars, editable in the Wagtail admin. Snippets are models which do not inherit the ``Page`` class and are thus not organized into the Wagtail tree, but can still be made editable by assigning panels and identifying the model as a snippet with ``register_snippet()``.
Snippets are not search-able or order-able in the Wagtail admin, so decide carefully if the content type you would want to build into a snippet might be more suited to a page.
Snippet Models
--------------
Here's an example snippet from the Wagtail demo website:
.. code-block:: python
from django.db import models
from wagtail.wagtailadmin.edit_handlers import FieldPanel
from wagtail.wagtailsnippets.models import register_snippet
...
class Advert(models.Model):
url = models.URLField(null=True, blank=True)
text = models.CharField(max_length=255)
panels = [
FieldPanel('url'),
FieldPanel('text'),
]
def __unicode__(self):
return self.text
register_snippet(Advert)
The ``Advert`` model uses the basic Django model class and defines two properties: text and url. The editing interface is very close to that provided for ``Page``-derived models, with fields assigned in the panels property. Snippets do not use multiple tabs of fields, nor do they provide the "save as draft" or "submit for moderation" features.
``register_snippet(Advert)`` tells Wagtail to treat the model as a snippet. The ``panels`` list defines the fields to show on the snippet editing page. It's also important to provide a string representation of the class through ``def __unicode__(self):`` so that the snippet objects make sense when listed in the Wagtail admin.
Including Snippets in Template Tags
-----------------------------------
The simplest way to make your snippets available to templates is with a template tag. This is mostly done with vanilla Django, so perhaps reviewing Django's documentation for `django custom template tags`_ will be more helpful. We'll go over the basics, though, and make note of any considerations to make for Wagtail.
First, add a new python file to a ``templatetags`` folder within your app. The demo website, for instance uses the path ``wagtaildemo/demo/templatetags/demo_tags.py``. We'll need to load some Django modules and our app's models and ready the ``register`` decorator:
.. _django custom template tags: https://docs.djangoproject.com/en/dev/howto/custom-template-tags/
.. code-block:: python
from django import template
from demo.models import *
register = template.Library()
...
# Advert snippets
@register.inclusion_tag('demo/tags/adverts.html', takes_context=True)
def adverts(context):
return {
'adverts': Advert.objects.all(),
'request': context['request'],
}
``@register.inclusion_tag()`` takes two variables: a template and a boolean on whether that template should be passed a request context. It's a good idea to include request contexts in your custom template tags, since some Wagtail-specific template tags like ``pageurl`` need the context to work properly. The template tag function could take arguments and filter the adverts to return a specific model, but for brevity we'll just use ``Advert.objects.all()``.
Here's what's in the template used by the template tag:
.. code-block:: django
{% for advert in adverts %}
<p>
<a href="{{ advert.url }}">
{{ advert.text }}
</a>
</p>
{% endfor %}
Then in your own page templates, you can include your snippet template tag with:
.. code-block:: django
{% block content %}
...
{% adverts %}
{% endblock %}
Binding Pages to Snippets
-------------------------
An alternate strategy for including snippets might involve explicitly binding a specific page object to a specific snippet object. Lets add another snippet class to see how that might work:
.. code-block:: python
from django.db import models
from wagtail.wagtailcore.models import Page
from wagtail.wagtailadmin.edit_handlers import PageChooserPanel
from wagtail.wagtailsnippets.models import register_snippet
from wagtail.wagtailsnippets.edit_handlers import SnippetChooserPanel
from modelcluster.fields import ParentalKey
...
class AdvertPlacement(models.Model):
page = ParentalKey('wagtailcore.Page', related_name='advert_placements')
advert = models.ForeignKey('demo.Advert', related_name='+')
class Meta:
verbose_name = "Advert Placement"
verbose_name_plural = "Advert Placements"
panels = [
PageChooserPanel('page'),
SnippetChooserPanel('advert', Advert),
]
def __unicode__(self):
return self.page.title + " -> " + self.advert.text
register_snippet(AdvertPlacement)
The class ``AdvertPlacement`` has two properties, ``page`` and ``advert``, which point to other models. Wagtail provides a ``PageChooserPanel`` and ``SnippetChooserPanel`` to let us make painless selection of those properties in the Wagtail admin. Note also the ``Meta`` class, which you can stock with the ``verbose_name`` and ``verbose_name_plural`` properties to override the snippet labels in the Wagtail admin. The text representation of the class has also gotten fancy, using both properties to construct a compound label showing the relationship it forms between a page and an Advert.
With this snippet in place, we can use the reverse ``related_name`` lookup label ``advert_placements`` to iterate over any placements within our template files. In the template for a ``Page``-derived model, we could include the following:
.. code-block:: django
{% if self.advert_placements %}
{% for advert_placement in self.advert_placements.all %}
<p><a href="{{ advert_placement.advert.url }}">{{ advert_placement.advert.text }}</a></p>
{% endfor %}
{% endif %}

View file

@ -0,0 +1,83 @@
Generating a static site
========================
This document describes how to render your Wagtail site into static HTML files on your local filesystem, Amazon S3 or Google App Engine, using `django medusa`_ and the ``wagtail.contrib.wagtailmedusa`` module.
Installing django-medusa
~~~~~~~~~~~~~~~~~~~~~~~~
First, install django medusa from pip:
.. code::
pip install django-medusa
Then add ``django_medusa`` and ``wagtail.contrib.wagtailmedusa`` to ``INSTALLED_APPS``:
.. code:: python
INSTALLED_APPS = [
...
'django_medusa',
'wagtail.contrib.wagtailmedusa',
]
Replacing GET parameters with custom routing
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Pages which require GET parameters (e.g. for pagination) don't generate suitable filenames for generated HTML files so they need to be changed to use custom routing instead.
For example, let's say we have a Blog Index which uses pagination. We can override the ``route`` method to make it respond on urls like '/page/1', and pass the page number through to the ``serve`` method:
.. code:: python
class BlogIndex(Page):
...
def serve(self, request, page=1):
...
def route(self, request, path_components):
if self.live and len(path_components) == 2 and path_components[0] == 'page':
try:
return self.serve(request, page=int(path_components[1]))
except (TypeError, ValueError):
pass
return super(BlogIndex, self).route(request, path_components)
Rendering pages which use custom routing
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
For page types that override the ``route`` method, we need to let django medusa know which URLs it responds on. This is done by overriding the ``get_static_site_paths`` method to make it yield one string per URL path.
For example, the BlogIndex above would need to yield one URL for each page of results:
.. code:: python
def get_static_site_paths(self):
# Get page count
page_count = ...
# Yield a path for each page
for page in range(page_count):
yield '/%d/' % (page + 1)
# Yield from superclass
for path in super(BlogIndex, self).get_static_site_paths():
yield path
Rendering
~~~~~~~~~
To render a site, run ``./manage.py staticsitegen``. This will render the entire website and place the HTML in a folder called 'medusa_output'. The static and media folders need to be copied into this folder manually after the rendering is complete. This feature inherits django-medusa's ability to render your static site to Amazon S3 or Google App Engine; see the `medusa docs <https://github.com/mtigas/django-medusa/blob/master/README.markdown>`_ for configuration details.
To test, open the 'medusa_output' folder in a terminal and run ``python -m SimpleHTTPServer``.
.. _django medusa: https://github.com/mtigas/django-medusa

View file

@ -1,15 +1,264 @@
Search
======
Wagtail can degrade to a database-backed text search, but we strongly recommend `Elasticsearch`_. If you prefer not to run an Elasticsearch server in development or production, there are many hosted services available, including `Searchly`_, who offer a free account suitable for testing and development. To use Searchly:
Wagtail provides a comprehensive and extensible search interface. In addition, it provides ways to promote search results through "Editor's Picks." Wagtail also collects simple statistics on queries made through the search interface.
Default Page Search
-------------------
Wagtail provides a default frontend search interface which indexes the ``title`` field common to all ``Page``-derived models. Let's take a look at all the components of the search interface.
The most basic search functionality just needs a search box which submits a request. Since this will be reused throughout the site, let's put it in ``mysite/includes/search_box.html`` and then use ``{% include ... %}`` to weave it into templates:
.. code-block:: django
<form action="{% url 'wagtailsearch_search' %}" method="get">
<input type="text" name="q"{% if query_string %} value="{{ query_string }}"{% endif %}>
<input type="submit" value="Search">
</form>
The form is submitted to the url of the ``wagtailsearch_search`` view, with the search terms variable ``q``. The view will use its own basic search results template.
Let's use our own template for the results, though. First, in your project's ``settings.py``, define a path to your template:
.. code-block:: python
WAGTAILSEARCH_RESULTS_TEMPLATE = 'mysite/search_results.html'
Next, let's look at the template itself:
.. code-block:: django
{% extends "mysite/base.html" %}
{% load pageurl %}
{% block title %}Search{% if search_results %} Results{% endif %}{% endblock %}
{% block search_box %}
{% include "mysite/includes/search_box.html" with query_string=query_string only %}
{% endblock %}
{% block content %}
<h2>Search Results{% if request.GET.q %} for {{ request.GET.q }}{% endif %}</h2>
<ul>
{% for result in search_results %}
<li>
<h4><a href="{% pageurl result.specific %}">{{ result.specific }}</a></h4>
{% if result.specific.search_description %}
{{ result.specific.search_description|safe }}
{% endif %}
</li>
{% empty %}
<li>No results found</li>
{% endfor %}
</ul>
{% endblock %}
The search view provides a context with a few useful variables.
``query_string``
The terms (string) used to make the search.
``search_results``
A collection of Page objects matching the query. The ``specific`` property of ``Page`` will give the most-specific subclassed model object for the Wagtail page. For instance, if an ``Event`` model derived from the basic Wagtail ``Page`` were included in the search results, you could use ``specific`` to access the custom properties of the ``Event`` model (``result.specific.date_of_event``).
``is_ajax``
Boolean. This returns Django's ``request.is_ajax()``.
``query``
A Wagtail ``Query`` object matching the terms. The ``Query`` model provides several class methods for viewing the statistics of all queries, but exposes only one property for single objects, ``query.hits``, which tracks the number of time the search string has been used over the lifetime of the site. ``Query`` also joins to the Editor's Picks functionality though ``query.editors_picks``. See :ref:`editors-picks`.
Editor's Picks
--------------
Editor's Picks are a way of explicitly linking relevant content to search terms, so results pages can contain curated content instead of being at the mercy of the search algorithm. In a template using the search results view, editor's picks can be accessed through the variable ``query.editors_picks``. To include editor's picks in your search results template, use the following properties.
``query.editors_picks.all``
This gathers all of the editor's picks objects relating to the current query, in order according to their sort order in the Wagtail admin. You can then iterate through them using a ``{% for ... %}`` loop. Each editor's pick object provides these properties:
``editors_pick.page``
The page object associated with the pick. Use ``{% pageurl editors_pick.page %}`` to generate a URL or provide other properties of the page object.
``editors_pick.description``
The description entered when choosing the pick, perhaps explaining why the page is relevant to the search terms.
Putting this all together, a block of your search results template displaying editor's Picks might look like this:
.. code-block:: django
{% with query.editors_picks.all as editors_picks %}
{% if editors_picks %}
<div class="well">
<h3>Editors picks</h3>
<ul>
{% for editors_pick in editors_picks %}
<li>
<h4>
<a href="{% pageurl editors_pick.page %}">
{{ editors_pick.page.title }}
</a>
</h4>
<p>{{ editors_pick.description|safe }}</p>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endwith %}
Asynchronous Search with JSON and AJAX
--------------------------------------
Wagtail provides JSON search results when queries are made to the ``wagtailsearch_suggest`` view. To take advantage of it, we need a way to make that URL available to a static script. Instead of hard-coding it, let's set a global variable in our ``base.html``:
.. code-block:: django
<script>
var wagtailJSONSearchURL = "{% url 'wagtailsearch_suggest' %}";
</script>
Now add a simple interface for the search with a ``<input>`` element to gather search terms and a ``<div>`` to display the results:
.. code-block:: html
<div>
<h3>Search</h3>
<input id="json-search" type="text">
<div id="json-results"></div>
</div>
Finally, we'll use JQuery to make the asynchronous requests and handle the interactivity:
.. code-block:: guess
$(function() {
// cache the elements
var searchBox = $('#json-search'),
resultsBox = $('#json-results');
// when there's something in the input box, make the query
searchBox.on('input', function() {
if( searchBox.val() == ''){
resultsBox.html('');
return;
}
// make the request to the Wagtail JSON search view
$.ajax({
url: wagtailJSONSearchURL + "?q=" + searchBox.val(),
dataType: "json"
})
.done(function(data) {
console.log(data);
if( data == undefined ){
resultsBox.html('');
return;
}
// we're in business! let's format the results
var htmlOutput = '';
data.forEach(function(element, index, array){
htmlOutput += '<p><a href="' + element.url + '">' + element.title + '</a></p>';
});
// and display them
resultsBox.html(htmlOutput);
})
.error(function(data){
console.log(data);
});
});
});
Results are returned as a JSON object with this structure:
.. code-block:: guess
{
[
{
title: "Lumpy Space Princess",
url: "/oh-my-glob/"
},
{
title: "Lumpy Space",
url: "/no-smooth-posers/"
},
...
]
}
What if you wanted access to the rest of the results context or didn't feel like using JSON? Wagtail also provides a generalized AJAX interface where you can use your own template to serve results asynchronously.
The AJAX interface uses the same view as the normal HTML search, ``wagtailsearch_search``, but will serve different results if Django classifies the request as AJAX (``request.is_ajax()``). Another entry in your project settings will let you override the template used to serve this response:
.. code-block:: python
WAGTAILSEARCH_RESULTS_TEMPLATE_AJAX = 'myapp/includes/search_listing.html'
In this template, you'll have access to the same context variables provided to the HTML template. You could provide a template in JSON format with extra properties, such as ``query.hits`` and editor's picks, or render an HTML snippet that can go directly into your results ``<div>``. If you need more flexibility, such as multiple formats/templates based on differing requests, you can set up a custom search view.
.. _editors-picks:
Indexing Custom Fields & Custom Search Views
--------------------------------------------
This functionality is still under active development to provide a streamlined interface, but take a look at ``wagtail/wagtail/wagtailsearch/views/frontend.py`` if you are interested in coding custom search views.
Search Backends
---------------
Wagtail can degrade to a database-backed text search, but we strongly recommend `Elasticsearch`_.
.. _Elasticsearch: http://www.elasticsearch.org/
Default DB Backend
``````````````````
The default DB search backend uses Django's ``__icontains`` filter.
Elasticsearch Backend
`````````````````````
Prerequisites are the Elasticsearch service itself and, via pip, the `elasticutils`_ and `pyelasticsearch`_ packages:
.. code-block:: guess
pip install elasticutils pyelasticsearch
.. note::
The dependency on pyelasticsearch is scheduled to be replaced by a dependency on `elasticsearch-py`_.
The backend is configured in settings:
.. code-block:: python
WAGTAILSEARCH_BACKENDS = {
'default': {
'BACKEND': 'wagtail.wagtailsearch.backends.elasticsearch.ElasticSearch',
'URLS': ['http://localhost:9200'],
'INDEX': 'wagtail',
'TIMEOUT': 5,
'FORCE_NEW': False,
}
}
Other than ``BACKEND`` the keys are optional and default to the values shown. ``FORCE_NEW`` is used by elasticutils. In addition, any other keys are passed directly to the Elasticsearch constructor as case-sensitive keyword arguments (e.g. ``'max_retries': 1``).
If you prefer not to run an Elasticsearch server in development or production, there are many hosted services available, including `Searchly`_, who offer a free account suitable for testing and development. To use Searchly:
- Sign up for an account at `dashboard.searchly.com/users/sign\_up`_
- Use your Searchly dashboard to create a new index, e.g. 'wagtaildemo'
- Note the connection URL from your Searchly dashboard
- Update ``WAGTAILSEARCH_ES_URLS`` and ``WAGTAILSEARCH_ES_INDEX`` in
your local settings
- Configure ``URLS`` and ``INDEX`` in the Elasticsearch entry in ``WAGTAILSEARCH_BACKENDS``
- Run ``./manage.py update_index``
.. _Elasticsearch: http://www.elasticsearch.org/
.. _elasticutils: http://elasticutils.readthedocs.org
.. _pyelasticsearch: http://pyelasticsearch.readthedocs.org
.. _elasticsearch-py: http://elasticsearch-py.readthedocs.org
.. _Searchly: http://www.searchly.com/
.. _dashboard.searchly.com/users/sign\_up: https://dashboard.searchly.com/users/sign_up
.. _dashboard.searchly.com/users/sign\_up: https://dashboard.searchly.com/users/sign_up
Rolling Your Own
````````````````
Wagtail search backends implement the interface defined in ``wagtail/wagtail/wagtailsearch/backends/base.py``. At a minimum, the backend's ``search()`` method must return a collection of objects or ``model.objects.none()``. For a fully-featured search backend, examine the Elasticsearch backend code in ``elasticsearch.py``.

View file

@ -1,5 +1,7 @@
# Requirements essential for developing wagtail (not needed to run it)
unittest2==0.5.1
# For coverage and PEP8 linting
coverage==3.7.1
flake8==2.1.0

View file

@ -26,20 +26,23 @@ if not settings.configured:
if has_elasticsearch:
WAGTAILSEARCH_BACKENDS['elasticsearch'] = {
'BACKEND': 'wagtail.wagtailsearch.backends.elasticsearch.ElasticSearch',
'TIMEOUT': 10,
'max_retries': 1,
}
settings.configure(
DATABASES={
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'ENGINE': os.environ.get('DATABASE_ENGINE', 'django.db.backends.postgresql_psycopg2'),
'NAME': 'wagtaildemo',
'USER': 'postgres',
'USER': os.environ.get('DATABASE_USER', 'postgres'),
}
},
ROOT_URLCONF='wagtail.tests.urls',
STATIC_URL='/static/',
STATIC_ROOT=STATIC_ROOT,
MEDIA_ROOT=MEDIA_ROOT,
USE_TZ=True,
STATICFILES_FINDERS=(
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'compressor.finders.CompressorFinder',
@ -80,8 +83,19 @@ if not settings.configured:
'wagtail.wagtailembeds',
'wagtail.wagtailsearch',
'wagtail.wagtailredirects',
'wagtail.wagtailforms',
'wagtail.tests',
],
# Using DatabaseCache to make sure that the cache is cleared between tests.
# This prevents false-positives in some wagtail core tests where we are
# changing the 'wagtail_root_paths' key which may cause future tests to fail.
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
'LOCATION': 'cache',
}
},
PASSWORD_HASHERS=(
'django.contrib.auth.hashers.MD5PasswordHasher', # don't use the intentionally slow default password hasher
),

View file

@ -1,5 +1,7 @@
# Production-configured Wagtail installation
# (secure services/account for full production use).
# Production-configured Wagtail installation.
# BUT, SECURE SERVICES/ACCOUNT FOR FULL PRODUCTION USE!
# For a non-dummy email backend configure Django's EMAIL_BACKEND
# in settings/production.py post-installation.
# Tested on Debian 7.0.
# Tom Dyson and Neal Todd
@ -42,6 +44,7 @@ aptitude -y install openjdk-7-jre-headless
curl -O https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-1.0.0.deb
dpkg -i elasticsearch-1.0.0.deb
rm elasticsearch-1.0.0.deb
perl -pi -e"s/# network.host: 192.168.0.1/network.host: 127.0.0.1/" /etc/elasticsearch/elasticsearch.yml
update-rc.d elasticsearch defaults 95 10
service elasticsearch start

View file

@ -1,5 +1,7 @@
# Production-configured Wagtail installation
# (secure services/account for full production use).
# Production-configured Wagtail installation.
# BUT, SECURE SERVICES/ACCOUNT FOR FULL PRODUCTION USE!
# For a non-dummy email backend configure Django's EMAIL_BACKEND
# in settings/production.py post-installation.
# Tested on Ubuntu 13.04 and 13.10.
# Tom Dyson and Neal Todd
@ -40,6 +42,7 @@ aptitude -y install openjdk-7-jre-headless
curl -O https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-1.0.0.deb
dpkg -i elasticsearch-1.0.0.deb
rm elasticsearch-1.0.0.deb
perl -pi -e"s/# network.host: 192.168.0.1/network.host: 127.0.0.1/" /etc/elasticsearch/elasticsearch.yml
update-rc.d elasticsearch defaults 95 10
service elasticsearch start

View file

@ -18,7 +18,7 @@ except ImportError:
setup(
name='wagtail',
version='0.2',
version='0.3',
description='A Django content management system focused on flexibility and user experience',
author='Matthew Westcott',
author_email='matthew.westcott@torchbox.com',
@ -34,6 +34,8 @@ setup(
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Framework :: Django',
'Topic :: Internet :: WWW/HTTP :: Site Management',
@ -44,10 +46,12 @@ setup(
"django-compressor>=1.3",
"django-libsass>=0.1",
"django-modelcluster>=0.1",
"django-taggit>=0.11.2",
"django-taggit==0.11.2",
"django-treebeard==2.0",
"Pillow>=2.3.0",
"beautifulsoup4>=4.3.2",
"lxml>=3.3.0",
'unicodecsv>=0.9.4',
'Unidecode>=0.04.14',
"BeautifulSoup==3.2.1", # django-compressor gets confused if we have lxml but not BS3 installed
],

69
tox.ini Normal file
View file

@ -0,0 +1,69 @@
[deps]
dj16=
Django>=1.6,<1.7
pyelasticsearch==0.6.1
elasticutils==0.8.2
[tox]
envlist =
py26-dj16-postgres,
py26-dj16-sqlite,
py27-dj16-postgres,
py27-dj16-sqlite
# mysql not currently supported
# (wagtail.wagtailimages.tests.TestImageEditView currently fails with a
# foreign key constraint error)
# py26-dj16-mysql
# py27-dj16-mysql
[testenv]
commands=./runtests.py
[testenv:py26-dj16-postgres]
basepython=python2.6
deps =
{[deps]dj16}
psycopg2==2.5.2
setenv =
DATABASE_ENGINE=django.db.backends.postgresql_psycopg2
[testenv:py26-dj16-sqlite]
basepython=python2.6
deps =
{[deps]dj16}
setenv =
DATABASE_ENGINE=django.db.backends.sqlite3
[testenv:py26-dj16-mysql]
basepython=python2.6
deps =
{[deps]dj16}
MySQL-python==1.2.5
setenv =
DATABASE_ENGINE=django.db.backends.mysql
DATABASE_USER=wagtail
[testenv:py27-dj16-postgres]
basepython=python2.7
deps =
{[deps]dj16}
psycopg2==2.5.2
setenv =
DATABASE_ENGINE=django.db.backends.postgresql_psycopg2
[testenv:py27-dj16-sqlite]
basepython=python2.7
deps =
{[deps]dj16}
setenv =
DATABASE_ENGINE=django.db.backends.sqlite3
[testenv:py27-dj16-mysql]
basepython=python2.7
deps =
{[deps]dj16}
MySQL-python==1.2.5
setenv =
DATABASE_ENGINE=django.db.backends.mysql
DATABASE_USER=wagtail

View file

View file

@ -0,0 +1,24 @@
from django_medusa.renderers import StaticSiteRenderer
from wagtail.wagtailcore.models import Site
from wagtail.wagtaildocs.models import Document
class PageRenderer(StaticSiteRenderer):
def get_paths(self):
# Get site
# TODO: Find way to get this to work with other sites
site = Site.objects.filter(is_default_site=True).first()
if site is None:
return []
# Return list of paths
return site.root_page.get_static_site_paths()
class DocumentRenderer(StaticSiteRenderer):
def get_paths(self):
# Return list of paths to documents
return (doc.url for doc in Document.objects.all())
renderers = [PageRenderer, DocumentRenderer]

View file

@ -23,7 +23,7 @@
"model": "wagtailcore.page",
"fields": {
"title": "Welcome to the Wagtail test site!",
"numchild": 1,
"numchild": 3,
"show_in_menus": false,
"live": true,
"depth": 2,
@ -141,6 +141,80 @@
}
},
{
"pk": 7,
"model": "wagtailcore.page",
"fields": {
"title": "About us",
"numchild": 0,
"show_in_menus": true,
"live": true,
"depth": 3,
"content_type": ["tests", "simplepage"],
"path": "000100010002",
"url_path": "/home/about-us/",
"slug": "about-us"
}
},
{
"pk": 7,
"model": "tests.simplepage",
"fields": {
"content": "<p>We are really good.</p>"
}
},
{
"pk": 8,
"model": "wagtailcore.page",
"fields": {
"title": "Contact us",
"numchild": 0,
"show_in_menus": true,
"live": true,
"depth": 3,
"content_type": ["tests", "formpage"],
"path": "000100010003",
"url_path": "/home/contact-us/",
"slug": "contact-us"
}
},
{
"pk": 8,
"model": "tests.formpage",
"fields": {
}
},
{
"pk": 1,
"model": "tests.formfield",
"fields": {
"sort_order": 1,
"label": "Your email",
"field_type": "email",
"required": true,
"choices": "",
"default_value": "",
"help_text": "",
"page": 8
}
},
{
"pk": 2,
"model": "tests.formfield",
"fields": {
"sort_order": 2,
"label": "Your message",
"field_type": "multiline",
"required": true,
"choices": "",
"default_value": "",
"help_text": "",
"page": 8
}
},
{
"pk": 1,
"model": "wagtailcore.site",
@ -178,6 +252,19 @@
]
}
},
{
"pk": 5,
"model": "auth.group",
"fields": {
"name": "Site-wide editors",
"permissions": [
["access_admin", "wagtailadmin", "admin"],
["add_image", "wagtailimages", "image"],
["change_image", "wagtailimages", "image"],
["delete_image", "wagtailimages", "image"]
]
}
},
{
"pk": 1,
"model": "wagtailcore.grouppagepermission",
@ -214,6 +301,15 @@
"permission_type": "publish"
}
},
{
"pk": 5,
"model": "wagtailcore.grouppagepermission",
"fields": {
"group": ["Site-wide editors"],
"page": 2,
"permission_type": "edit"
}
},
{
"pk": 1,
@ -285,5 +381,42 @@
"password": "md5$seasalt$1e9bf2bf5606aa5c39852cc30f0f6f22",
"email": "inactiveuser@example.com"
}
},
{
"pk": 5,
"model": "auth.user",
"fields": {
"username": "siteeditor",
"first_name": "",
"last_name": "",
"is_active": true,
"is_superuser": false,
"is_staff": false,
"groups": [
["Site-wide editors"]
],
"user_permissions": [],
"password": "md5$seasalt$1e9bf2bf5606aa5c39852cc30f0f6f22",
"email": "siteeditor@example.com"
}
},
{
"pk": 1,
"model": "wagtailforms.formsubmission",
"fields": {
"form_data": "{\"your-email\": \"old@example.com\", \"your-message\": \"this is a really old message\"}",
"page": 8,
"submit_time": "2013-01-01T12:00:00.000Z"
}
},
{
"pk": 2,
"model": "wagtailforms.formsubmission",
"fields": {
"form_data": "{\"your-email\": \"new@example.com\", \"your-message\": \"this is a fairly new message\"}",
"page": 8,
"submit_time": "2014-01-01T12:00:00.000Z"
}
}
]

View file

@ -1,10 +1,12 @@
from django.db import models
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from modelcluster.fields import ParentalKey
from wagtail.wagtailcore.models import Page, Orderable
from wagtail.wagtailcore.fields import RichTextField
from wagtail.wagtailadmin.edit_handlers import FieldPanel, MultiFieldPanel, InlinePanel, PageChooserPanel
from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
from wagtail.wagtaildocs.edit_handlers import DocumentChooserPanel
from wagtail.wagtailforms.models import AbstractEmailForm, AbstractFormField
EVENT_AUDIENCE_CHOICES = (
@ -187,12 +189,66 @@ class EventIndex(Page):
intro = RichTextField(blank=True)
ajax_template = 'tests/includes/event_listing.html'
def get_context(self, request):
def get_events(self):
return self.get_children().live().type(EventPage)
def get_paginator(self):
return Paginator(self.get_events(), 4)
def get_context(self, request, page=1):
# Pagination
paginator = self.get_paginator()
try:
events = paginator.page(page)
except PageNotAnInteger:
events = paginator.page(1)
except EmptyPage:
events = paginator.page(paginator.num_pages)
# Update context
context = super(EventIndex, self).get_context(request)
context['events'] = EventPage.objects.filter(live=True)
context['events'] = events
return context
def route(self, request, path_components):
if self.live and len(path_components) == 1:
try:
return self.serve(request, page=int(path_components[0]))
except (TypeError, ValueError):
pass
return super(EventIndex, self).route(request, path_components)
def get_static_site_paths(self):
# Get page count
page_count = self.get_paginator().num_pages
# Yield a path for each page
for page in range(page_count):
yield '/%d/' % (page + 1)
# Yield from superclass
for path in super(EventIndex, self).get_static_site_paths():
yield path
EventIndex.content_panels = [
FieldPanel('title', classname="full title"),
FieldPanel('intro', classname="full"),
]
class FormField(AbstractFormField):
page = ParentalKey('FormPage', related_name='form_fields')
class FormPage(AbstractEmailForm):
pass
FormPage.content_panels = [
FieldPanel('title', classname="full title"),
InlinePanel(FormPage, 'form_fields', label="Form fields"),
MultiFieldPanel([
FieldPanel('to_address', classname="full"),
FieldPanel('from_address', classname="full"),
FieldPanel('subject', classname="full"),
], "Email")
]

View file

@ -0,0 +1,15 @@
{% load pageurl %}
<!DOCTYPE HTML>
<html>
<head>
<title>{{ self.title }}</title>
</head>
<body>
<h1>{{ self.title }}</h1>
<form action="{% pageurl self %}" method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit">
</form>
</body>
</html>

View file

@ -0,0 +1,11 @@
{% load pageurl %}
<!DOCTYPE HTML>
<html>
<head>
<title>{{ self.title }}</title>
</head>
<body>
<h1>{{ self.title }}</h1>
<p>Thank you for your feedback.</p>
</body>
</html>

View file

@ -2,14 +2,8 @@ from django.conf.urls import patterns, include, url
from wagtail.wagtailcore import urls as wagtail_urls
from wagtail.wagtailadmin import urls as wagtailadmin_urls
from wagtail.wagtailimages import urls as wagtailimages_urls
from wagtail.wagtailembeds import urls as wagtailembeds_urls
from wagtail.wagtaildocs import admin_urls as wagtaildocs_admin_urls
from wagtail.wagtaildocs import urls as wagtaildocs_urls
from wagtail.wagtailsnippets import urls as wagtailsnippets_urls
from wagtail.wagtailsearch.urls import frontend as wagtailsearch_frontend_urls, admin as wagtailsearch_admin_urls
from wagtail.wagtailusers import urls as wagtailusers_urls
from wagtail.wagtailredirects import urls as wagtailredirects_urls
from wagtail.wagtailsearch.urls import frontend as wagtailsearch_frontend_urls
# Signal handlers
from wagtail.wagtailsearch import register_signal_handlers as wagtailsearch_register_signal_handlers
@ -17,16 +11,8 @@ wagtailsearch_register_signal_handlers()
urlpatterns = patterns('',
url(r'^admin/images/', include(wagtailimages_urls)),
url(r'^admin/embeds/', include(wagtailembeds_urls)),
url(r'^admin/documents/', include(wagtaildocs_admin_urls)),
url(r'^admin/snippets/', include(wagtailsnippets_urls)),
url(r'^admin/search/', include(wagtailsearch_admin_urls)),
url(r'^admin/users/', include(wagtailusers_urls)),
url(r'^admin/redirects/', include(wagtailredirects_urls)),
url(r'^admin/', include(wagtailadmin_urls)),
url(r'^search/', include(wagtailsearch_frontend_urls)),
url(r'^documents/', include(wagtaildocs_urls)),
# For anything not caught by a more specific rule above, hand over to

View file

@ -1,9 +1,20 @@
from django.contrib.auth.models import User
# We need to make sure that we're using the same unittest library that Django uses internally
# Otherwise, we get issues with the "SkipTest" and "ExpectedFailure" exceptions being recognised as errors
try:
# Firstly, try to import unittest from Django
from django.utils import unittest
except ImportError:
# Django doesn't include unittest
# We must be running on Django 1.7+ which doesn't support Python 2.6 so
# the standard unittest library should be unittest2
import unittest
def login(client):
# Create a user
User.objects.create_superuser(username='test', email='test@email.com', password='password')
# Login
client.login(username='test', password='password')
client.login(username='test', password='password')

View file

@ -1,17 +0,0 @@
[run]
branch = True
source = treebeard
parallel = True
[paths]
source =
./
*\workspace\django-treebeard\tox_db\*\tox_django\*\tox_python\*\os\windows/
*/jobs/django-treebeard/workspace/TOX_DB/*/TOX_DJANGO/*/TOX_PYTHON/*/os/osx/
*/workspace/django-treebeard/TOX_DB/*/TOX_DJANGO/*/TOX_PYTHON/*/os/linux/
[report]
omit =
*/tests/*
*/numconv.py
precision = 2

View file

@ -1,20 +0,0 @@
syntax: glob
.DS_Store
.buildinfo
*.pyc
*.orig
*.swp
.coverage
build
dist
_build
MANIFEST
.project
.pydevproject
.settings
htmlcov
.tox
*.xml
.coverage.*
*.egg-info

View file

@ -1,14 +0,0 @@
29b76a1f6042e63bf9f234bb48c95dcfbd0afc8d 1.0
5e39c474d8ea24993777332f8d7ccfd0da1014ad 1.1
859f2a36845426d0ff8914cbc58a8b5c52f07256 1.5
859f2a36845426d0ff8914cbc58a8b5c52f07256 1.5
630024c53f5fac1f5aae412fcfc8c207e5a9d3da 1.5
3fe083f135c7e36c08e76448368355af30125e50 1.51
0ea8c876d30783ef3a0e8b6f9565371c0f13e8a5 1.52
d73b1298ef049d6ddc5fbfc665f51a3c7b376494 1.6
d73b1298ef049d6ddc5fbfc665f51a3c7b376494 1.6
b510c7559b915a59f647276affd460e24c85ae9c 1.6
0b95d619fc8a264ac93ed6de6b1e34886e7d5d07 1.60
1af1b23d695d27f963d6393327f6a3c0bdd7df31 1.61
23f4629de5d7df21f03fdf26abed3baea786ca8b 2.0b1
87a28ab0b44063a2989d805c56483dacbb637532 2.0b2

View file

@ -1,19 +0,0 @@
Treebeard was created in 2008 by Gustavo Picon.
Contributions made by:
* Aureal
* Jean-Matthieu Barbier
* Jesus del Carpio
* chembervint
* Matt Hoskins
* Rob Hudson
* Alexey Kinyov
* omad
* Oregon Center for Applied Science
* Alejandro Peralta
* Jaap Roes
* Alexei Vlasov
* moberley
* czare1
* Fernando Gutierrez (xbito)

View file

@ -1,150 +0,0 @@
Release 2.0b2 (December, 2013)
------------------------------
* Dropped support for Python 2.5
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.feincms.org
.. _Jesús del Carpio: http://www.isgeek.net
.. _Fernando Gutierrez: http://xbito.pe

View file

@ -1,203 +0,0 @@
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.

View file

@ -1,3 +0,0 @@
include CHANGES LICENSE NOTICE README.rst UPDATING MANIFEST.in
recursive-include docs Makefile README.rst *.py *.rst
recursive-include treebeard *.py *.html *.js *.css *.png

View file

@ -1,29 +0,0 @@
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
- **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/

View file

@ -1,16 +0,0 @@
This 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.

View file

@ -1,96 +0,0 @@
# 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 <target>' where <target> 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"

View file

@ -1,7 +0,0 @@
This is the documentation source for django-treebeard.
You can read the documentation in:
http://docs.tabo.pe/django-treebeard/tip/
Or read the documentation for this version re reading the .rst files in this
directory.

View file

@ -1,10 +0,0 @@
# taken from:
# http://reinout.vanrees.org/weblog/2012/12/01/django-intersphinx.html
def setup(app):
app.add_crossref_type(
directivename="setting",
rolename="setting",
indextemplate="pair: %s; setting",
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

View file

@ -1,52 +0,0 @@
Admin
=====
API
---
.. module:: treebeard.admin
.. autoclass:: TreeAdmin
:show-inheritance:
Example:
.. code-block:: python
from django.contrib import admin
from treebeard.admin import TreeAdmin
from treebeard.forms import movenodeform_factory
from myproject.models import MyNode
class MyAdmin(TreeAdmin):
form = movenodeform_factory(MyNode)
admin.site.register(MyNode, MyAdmin)
.. autofunction:: admin_factory
Interface
---------
The features of the admin interface will depend on the tree type.
Advanced Interface
~~~~~~~~~~~~~~~~~~
:doc:`Materialized Path <mp_tree>` and :doc:`Nested Sets <ns_tree>` trees have
an AJAX interface based on `FeinCMS`_, that includes features like
drag&drop and an attractive interface.
.. image:: _static/treebeard-admin-advanced.png
Basic Interface
~~~~~~~~~~~~~~~
:doc:`Adjacency List <al_tree>` trees have a basic admin interface.
.. image:: _static/treebeard-admin-basic.png
.. _FeinCMS: http://www.feincms.org

View file

@ -1,106 +0,0 @@
Adjacency 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.
.. warning::
As with all tree implementations, please be aware of the
:doc:`caveats`.
.. inheritance-diagram:: AL_Node
.. autoclass:: AL_Node
:show-inheritance:
.. warning::
If you need to define your own
:py:class:`~django.db.models.Manager` class,
you'll need to subclass
:py:class:`~AL_NodeManager`.
.. 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:
.. code-block:: python
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:
.. code-block:: python
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:
.. code-block:: python
sib_order = models.PositiveIntegerField()
Examples:
.. code-block:: python
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`
.. autoclass:: AL_NodeManager
:show-inheritance:

View file

@ -1,405 +0,0 @@
API
===
.. 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 be aware of the :doc:`caveats` when using this library.
.. automethod:: Node.add_root
Example:
.. code-block:: python
MyNode.add_root(numval=1, strval='abcd')
.. automethod:: add_child
Example:
.. code-block:: python
node.add_child(numval=1, strval='abcd')
.. automethod:: add_sibling
Examples:
.. code-block:: python
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:
.. code-block:: python
node.get_depth()
.. automethod:: get_ancestors
Example:
.. code-block:: python
node.get_ancestors()
.. automethod:: get_children
Example:
.. code-block:: python
node.get_children()
.. automethod:: get_children_count
Example:
.. code-block:: python
node.get_children_count()
.. automethod:: get_descendants
Example:
.. code-block:: python
node.get_descendants()
.. automethod:: get_descendant_count
Example:
.. code-block:: python
node.get_descendant_count()
.. automethod:: get_first_child
Example:
.. code-block:: python
node.get_first_child()
.. automethod:: get_last_child
Example:
.. code-block:: python
node.get_last_child()
.. automethod:: get_first_sibling
Example:
.. code-block:: python
node.get_first_sibling()
.. automethod:: get_last_sibling
Example:
.. code-block:: python
node.get_last_sibling()
.. automethod:: get_prev_sibling
Example:
.. code-block:: python
node.get_prev_sibling()
.. automethod:: get_next_sibling
Example:
.. code-block:: python
node.get_next_sibling()
.. automethod:: get_parent
Example:
.. code-block:: python
node.get_parent()
.. automethod:: get_root
Example:
.. code-block:: python
node.get_root()
.. automethod:: get_siblings
Example:
.. code-block:: python
node.get_siblings()
.. automethod:: is_child_of
Example:
.. code-block:: python
node.is_child_of(node2)
.. automethod:: is_descendant_of
Example:
.. code-block:: python
node.is_descendant_of(node2)
.. automethod:: is_sibling_of
Example:
.. code-block:: python
node.is_sibling_of(node2)
.. automethod:: is_root
Example:
.. code-block:: python
node.is_root()
.. automethod:: is_leaf
Example:
.. code-block:: python
node.is_leaf()
.. automethod:: move
.. note:: The node can be moved under another root node.
Examples:
.. code-block:: python
node.move(node2, 'sorted-child')
node.move(node2, 'prev-sibling')
.. automethod:: save
.. automethod:: get_first_root_node
Example:
.. code-block:: python
MyNodeModel.get_first_root_node()
.. automethod:: get_last_root_node
Example:
.. code-block:: python
MyNodeModel.get_last_root_node()
.. automethod:: get_root_nodes
Example:
.. code-block:: python
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:
.. code-block:: python
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:
.. code-block:: python
tree = MyNodeModel.dump_bulk()
branch = MyNodeModel.dump_bulk(node_obj)
.. automethod:: find_problems
.. automethod:: fix_tree
.. automethod:: get_descendants_group_count
Example:
.. code-block:: python
# 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:
.. code-block:: python
annotated_list = MyModel.get_annotated_list()
With data:
.. digraph:: get_annotated_list_digraph
"a";
"a" -> "ab";
"ab" -> "aba";
"ab" -> "abb";
"ab" -> "abc";
"a" -> "ac";
Will return:
.. code-block:: python
[
(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:
.. code-block:: django
{% for item, info in annotated_list %}
{% if info.open %}
<ul><li>
{% else %}
</li><li>
{% endif %}
{{ item }}
{% for close in info.close %}
</li></ul>
{% endfor %}
{% endfor %}
.. note::
This method was contributed originally by
`Alexey Kinyov <rudi@05bit.com>`_, using an idea borrowed from
`django-mptt`_.
.. versionadded:: 1.55
.. automethod:: get_database_vendor
Example:
.. code-block:: python
MyNodeModel.get_database_vendor("write")
.. versionadded:: 1.61
.. _django-mptt: https://github.com/django-mptt/django-mptt/

View file

@ -1,40 +0,0 @@
Known Caveats
=============
Raw Queries
-----------
``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.
Overriding the default manager
------------------------------
One of the most common source of bug reports in ``django-treebeard``
is the overriding of the default managers in the subclasses.
``django-treebeard`` relies on the default manager for correctness
and internal maintenance. If you override the default manager,
by overriding the ``objects`` member in your subclass, you
*WILL* have errors and inconsistencies in your tree.
To avoid this problem, if you need to override the default
manager, you'll *NEED* to subclass the manager from
the base manager class for the tree you are using.
Read the documentation in each tree type for details.
Custom Managers
---------------
Related to the previous caveat, if you need to create custom
managers, you *NEED* to subclass the manager from the
base manager class for the tree you are using.
Read the documentation in each tree type for details.

View file

@ -1,4 +0,0 @@
Changelog
=========
.. include:: ../CHANGES

View file

@ -1,59 +0,0 @@
# -*- coding: utf-8 -*-
"""
Configuration for the Sphinx documentation generator.
Reference: http://sphinx.pocoo.org/config.html
"""
import os
import sys
def docs_dir():
rd = os.path.dirname(__file__)
if rd:
return rd
return '.'
for directory in ('_ext', '..'):
sys.path.insert(0, os.path.abspath(os.path.join(docs_dir(), directory)))
os.environ['DJANGO_SETTINGS_MODULE'] = 'treebeard.tests.settings'
extensions = [
'djangodocs',
'sphinx.ext.autodoc',
'sphinx.ext.coverage',
'sphinx.ext.graphviz',
'sphinx.ext.inheritance_diagram',
'sphinx.ext.todo',
'sphinx.ext.intersphinx',
]
templates_path = ['_templates']
source_suffix = '.rst'
master_doc = 'index'
project = 'django-treebeard'
copyright = '2008-2013, Gustavo Picon'
version = '2.0b2'
release = '2.0b2'
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')]
intersphinx_mapping = {
'python': ('http://docs.python.org/3', None),
'django': (
'https://docs.djangoproject.com/en/1.6/',
'https://docs.djangoproject.com/en/1.6/_objects/'
),
}

View file

@ -1,12 +0,0 @@
Exceptions
==========
.. module:: treebeard.exceptions
.. autoexception:: InvalidPosition
.. autoexception:: InvalidMoveToDescendant
.. autoexception:: PathOverflow
.. autoexception:: MissingNodeOrderBy

View file

@ -1,29 +0,0 @@
Forms
=====
.. module:: treebeard.forms
.. autoclass:: MoveNodeForm
:show-inheritance:
.. autofunction:: movenodeform_factory
For a full reference of this function, please read
:py:func:`~django.forms.models.modelform_factory`
Example, ``MyNode`` is a subclass of :py:class:`treebeard.al_tree.AL_Node`:
.. code-block:: python
MyNodeForm = movenodeform_factory(MyNode)
is equivalent to:
.. code-block:: python
class MyNodeForm(MoveNodeForm):
class Meta:
model = models.MyNode
exclude = ('sib_order', 'parent')

View file

@ -1,80 +0,0 @@
django-treebeard
================
`django-treebeard <https://tabo.pe/projects/django-treebeard/>`_
is a library that implements efficient tree implementations for the
`Django Web Framework 1.4+ <http://www.djangoproject.com/>`_, written by
`Gustavo Picón <https://tabo.pe>`_ 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 <al_tree>`
2. :doc:`Materialized Path <mp_tree>`
3. :doc:`Nested Sets <ns_tree>`
- **Fast**: Optimized non-naive tree operations
- **Easy**: Uses Django's
:ref:`model-inheritance` with :ref:`abstract-base-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`_
Overview
--------
.. toctree::
install
tutorial
caveats
.. toctree::
:titlesonly:
changes
Reference
---------
.. toctree::
api
mp_tree
ns_tree
al_tree
exceptions
Additional features
-------------------
.. toctree::
admin
forms
Development
-----------
.. toctree::
tests
.. _`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`

View file

@ -1,89 +0,0 @@
Installation
============
Prerequisites
-------------
``django-treebeard`` needs at least **Python 2.6** to run, and
**Django 1.4 or better**.
Installing
----------
You have several ways to install ``django-treebeard``. If you're not sure,
`just use pip <http://guide.python-distribute.org/pip.html>`_
pip (or easy_install)
~~~~~~~~~~~~~~~~~~~~~
You can install the release versions from
`django-treebeard's PyPI page`_ using ``pip``:
.. code-block:: console
$ pip install django-treebeard
or if for some reason you can't use ``pip``, you can try ``easy_install``,
(at your own risk):
.. code-block:: console
$ easy_install --always-unzip django-treebeard
setup.py
~~~~~~~~
Download a release from the `treebeard download page`_ and unpack it, then
run:
.. code-block:: console
$ python setup.py install
.deb packages
~~~~~~~~~~~~~
Both Debian and Ubuntu include ``django-treebeard`` as a package, so you can
just use:
.. code-block:: console
$ apt-get install python-django-treebeard
or:
.. code-block:: console
$ 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
:django:setting:`INSTALLED_APPS` section in your django
settings file.
.. note::
If you are going to use the :class:`~treebeard.admin.TreeAdmin`
class, you need to add the path to treebeard's templates in
:django:setting:`TEMPLATE_DIRS`.
Also you need to enable
``django.core.context_processors.request``
in the :django:setting:`TEMPLATE_CONTEXT_PROCESSORS`
setting in your django settings file.
.. _`django-treebeard's PyPI page`:
http://pypi.python.org/pypi/django-treebeard
.. _`treebeard download page`:
https://tabo.pe/projects/django-treebeard/download/

View file

@ -1,262 +0,0 @@
Materialized 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.
.. warning::
As with all tree implementations, please be aware of the
:doc:`caveats`.
.. 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.
.. 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 object. Doing so will
corrupt the tree.
.. warning::
If you need to define your own
:py:class:`~django.db.models.Manager` class,
you'll need to subclass
:py:class:`~MP_NodeManager`.
Also, if in your manager you need to change the default
queryset handler, you'll need to subclass
:py:class:`~MP_NodeQuerySet`.
Example:
.. code-block:: python
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:
.. code-block:: console
$ 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:
.. code-block:: python
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:
.. code-block:: python
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:
.. code-block:: python
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 per character.
.. note::
``django-treebeard`` uses `numconv`_ for path encoding.
.. 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:
.. code-block:: python
MyNodeModel.find_problems()
.. automethod:: fix_tree
Example:
.. code-block:: python
MyNodeModel.fix_tree()
.. autoclass:: MP_NodeManager
:show-inheritance:
.. autoclass:: MP_NodeQuerySet
:show-inheritance:
.. _`Vadim Tropashko`: http://vadimtropashko.wordpress.com/
.. _`Sql Design Patterns`:
http://www.rampant-books.com/book_2006_1_sql_coding_styles.htm
.. _numconv: https://tabo.pe/projects/numconv/

View file

@ -1,81 +0,0 @@
Nested 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.
.. warning::
As with all tree implementations, please be aware of the
:doc:`caveats`.
.. inheritance-diagram:: NS_Node
.. autoclass:: NS_Node
:show-inheritance:
.. warning::
If you need to define your own
:py:class:`~django.db.models.Manager` class,
you'll need to subclass
:py:class:`~NS_NodeManager`.
Also, if in your manager you need to change the default
queryset handler, you'll need to subclass
:py:class:`~NS_NodeQuerySet`.
.. 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:
.. code-block:: python
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.
.. autoclass:: NS_NodeManager
:show-inheritance:
.. autoclass:: NS_NodeQuerySet
:show-inheritance:
.. _`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

View file

@ -1,80 +0,0 @@
Running 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:
.. code-block:: console
$ 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:
- py26-dj14-sqlite
- py26-dj14-mysql
- py26-dj14-pgsql
- py26-dj15-sqlite
- py26-dj15-mysql
- py26-dj15-pgsql
- py26-dj16-sqlite
- py26-dj16-mysql
- py26-dj16-pgsql
- py27-dj14-sqlite
- py27-dj14-mysql
- py27-dj14-pgsql
- py27-dj15-sqlite
- py27-dj15-mysql
- py27-dj15-pgsql
- py32-dj15-sqlite
- py32-dj15-pgsql
- py33-dj15-sqlite
- py33-dj15-pgsq
- py27-dj16-sqlite
- py27-dj16-mysql
- py27-dj16-pgsql
- py32-dj16-sqlite
- py32-dj16-pgsql
- py33-dj16-sqlite
- py33-dj16-pgsql
This means that the test suite will run 26 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:
.. code-block:: console
$ tox -e py33-dj16-pgsql
.. _pytest: http://pytest.org/
.. _coverage: http://nedbatchelder.com/code/coverage/
.. _tox: http://codespeak.net/tox/

View file

@ -1,106 +0,0 @@
Tutorial
========
Create a basic model for your tree. In this example we'll use a Materialized
Path tree:
.. code-block:: python
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:
.. code-block:: console
$ python manage.py syncdb
Let's create some nodes:
.. code-block:: python
>>> from treebeard_tutorial.models import Category
>>> get = lambda node_id: Category.objects.get(pk=node_id)
>>> root = Category.add_root(name='Computer Hardware')
>>> node = get(root.pk).add_child(name='Memory')
>>> get(node.pk).add_sibling(name='Hard Drives')
<Category: Category: Hard Drives>
>>> get(node.pk).add_sibling(name='SSD')
<Category: Category: SSD>
>>> get(node.pk).add_child(name='Desktop Memory')
<Category: Category: Desktop Memory>
>>> get(node.pk).add_child(name='Laptop Memory')
<Category: Category: Laptop Memory>
>>> get(node.pk).add_child(name='Server Memory')
<Category: Category: 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. See: :doc:`caveats`.
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:
.. code-block:: python
>>> 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()
[(<Category: Category: Computer Hardware>,
{'close': [], 'level': 0, 'open': True}),
(<Category: Category: Hard Drives>,
{'close': [], 'level': 1, 'open': True}),
(<Category: Category: Memory>,
{'close': [], 'level': 1, 'open': False}),
(<Category: Category: Desktop Memory>,
{'close': [], 'level': 2, 'open': True}),
(<Category: Category: Laptop Memory>,
{'close': [], 'level': 2, 'open': False}),
(<Category: Category: Server Memory>,
{'close': [0], 'level': 2, 'open': False}),
(<Category: Category: SSD>,
{'close': [0, 1], 'level': 1, 'open': False})]
Read the :class:`treebeard.models.Node` API reference for detailed info.
.. _`treebeard mercurial repository`:
http://code.tabo.pe/django-treebeard
.. _`latest treebeard version from PyPi`:
http://pypi.python.org/pypi/django-treebeard/

View file

@ -1,59 +0,0 @@
#!/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.0b2',
url='https://tabo.pe/projects/django-treebeard/',
author='Gustavo Picon',
author_email='tabo@tabo.pe',
license='Apache License 2.0',
packages=['treebeard', 'treebeard.templatetags', 'treebeard.tests'],
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.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)

View file

@ -1,302 +0,0 @@
#
# tox.ini for django-treebeard
#
# Read docs/tests for help on how to use tox to run the test suite.
#
[tox]
envlist =
py26-dj14-sqlite,
py26-dj14-mysql,
py26-dj14-pgsql,
py26-dj15-sqlite,
py26-dj15-mysql,
py26-dj15-pgsql,
py26-dj16-sqlite,
py26-dj16-mysql,
py26-dj16-pgsql,
py27-dj14-sqlite,
py27-dj14-mysql,
py27-dj14-pgsql,
py27-dj15-sqlite,
py27-dj15-mysql,
py27-dj15-pgsql,
py32-dj15-sqlite,
py32-dj15-pgsql,
py33-dj15-sqlite,
py33-dj15-pgsql
py27-dj16-sqlite,
py27-dj16-mysql,
py27-dj16-pgsql,
py32-dj16-sqlite,
py32-dj16-pgsql,
py33-dj16-sqlite,
py33-dj16-pgsql
[testenv]
commands =
{envpython} treebeard/tests/jenkins/toxhelper.py \
--tb=long --fulltrace -l --junitxml junit-{envname}.xml \
{posargs}
[testenv:docs]
basepython=python
changedir = docs
deps =
Sphinx
Django
commands =
sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html
[testenv:py26-dj14-sqlite]
basepython=python2.6
deps =
Django>=1.4,<1.5
coverage
pytest
setenv =
DATABASE_ENGINE=sqlite3
[testenv:py26-dj14-mysql]
basepython=python2.6
deps =
Django>=1.4,<1.5
MySQL-python
coverage
pytest
setenv =
DATABASE_ENGINE=mysql
[testenv:py26-dj14-pgsql]
basepython=python2.6
deps =
Django>=1.4,<1.5
psycopg2>2.4.1
coverage
pytest
setenv =
DATABASE_ENGINE=postgresql_psycopg2
[testenv:py26-dj15-sqlite]
basepython=python2.6
deps =
Django>=1.5,<1.6
coverage
pytest
setenv =
DATABASE_ENGINE=sqlite3
[testenv:py26-dj15-mysql]
basepython=python2.6
deps =
Django>=1.5,<1.6
MySQL-python
coverage
pytest
setenv =
DATABASE_ENGINE=mysql
[testenv:py26-dj15-pgsql]
basepython=python2.6
deps =
Django>=1.5,<1.6
psycopg2>2.4.1
coverage
pytest
setenv =
DATABASE_ENGINE=postgresql_psycopg2
[testenv:py26-dj16-sqlite]
basepython=python2.6
deps =
Django>=1.6,<1.7
coverage
pytest
setenv =
DATABASE_ENGINE=sqlite3
[testenv:py26-dj16-mysql]
basepython=python2.6
deps =
Django>=1.6,<1.7
MySQL-python
coverage
pytest
setenv =
DATABASE_ENGINE=mysql
[testenv:py26-dj16-pgsql]
basepython=python2.6
deps =
Django>=1.6,<1.7
psycopg2>2.4.1
coverage
pytest
setenv =
DATABASE_ENGINE=postgresql_psycopg2
[testenv:py27-dj14-sqlite]
basepython=python2.7
deps =
Django>=1.4,<1.5
coverage
pytest
setenv =
DATABASE_ENGINE=sqlite3
[testenv:py27-dj14-mysql]
basepython=python2.7
deps =
Django>=1.4,<1.5
MySQL-python
coverage
pytest
setenv =
DATABASE_ENGINE=mysql
[testenv:py27-dj14-pgsql]
basepython=python2.7
deps =
Django>=1.4,<1.5
psycopg2>2.4.1
coverage
pytest
setenv =
DATABASE_ENGINE=postgresql_psycopg2
[testenv:py27-dj15-sqlite]
basepython=python2.7
deps =
Django>=1.5,<1.6
coverage
pytest
setenv =
DATABASE_ENGINE=sqlite3
[testenv:py27-dj15-mysql]
basepython=python2.7
deps =
Django>=1.5,<1.6
MySQL-python
coverage
pytest
setenv =
DATABASE_ENGINE=mysql
[testenv:py27-dj15-pgsql]
basepython=python2.7
deps =
Django>=1.5,<1.6
psycopg2>2.4.1
coverage
pytest
setenv =
DATABASE_ENGINE=postgresql_psycopg2
[testenv:py32-dj15-sqlite]
basepython=python3.2
deps =
Django>=1.5,<1.6
coverage
pytest
setenv =
DATABASE_ENGINE=sqlite3
[testenv:py32-dj15-pgsql]
basepython=python3.2
deps =
Django>=1.5,<1.6
psycopg2
coverage
pytest
setenv =
DATABASE_ENGINE=postgresql_psycopg2
[testenv:py33-dj15-sqlite]
basepython=python3.3
deps =
Django>=1.5,<1.6
coverage
pytest
setenv =
DATABASE_ENGINE=sqlite3
[testenv:py33-dj15-pgsql]
basepython=python3.3
deps =
Django>=1.5,<1.6
psycopg2
coverage
pytest
setenv =
DATABASE_ENGINE=postgresql_psycopg2
[testenv:py27-dj16-sqlite]
basepython=python2.7
deps =
Django>=1.6,<1.7
coverage
pytest
setenv =
DATABASE_ENGINE=sqlite3
[testenv:py27-dj16-mysql]
basepython=python2.7
deps =
Django>=1.6,<1.7
MySQL-python
coverage
pytest
setenv =
DATABASE_ENGINE=mysql
[testenv:py27-dj16-pgsql]
basepython=python2.7
deps =
Django>=1.6,<1.7
psycopg2>2.4.1
coverage
pytest
setenv =
DATABASE_ENGINE=postgresql_psycopg2
[testenv:py32-dj16-sqlite]
basepython=python3.2
deps =
Django>=1.6,<1.7
coverage
pytest
setenv =
DATABASE_ENGINE=sqlite3
[testenv:py32-dj16-pgsql]
basepython=python3.2
deps =
Django>=1.6,<1.7
psycopg2
coverage
pytest
setenv =
DATABASE_ENGINE=postgresql_psycopg2
[testenv:py33-dj16-sqlite]
basepython=python3.3
deps =
Django>=1.6,<1.7
coverage
pytest
setenv =
DATABASE_ENGINE=sqlite3
[testenv:py33-dj16-pgsql]
basepython=python3.3
deps =
Django>=1.6,<1.7
psycopg2
coverage
pytest
setenv =
DATABASE_ENGINE=postgresql_psycopg2

View file

@ -1 +0,0 @@
__version__ = '2.0b2'

View file

@ -1,111 +0,0 @@
"""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 try_to_move_node(self, as_child, node, pos, request, target):
try:
node.move(target, pos=pos)
# Call the save method on the (reloaded) node in order to trigger
# possible signal handlers etc.
node = self.get_node(node.pk)
node.save()
except (MissingNodeOrderBy, PathOverflow, InvalidMoveToDescendant,
InvalidPosition):
e = sys.exc_info()[1]
# An error was raised while trying to move the node, then set an
# error message and return 400, this will cause a reload on the
# client to show the message
messages.error(request,
_('Exception raised while moving node: %s') % _(
force_str(e)))
return HttpResponseBadRequest('Exception raised during move')
if as_child:
msg = _('Moved node "%(node)s" as child of "%(other)s"')
else:
msg = _('Moved node "%(node)s" as sibling of "%(other)s"')
messages.info(request, msg % {'node': node, 'other': target})
return HttpResponse('OK')
def move_node(self, request):
try:
node_id = request.POST['node_id']
target_id = request.POST['sibling_id']
as_child = bool(int(request.POST.get('as_child', 0)))
except (KeyError, ValueError):
# Some parameters were missing return a BadRequest
return HttpResponseBadRequest('Malformed POST params')
node = self.get_node(node_id)
target = self.get_node(target_id)
is_sorted = True if node.node_order_by else False
pos = {
(True, True): 'sorted-child',
(True, False): 'last-child',
(False, True): 'sorted-sibling',
(False, False): 'left',
}[as_child, is_sorted]
return self.try_to_move_node(as_child, node, pos, request, target)
def admin_factory(form_class):
"""Dynamically build a TreeAdmin subclass for the given form class.
:param form_class:
:return: A TreeAdmin subclass.
"""
return type(
form_class.__name__ + 'Admin',
(TreeAdmin,),
dict(form=form_class))

View file

@ -1,334 +0,0 @@
"""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 in an Adjacency List tree."""
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

View file

@ -1,24 +0,0 @@
"""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)
"""

View file

@ -1,229 +0,0 @@
"""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.
It adds two fields to the form:
- Relative to: The target node where the current node will
be moved to.
- Position: The position relative to the target node that
will be used to move the node. These can be:
- For sorted trees: ``Child of`` and ``Sibling of``
- For unsorted trees: ``First child of``, ``Before`` and
``After``
.. warning::
Subclassing :py:class:`MoveNodeForm` directly is
discouraged, since special care is needed to handle
excluded fields, and these change depending on the
tree type.
It is recommended that the :py:func:`movenodeform_factory`
function is used instead.
"""
__position_choices_sorted = (
('sorted-child', _('Child of')),
('sorted-sibling', _('Sibling of')),
)
__position_choices_unsorted = (
('first-child', _('First child of')),
('left', _('Before')),
('right', _('After')),
)
_position = forms.ChoiceField(required=True, label=_("Position"))
_ref_node_id = forms.TypedChoiceField(required=False,
coerce=int,
label=_("Relative to"))
def _get_position_ref_node(self, instance):
if self.is_sorted:
position = 'sorted-child'
node_parent = instance.get_parent()
if node_parent:
ref_node_id = node_parent.pk
else:
ref_node_id = ''
else:
prev_sibling = instance.get_prev_sibling()
if prev_sibling:
position = 'right'
ref_node_id = prev_sibling.pk
else:
position = 'first-child'
if instance.is_root():
ref_node_id = ''
else:
ref_node_id = instance.get_parent().pk
return {'_ref_node_id': ref_node_id,
'_position': position}
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
initial=None, error_class=ErrorList, label_suffix=':',
empty_permitted=False, instance=None):
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 '&nbsp;&nbsp;&nbsp;&nbsp;' * (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 Node model:
The subclass of :py:class:`Node` that will be handled
by the form.
:param form:
The form class that will be used as a base. By
default, :py:class:`MoveNodeForm` will be used.
:return: A :py:class:`MoveNodeForm` subclass
"""
_exclude = _get_exclude_for_model(model, exclude)
return django_modelform_factory(
model, form, fields, _exclude, formfield_callback, widgets)
def _get_exclude_for_model(model, exclude):
if exclude:
_exclude = tuple(exclude)
else:
_exclude = ()
if issubclass(model, AL_Node):
_exclude += ('sib_order', 'parent')
elif issubclass(model, MP_Node):
_exclude += ('depth', 'numchild', 'path')
elif issubclass(model, NS_Node):
_exclude += ('depth', 'lft', 'rgt', 'tree_id')
return _exclude

View file

@ -1,78 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Django-treebeard\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2011-07-18 17:36+0200\n"
"PO-Revision-Date: 2010-05-03 23:40-0500\n"
"Last-Translator: Gustavo Picon <tabo@tabo.pe>\n"
"Language-Team: Spanish\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: admin.py:113
#, python-format
msgid "Moved node \"%(node)s\" as child of \"%(other)s\""
msgstr ""
#: admin.py:119
#, python-format
msgid "Moved node \"%(node)s\" as sibling of \"%(other)s\""
msgstr ""
#: admin.py:129
#, python-format
msgid "Exception raised while moving node: %s"
msgstr ""
#: al_tree.py:319 mp_tree.py:641 ns_tree.py:308
msgid "Can't move node to a descendant."
msgstr ""
#: forms.py:17
msgid "Child of"
msgstr "Hijo de"
#: forms.py:18
msgid "Sibling of"
msgstr "Hermano de"
#: forms.py:22
msgid "First child of"
msgstr "Primer hijo de"
#: forms.py:23
msgid "Before"
msgstr "Antes"
#: forms.py:24
msgid "After"
msgstr "Después"
#: forms.py:27
msgid "Position"
msgstr "Posición"
#: forms.py:31
msgid "Relative to"
msgstr "Relativo a"
#: forms.py:81
msgid "-- root --"
msgstr "-- raíz --"
#: mp_tree.py:521
msgid ""
"The new node is too deep in the tree, try increasing the path.max_length "
"property and UPDATE your database"
msgstr ""
#: mp_tree.py:702
#, python-format
msgid "Path Overflow from: '%s'"
msgstr ""
#: templatetags/admin_tree.py:148
msgid "Return to ordered tree"
msgstr ""

View file

@ -1,24 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Django-treebeard\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2011-07-18 14:12+0200\n"
"PO-Revision-Date: 2011-07-18 14:12+0200\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: static/treebeard/treebeard-admin.js:157
msgid "Abort"
msgstr ""
#: static/treebeard/treebeard-admin.js:172
msgid "As Sibling"
msgstr ""
#: static/treebeard/treebeard-admin.js:190
msgid "As child"
msgstr ""

View file

@ -1,80 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Django-treebeard\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2011-07-18 17:36+0200\n"
"PO-Revision-Date: 2011-07-18 14:11+0200\n"
"Last-Translator: Jaap Roes <jaap@eight.nl>\n"
"Language-Team: Dutch\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: admin.py:113
#, python-format
msgid "Moved node \"%(node)s\" as child of \"%(other)s\""
msgstr "\"%(node)s\" is nu onderdeel van \"%(other)s\""
#: admin.py:119
#, python-format
msgid "Moved node \"%(node)s\" as sibling of \"%(other)s\""
msgstr "\"%(node)s\" staat nu voor \"%(other)s\""
#: admin.py:129
#, python-format
msgid "Exception raised while moving node: %s"
msgstr "Fatale fout tijdens het verplaatsen: %s"
#: al_tree.py:319 mp_tree.py:641 ns_tree.py:308
msgid "Can't move node to a descendant."
msgstr "Kan node niet naar eigen subnode verplaatsen"
#: forms.py:17
msgid "Child of"
msgstr "Onderdeel"
#: forms.py:18
msgid "Sibling of"
msgstr "Naast"
#: forms.py:22
msgid "First child of"
msgstr "1e onderdeel"
#: forms.py:23
msgid "Before"
msgstr "Voor"
#: forms.py:24
msgid "After"
msgstr "Na"
#: forms.py:27
msgid "Position"
msgstr "Positie"
#: forms.py:31
msgid "Relative to"
msgstr "Ten opzichte van"
#: forms.py:81
msgid "-- root --"
msgstr "-- hoofdniveau --"
#: mp_tree.py:521
msgid ""
"The new node is too deep in the tree, try increasing the path.max_length "
"property and UPDATE your database"
msgstr ""
"De nieuwe node bevindt zich te diep in de boom. Verhoog de path.max_lenght "
"waarde en UPDATE de database."
#: mp_tree.py:702
#, python-format
msgid "Path Overflow from: '%s'"
msgstr "Path overflow van: '%s'"
#: templatetags/admin_tree.py:148
msgid "Return to ordered tree"
msgstr "Als gesorteerde boom"

View file

@ -1,24 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Django-treebeard\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2011-07-18 14:12+0200\n"
"PO-Revision-Date: 2011-07-18 14:12+0200\n"
"Last-Translator: Jaap Roes <jaap@eight.nl>\n"
"Language-Team: Dutch\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: static/treebeard/treebeard-admin.js:157
msgid "Abort"
msgstr "Annuleren"
#: static/treebeard/treebeard-admin.js:172
msgid "As Sibling"
msgstr "Als naastliggend onderdeel"
#: static/treebeard/treebeard-admin.js:190
msgid "As child"
msgstr "Als subonderdeel"

View file

@ -1,44 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Django-treebeard\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2010-05-03 23:53-0500\n"
"PO-Revision-Date: 2010-05-03 23:40-0500\n"
"Last-Translator: Bartosz Turkot <bartosz.turkot@blueservices.pl>\n"
"Language-Team: Polish\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: forms.py:16
msgid "Child of"
msgstr "Dziecko kategorii"
#: forms.py:17
msgid "Sibling of"
msgstr "Sąsiad kategorii"
#: forms.py:21
msgid "First child of"
msgstr "Pierwsze dziecko kategorii"
#: forms.py:22
msgid "Before"
msgstr "Przed"
#: forms.py:23
msgid "After"
msgstr "Za"
#: forms.py:26
msgid "Position"
msgstr "Pozycja"
#: forms.py:30
msgid "Relative to"
msgstr "Względem"
#: forms.py:80
msgid "-- root --"
msgstr "-- kategoria główna --"

View file

@ -1,79 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Django-treebeard\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2011-07-18 17:36+0200\n"
"PO-Revision-Date: 2009-04-10 18:37+0400\n"
"Last-Translator: chembervint <chembervint@gmail.com>\n"
"Language-Team: Russian\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%"
"10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
#: admin.py:113
#, python-format
msgid "Moved node \"%(node)s\" as child of \"%(other)s\""
msgstr ""
#: admin.py:119
#, python-format
msgid "Moved node \"%(node)s\" as sibling of \"%(other)s\""
msgstr ""
#: admin.py:129
#, python-format
msgid "Exception raised while moving node: %s"
msgstr ""
#: al_tree.py:319 mp_tree.py:641 ns_tree.py:308
msgid "Can't move node to a descendant."
msgstr ""
#: forms.py:17
msgid "Child of"
msgstr "Вложенный"
#: forms.py:18
msgid "Sibling of"
msgstr "Соседний к"
#: forms.py:22
msgid "First child of"
msgstr "Первый вложенный"
#: forms.py:23
msgid "Before"
msgstr "До"
#: forms.py:24
msgid "After"
msgstr "После"
#: forms.py:27
msgid "Position"
msgstr "Позиция"
#: forms.py:31
msgid "Relative to"
msgstr "Относительно"
#: forms.py:81
msgid "-- root --"
msgstr "-- корень --"
#: mp_tree.py:521
msgid ""
"The new node is too deep in the tree, try increasing the path.max_length "
"property and UPDATE your database"
msgstr ""
#: mp_tree.py:702
#, python-format
msgid "Path Overflow from: '%s'"
msgstr ""
#: templatetags/admin_tree.py:148
msgid "Return to ordered tree"
msgstr ""

View file

@ -1,25 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Django-treebeard\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2011-07-18 14:12+0200\n"
"PO-Revision-Date: 2011-07-18 14:12+0200\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%"
"10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
#: static/treebeard/treebeard-admin.js:157
msgid "Abort"
msgstr ""
#: static/treebeard/treebeard-admin.js:172
msgid "As Sibling"
msgstr ""
#: static/treebeard/treebeard-admin.js:190
msgid "As child"
msgstr ""

View file

@ -1,620 +0,0 @@
"""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):
return {
'read': connections[router.db_for_read(cls)],
'write': connections[router.db_for_write(cls)]
}[action]
@classmethod
def get_database_vendor(cls, action):
"""
returns the supported database vendor used by a treebeard model when
performing read (select) or write (update, insert, delete) operations.
:param action:
`read` or `write`
:returns: postgresql, mysql or sqlite
"""
return cls._get_database_connection(action).vendor
@classmethod
def _get_database_cursor(cls, action):
return cls._get_database_connection(action).cursor()
class Meta:
"""Abstract model."""
abstract = True

File diff suppressed because it is too large Load diff

View file

@ -1,623 +0,0 @@
"""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 in a Nested Sets tree."""
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

View file

@ -1,115 +0,0 @@
"""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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,020 B

View file

@ -1,83 +0,0 @@
/* 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;
}*/

View file

@ -1,314 +0,0 @@
(function ($) {
// Ok, let's do eeet
ACTIVE_NODE_BG_COLOR = '#B7D7E8';
RECENTLY_MOVED_COLOR = '#FFFF00';
RECENTLY_MOVED_FADEOUT = '#FFFFFF';
ABORT_COLOR = '#EECCCC';
DRAG_LINE_COLOR = '#AA00AA';
RECENTLY_FADE_DURATION = 2000;
// This is the basic Node class, which handles UI tree operations for each 'row'
var Node = function (elem) {
var $elem = $(elem);
var node_id = $elem.attr('node');
var parent_id = $elem.attr('parent');
var level = parseInt($elem.attr('level'));
var children_num = parseInt($elem.attr('children-num'));
return {
elem: elem,
$elem: $elem,
node_id: node_id,
parent_id: parent_id,
level: level,
has_children: function () {
return children_num > 0;
},
node_name: function () {
// Returns the text of the node
return $elem.find('th a:not(.collapse)').text();
},
is_collapsed: function () {
return $elem.find('a.collapse').hasClass('collapsed');
},
children: function () {
return $('tr[parent=' + node_id + ']');
},
collapse: function () {
// For each children, hide it's childrens and so on...
$.each(this.children(),function () {
var node = new Node(this);
node.collapse();
}).hide();
// Swicth class to set the proprt expand/collapse icon
$elem.find('a.collapse').removeClass('expanded').addClass('collapsed');
},
parent_node: function () {
// Returns a Node object of the parent
return new Node($('tr[node=' + parent_id + ']', $elem.parent())[0]);
},
expand: function () {
// Display each kid (will display in collapsed state)
this.children().show();
// Swicth class to set the proprt expand/collapse icon
$elem.find('a.collapse').removeClass('collapsed').addClass('expanded');
},
toggle: function () {
if (this.is_collapsed()) {
this.expand();
} else {
this.collapse();
}
},
clone: function () {
return $elem.clone();
}
}
};
$(document).ready(function () {
// begin csrf token code
// Taken from http://docs.djangoproject.com/en/dev/ref/contrib/csrf/#ajax
$(document).ajaxSend(function (event, xhr, settings) {
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie != '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) == (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
if (!(/^http:.*/.test(settings.url) || /^https:.*/.test(settings.url))) {
// Only send the token to relative URLs i.e. locally.
xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
}
});
// end csrf token code
// Don't activate drag or collapse if GET filters are set on the page
if ($('#has-filters').val() === "1") {
return;
}
$body = $('body');
// Activate all rows for drag & drop
// then bind mouse down event
$('td.drag-handler span').addClass('active').bind('mousedown', function (evt) {
$ghost = $('<div id="ghost"></div>');
$drag_line = $('<div id="drag_line"><span></span></div>');
$ghost.appendTo($body);
$drag_line.appendTo($body);
var stop_drag = function () {
$ghost.remove();
$drag_line.remove();
$body.enableSelection().unbind('mousemove').unbind('mouseup');
node.elem.removeAttribute('style');
};
// Create a clone create the illusion that we're moving the node
var node = new Node($(this).closest('tr')[0]);
cloned_node = node.clone();
node.$elem.css({
'background': ACTIVE_NODE_BG_COLOR
});
$targetRow = null;
as_child = false;
// Now make the new clone move with the mouse
$body.disableSelection().bind('mousemove',function (evt2) {
$ghost.html(cloned_node).css({ // from FeinCMS :P
'opacity': .8,
'position': 'absolute',
'top': evt2.pageY,
'left': evt2.pageX - 30,
'width': 600
});
// Iterate through all rows and see where am I moving so I can place
// the drag line accordingly
rowHeight = node.$elem.height();
$('tr', node.$elem.parent()).each(function (index, element) {
$row = $(element);
rtop = $row.offset().top;
// The tooltop will display whether I'm droping the element as
// child or sibling
$tooltip = $drag_line.find('span');
$tooltip.css({
'left': node.$elem.width() - $tooltip.width(),
'height': rowHeight,
});
node_top = node.$elem.offset().top;
// Check if you are dragging over the same node
if (evt2.pageY >= node_top && evt2.pageY <= node_top + rowHeight) {
$targetRow = null;
$tooltip.text(gettext('Abort'));
$drag_line.css({
'top': node_top,
'height': rowHeight,
'borderWidth': 0,
'opacity': 0.8,
'backgroundColor': ABORT_COLOR
});
} else
// Check if mouse is over this row
if (evt2.pageY >= rtop && evt2.pageY <= rtop + rowHeight / 2) {
// The mouse is positioned on the top half of a $row
$targetRow = $row;
as_child = false;
$drag_line.css({
'left': node.$elem.offset().left,
'width': node.$elem.width(),
'top': rtop,
'borderWidth': '5px',
'height': 0,
'opacity': 1
});
$tooltip.text(gettext('As Sibling'));
} else if (evt2.pageY >= rtop + rowHeight / 2 && evt2.pageY <= rtop + rowHeight) {
// The mouse is positioned on the bottom half of a row
$targetRow = $row;
target_node = new Node($targetRow[0]);
if (target_node.is_collapsed()) {
target_node.expand();
}
as_child = true;
$drag_line.css({
'top': rtop,
'left': node.$elem.offset().left,
'height': rowHeight,
'opacity': 0.4,
'width': node.$elem.width(),
'borderWidth': 0,
'backgroundColor': DRAG_LINE_COLOR
});
$tooltip.text(gettext('As child'));
}
});
}).bind('mouseup',function () {
if ($targetRow !== null) {
target_node = new Node($targetRow[0]);
if (target_node.node_id !== node.node_id) {
/*alert('Insert node ' + node.node_name() + ' as child of: '
+ target_node.parent_node().node_name() + '\n and sibling of: '
+ target_node.node_name());*/
// Call $.ajax so we can handle the error
// On Drop, make an XHR call to perform the node move
$.ajax({
url: window.MOVE_NODE_ENDPOINT,
type: 'POST',
data: {
node_id: node.node_id,
parent_id: target_node.parent_id,
sibling_id: target_node.node_id,
as_child: as_child ? 1 : 0
},
complete: function (req, status) {
// http://stackoverflow.com/questions/1439895/add-a-hash-with-javascript-to-url-without-scrolling-page/1439910#1439910
node.$elem.remove();
window.location.hash = 'node-' + node.node_id;
window.location.reload();
},
error: function (req, status, error) {
// On error (!200) also reload to display
// the message
node.$elem.remove();
window.location.hash = 'node-' + node.node_id;
window.location.reload();
}
});
}
}
stop_drag();
}).bind('keyup', function (kbevt) {
// Cancel drag on escape
if (kbevt.keyCode === 27) {
stop_drag();
}
});
});
$('a.collapse').click(function () {
var node = new Node($(this).closest('tr')[0]); // send the DOM node, not jQ
node.toggle();
return false;
});
var hash = window.location.hash;
// This is a hack, the actual element's id ends in '-id' but the url's hash
// doesn't, I'm doing this to avoid scrolling the page... is that a good thing?
if (hash) {
$(hash + '-id').animate({
backgroundColor: RECENTLY_MOVED_COLOR
}, RECENTLY_FADE_DURATION, function () {
$(this).animate({
backgroundColor: RECENTLY_MOVED_FADEOUT
}, RECENTLY_FADE_DURATION, function () {
this.removeAttribute('style');
});
});
}
});
})(django.jQuery);
// http://stackoverflow.com/questions/190560/jquery-animate-backgroundcolor/2302005#2302005
(function (d) {
d.each(["backgroundColor", "borderBottomColor", "borderLeftColor", "borderRightColor", "borderTopColor", "color", "outlineColor"], function (f, e) {
d.fx.step[e] = function (g) {
if (!g.colorInit) {
g.start = c(g.elem, e);
g.end = b(g.end);
g.colorInit = true
}
g.elem.style[e] = "rgb(" + [Math.max(Math.min(parseInt((g.pos * (g.end[0] - g.start[0])) + g.start[0]), 255), 0), Math.max(Math.min(parseInt((g.pos * (g.end[1] - g.start[1])) + g.start[1]), 255), 0), Math.max(Math.min(parseInt((g.pos * (g.end[2] - g.start[2])) + g.start[2]), 255), 0)].join(",") + ")"
}
});
function b(f) {
var e;
if (f && f.constructor == Array && f.length == 3) {
return f
}
if (e = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(f)) {
return[parseInt(e[1]), parseInt(e[2]), parseInt(e[3])]
}
if (e = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(f)) {
return[parseFloat(e[1]) * 2.55, parseFloat(e[2]) * 2.55, parseFloat(e[3]) * 2.55]
}
if (e = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(f)) {
return[parseInt(e[1], 16), parseInt(e[2], 16), parseInt(e[3], 16)]
}
if (e = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(f)) {
return[parseInt(e[1] + e[1], 16), parseInt(e[2] + e[2], 16), parseInt(e[3] + e[3], 16)]
}
if (e = /rgba\(0, 0, 0, 0\)/.exec(f)) {
return a.transparent
}
return a[d.trim(f).toLowerCase()]
}
function c(g, e) {
var f;
do {
f = d.css(g, e);
if (f != "" && f != "transparent" || d.nodeName(g, "body")) {
break
}
e = "backgroundColor"
} while (g = g.parentNode);
return b(f)
}
var a = {aqua: [0, 255, 255], azure: [240, 255, 255], beige: [245, 245, 220], black: [0, 0, 0], blue: [0, 0, 255], brown: [165, 42, 42], cyan: [0, 255, 255], darkblue: [0, 0, 139], darkcyan: [0, 139, 139], darkgrey: [169, 169, 169], darkgreen: [0, 100, 0], darkkhaki: [189, 183, 107], darkmagenta: [139, 0, 139], darkolivegreen: [85, 107, 47], darkorange: [255, 140, 0], darkorchid: [153, 50, 204], darkred: [139, 0, 0], darksalmon: [233, 150, 122], darkviolet: [148, 0, 211], fuchsia: [255, 0, 255], gold: [255, 215, 0], green: [0, 128, 0], indigo: [75, 0, 130], khaki: [240, 230, 140], lightblue: [173, 216, 230], lightcyan: [224, 255, 255], lightgreen: [144, 238, 144], lightgrey: [211, 211, 211], lightpink: [255, 182, 193], lightyellow: [255, 255, 224], lime: [0, 255, 0], magenta: [255, 0, 255], maroon: [128, 0, 0], navy: [0, 0, 128], olive: [128, 128, 0], orange: [255, 165, 0], pink: [255, 192, 203], purple: [128, 0, 128], violet: [128, 0, 128], red: [255, 0, 0], silver: [192, 192, 192], white: [255, 255, 255], yellow: [255, 255, 0], transparent: [255, 255, 255]}
})(django.jQuery);

View file

@ -1,23 +0,0 @@
{# 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 %}

View file

@ -1,38 +0,0 @@
{% if result_hidden_fields %}
<div class="hiddenfields"> {# DIV for HTML validation #}
{% for item in result_hidden_fields %}{{ item }}{% endfor %}
</div>
{% endif %}
{% if results %}
<table cellspacing="0" id="result_list">
<thead>
<tr>
{% for header in result_headers %}
<th{{ header.class_attrib }}>
{% if header.sortable %}<a href="{{ header.url }}"
{% if header.tooltip %}title="{{ header.tooltip }}"{% endif %}>{% endif %}
{{ header.text|capfirst }}
{% if header.sortable %}</a>{% endif %}</th>{% endfor %}
</tr>
</thead>
<tbody>
{% for node_id, parent_id, node_level, has_children, result in results %}
<tr id="node-{{ node_id }}-id" class="{% cycle 'row1' 'row2' %}"
level="{{ node_level }}" children-num="{{ children_num }}"
parent="{{ parent_id }}" node="{{ node_id }}">
{% for item in result %}
{% if forloop.counter == 1 %}
{% for spacer in item.depth %}<span class="grab">&nbsp;
</span>{% endfor %}
{% endif %}
{{ item }}
{% endfor %}</tr>
{% endfor %}
</tbody>
</table>
<input type="hidden" id="has-filters" value="{{ filtered|yesno:"1,0" }}"/>
<script>
var MOVE_NODE_ENDPOINT = 'move/';
</script>
{% endif %}

View file

@ -1,21 +0,0 @@
{# 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 %}

View file

@ -1,7 +0,0 @@
{% if results %}
<ul>
{% for result in results %}
<li class="{% cycle 'row1' 'row2' %}">{{ result }}</li>
{% endfor %}
</ul>
{% endif %}

View file

@ -1,51 +0,0 @@
import datetime
import decimal
from django.template import Variable, VariableDoesNotExist
from django.utils import formats, timezone, six
from django.utils.encoding import smart_text
from django.utils.html import conditional_escape
from django.utils.safestring import mark_safe
action_form_var = Variable('action_form')
def needs_checkboxes(context):
try:
return action_form_var.resolve(context) is not None
except VariableDoesNotExist:
return False
def display_for_value(value, boolean=False): # pragma: no cover
""" Added for compatibility with django 1.4, copied from django trunk.
"""
from django.contrib.admin.templatetags.admin_list import _boolean_icon
from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
if boolean:
return _boolean_icon(value)
elif value is None:
return EMPTY_CHANGELIST_VALUE
elif isinstance(value, datetime.datetime):
return formats.localize(timezone.template_localtime(value))
elif isinstance(value, (datetime.date, datetime.time)):
return formats.localize(value)
elif isinstance(value, six.integer_types + (decimal.Decimal, float)):
return formats.number_format(value)
else:
return smart_text(value)
def format_html(format_string, *args, **kwargs): # pragma: no cover
"""
Added for compatibility with django 1.4, copied from django trunk.
Similar to str.format, but passes all arguments through conditional_escape,
and calls 'mark_safe' on the result. This function should be used instead
of str.format or % interpolation to build up small HTML fragments.
"""
args_safe = map(conditional_escape, args)
kwargs_safe = dict([(k, conditional_escape(v)) for (k, v) in
six.iteritems(kwargs)])
return mark_safe(format_string.format(*args_safe, **kwargs_safe))

View file

@ -1,279 +0,0 @@
# -*- coding: utf-8 -*-
"""
Templatetags for django-treebeard to add drag and drop capabilities to the
nodes change list - @jjdelc
"""
import datetime
import sys
from django.db import models
from django.conf import settings
from django.contrib.admin.templatetags.admin_list import (
result_headers, result_hidden_fields)
from django.contrib.admin.util import lookup_field, display_for_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
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
if sys.version < '3':
import codecs
def u(x):
return codecs.unicode_escape_decode(x)[0]
else:
def u(x):
return x
register = Library()
if sys.version_info >= (3, 0):
from django.utils.encoding import force_str, 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
try:
from django.contrib.admin.util import display_for_value
from django.utils.html import format_html
except ImportError:
from treebeard.templatetags import display_for_value, format_html
from treebeard.templatetags import needs_checkboxes
def get_result_and_row_class(cl, field_name, result):
row_class = ''
try:
f, attr, value = lookup_field(field_name, result, cl.model_admin)
except ObjectDoesNotExist:
result_repr = EMPTY_CHANGELIST_VALUE
else:
if f is None:
if field_name == 'action_checkbox':
row_class = mark_safe(' class="action-checkbox"')
allow_tags = getattr(attr, 'allow_tags', False)
boolean = getattr(attr, 'boolean', False)
if boolean:
allow_tags = True
result_repr = display_for_value(value, boolean)
# Strip HTML tags in the resulting text, except if the
# function has an "allow_tags" attribute set to True.
if allow_tags:
result_repr = mark_safe(result_repr)
if isinstance(value, (datetime.date, datetime.time)):
row_class = mark_safe(' class="nowrap"')
else:
if isinstance(f.rel, models.ManyToOneRel):
field_val = getattr(result, f.name)
if field_val is None:
result_repr = EMPTY_CHANGELIST_VALUE
else:
result_repr = field_val
else:
result_repr = display_for_field(value, f)
if isinstance(f, (models.DateField, models.TimeField,
models.ForeignKey)):
row_class = mark_safe(' class="nowrap"')
if force_str(result_repr) == '':
result_repr = mark_safe('&nbsp;')
return result_repr, row_class
def get_spacer(first, result):
if first:
spacer = '<span class="spacer">&nbsp;</span>' * (
result.get_depth() - 1)
else:
spacer = ''
return spacer
def get_collapse(result):
if result.get_children_count():
collapse = ('<a href="#" title="" class="collapse expanded">'
'-</a>')
else:
collapse = '<span class="collapse">&nbsp;</span>'
return collapse
def get_drag_handler(first):
drag_handler = ''
if first:
drag_handler = ('<td class="drag-handler">'
'<span>&nbsp;</span></td>')
return drag_handler
def items_for_result(cl, result, form):
"""
Generates the actual list of data.
@jjdelc:
This has been shamelessly copied from original
django.contrib.admin.templatetags.admin_list.items_for_result
in order to alter the dispay for the first element
"""
first = True
pk = cl.lookup_opts.pk.attname
for field_name in cl.list_display:
result_repr, row_class = get_result_and_row_class(cl, field_name,
result)
# If list_display_links not defined, add the link tag to the
# first field
if (first and not cl.list_display_links) or \
field_name in cl.list_display_links:
table_tag = {True: 'th', False: 'td'}[first]
# This spacer indents the nodes based on their depth
spacer = get_spacer(first, result)
# This shows a collapse or expand link for nodes with childs
collapse = get_collapse(result)
# Add a <td/> before the first col to show the drag handler
drag_handler = get_drag_handler(first)
first = False
url = cl.url_for_result(result)
# Convert the pk to something that can be used in Javascript.
# Problem cases are long ints (23L) and non-ASCII strings.
if cl.to_field:
attr = str(cl.to_field)
else:
attr = pk
value = result.serializable_value(attr)
result_id = repr(force_str(value))[1:]
onclickstr = (
' onclick="opener.dismissRelatedLookupPopup(window, %s);'
' return false;"')
yield mark_safe(
u('%s<%s%s>%s %s <a href="%s"%s>%s</a></%s>') % (
drag_handler, table_tag, row_class, spacer, collapse, url,
(cl.is_popup and onclickstr % result_id or ''),
conditional_escape(result_repr), table_tag))
else:
# By default the fields come from ModelAdmin.list_editable, but if
# we pull the fields out of the form instead of list_editable
# custom admins can provide fields on a per request basis
if (form and field_name in form.fields and not (
field_name == cl.model._meta.pk.name and
form[cl.model._meta.pk.name].is_hidden)):
bf = form[field_name]
result_repr = mark_safe(force_str(bf.errors) + force_str(bf))
yield format_html(u('<td{0}>{1}</td>'), row_class, result_repr)
if form and not form[cl.model._meta.pk.name].is_hidden:
yield format_html(u('<td>{0}</td>'),
force_str(form[cl.model._meta.pk.name]))
def get_parent_id(node):
"""Return the node's parent id or 0 if node is a root node."""
if node.is_root():
return 0
return node.get_parent().pk
def results(cl):
if cl.formset:
for res, form in zip(cl.result_list, cl.formset.forms):
yield (res.pk, get_parent_id(res), res.get_depth(),
res.get_children_count(),
list(items_for_result(cl, res, form)))
else:
for res in cl.result_list:
yield (res.pk, get_parent_id(res), res.get_depth(),
res.get_children_count(),
list(items_for_result(cl, res, None)))
def check_empty_dict(GET_dict):
"""
Returns True if the GET querstring contains on values, but it can contain
empty keys.
This is better than doing not bool(request.GET) as an empty key will return
True
"""
empty = True
for k, v in GET_dict.items():
# Don't disable on p(age) or 'all' GET param
if v and k != 'p' and k != 'all':
empty = False
return empty
@register.inclusion_tag(
'admin/tree_change_list_results.html', takes_context=True)
def result_tree(context, cl, request):
"""
Added 'filtered' param, so the template's js knows whether the results have
been affected by a GET param or not. Only when the results are not filtered
you can drag and sort the tree
"""
# Here I'm adding an extra col on pos 2 for the drag handlers
headers = list(result_headers(cl))
headers.insert(1 if needs_checkboxes(context) else 0, {
'text': '+',
'sortable': True,
'url': request.path,
'tooltip': _('Return to ordered tree'),
'class_attrib': mark_safe(' class="oder-grabber"')
})
return {
'filtered': not check_empty_dict(request.GET),
'result_hidden_fields': list(result_hidden_fields(cl)),
'result_headers': headers,
'results': list(results(cl)),
}
def get_static_url():
"""Return a base static url, always ending with a /"""
path = getattr(settings, 'STATIC_URL', None)
if not path:
path = getattr(settings, 'MEDIA_URL', None)
if not path:
path = '/'
return path
@register.simple_tag
def treebeard_css():
"""
Template tag to print out the proper <link/> tag to include a custom .css
"""
LINK_HTML = """<link rel="stylesheet" type="text/css" href="%s"/>"""
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 <script/> tag to include a custom .js
"""
path = get_static_url()
SCRIPT_HTML = """<script type="text/javascript" src="%s"></script>"""
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 = ("<script>"
"(function($){jQuery = $.noConflict(true);})(django.jQuery);"
"</script>"
"<script type=\"text/javascript\" src=\"%s\"></script>")
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)

View file

@ -1,40 +0,0 @@
# -*- coding: utf-8 -*-
from django.template import Library
from treebeard.templatetags import needs_checkboxes
register = Library()
CHECKBOX_TMPL = ('<input type="checkbox" class="action-select" value="%d" '
'name="_selected_action" />')
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 + '<a href="%d/" %s>%s</a>' % (
node.pk, raw_id_fields, str(node))
def _subtree(context, node, request):
tree = ''
for subnode in node.get_children():
tree += '<li>%s</li>' % _subtree(context, subnode, request)
if tree:
tree = '<ul>%s</ul>' % 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 += '<li>%s</li>' % _subtree(context, root_node, request)
return "<ul>%s</ul>" % tree

Some files were not shown because too many files have changed in this diff Show more