diff --git a/.gitignore b/.gitignore index 890183617..bd904efc9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /MANIFEST /wagtail.egg-info/ /docs/_build/ +/.tox/ diff --git a/.travis.yml b/.travis.yml index e23794d4f..cc1cb74e8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/CHANGELOG.txt b/CHANGELOG.txt index d1e313295..0a6024bd1 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -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
, 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) ~~~~~~~~~~~~~~~~ diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst index 0c4153a7d..d7222307d 100644 --- a/CONTRIBUTORS.rst +++ b/CONTRIBUTORS.rst @@ -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 diff --git a/README.rst b/README.rst index 62d42aa83..d967dcb1f 100644 --- a/README.rst +++ b/README.rst @@ -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 `_-friendly if you need it * Tests! But not enough; we're working hard to improve this -Find out more at `wagtail.io `_. Documentation is at `wagtail.readthedocs.org `_. +Find out more at `wagtail.io `_. Got a question? Ask it on our `Google Group `_. @@ -36,6 +36,15 @@ Getting started * See the `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 `_ has written a `tutorial `_ with all the steps to build a simple Wagtail site from scratch. +Documentation +~~~~~~~~~~~~~ +Available at `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 `_. Our immediate priorities are better docs, more tests, internationalisation and localisation. + diff --git a/docs/advanced_topics.rst b/docs/advanced_topics.rst new file mode 100644 index 000000000..7e23d8442 --- /dev/null +++ b/docs/advanced_topics.rst @@ -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) diff --git a/docs/building_your_site.rst b/docs/building_your_site.rst deleted file mode 100644 index b506f67f2..000000000 --- a/docs/building_your_site.rst +++ /dev/null @@ -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/ `_ \ No newline at end of file diff --git a/docs/building_your_site/djangodevelopers.rst b/docs/building_your_site/djangodevelopers.rst new file mode 100644 index 000000000..b4899d70f --- /dev/null +++ b/docs/building_your_site/djangodevelopers.rst @@ -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 ```` 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. `_ + +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. diff --git a/docs/building_your_site/frontenddevelopers.rst b/docs/building_your_site/frontenddevelopers.rst new file mode 100644 index 000000000..d39bd8c33 --- /dev/null +++ b/docs/building_your_site/frontenddevelopers.rst @@ -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 `. + +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 `_ + + +.. _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 %} + + + {% 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 %} + ... + + +**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 %} + ... + + + + + +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 %} diff --git a/docs/building_your_site/index.rst b/docs/building_your_site/index.rst new file mode 100644 index 000000000..da5cf4f2d --- /dev/null +++ b/docs/building_your_site/index.rst @@ -0,0 +1,11 @@ +Building your site +================== + +.. note:: + This documentation is currently incomplete. + +.. toctree:: + :maxdepth: 3 + + djangodevelopers + frontenddevelopers diff --git a/docs/editing_api.rst b/docs/editing_api.rst new file mode 100644 index 000000000..99e859437 --- /dev/null +++ b/docs/editing_api.rst @@ -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 +~~~~~~~~~~~~~~~~ + + + diff --git a/docs/editor_manual/index.rst b/docs/editor_manual/index.rst index 4562844e7..b7b5c84f7 100644 --- a/docs/editor_manual/index.rst +++ b/docs/editor_manual/index.rst @@ -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 diff --git a/docs/form_builder.rst b/docs/form_builder.rst new file mode 100644 index 000000000..9aa220e19 --- /dev/null +++ b/docs/form_builder.rst @@ -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 %} + + + {{ self.title }} + + +

{{ self.title }}

+ {{ self.intro|richtext }} +
+ {% csrf_token %} + {{ form.as_p }} + +
+ + + +form_page_landing.html is a regular Wagtail template, displayed after the user makes a successful form submission. diff --git a/docs/gettingstarted.rst b/docs/gettingstarted.rst index e4d2d6484..458de387b 100644 --- a/docs/gettingstarted.rst +++ b/docs/gettingstarted.rst @@ -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 `_ +- `libjpeg `_ +- `libxml2 `_ +- `libxslt `_ +- `zlib `_ 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 diff --git a/docs/index.rst b/docs/index.rst index 96ff94c23..abbb7fdbe 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -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 diff --git a/docs/model_recipes.rst b/docs/model_recipes.rst new file mode 100644 index 000000000..fe8a89e32 --- /dev/null +++ b/docs/model_recipes.rst @@ -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 %} +
{{ tag }} + {% 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 + + + + diff --git a/docs/roadmap.rst b/docs/roadmap.rst index c94692fac..bdc711470 100644 --- a/docs/roadmap.rst +++ b/docs/roadmap.rst @@ -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 `_ gives a detailed * More and better tests (>80% `coverage `_) * 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 `_) * Site settings management - * Edit bird for logged-in visitors * Support for an HTML content type * Simple inline stats diff --git a/docs/snippets.rst b/docs/snippets.rst new file mode 100644 index 000000000..a40b57d94 --- /dev/null +++ b/docs/snippets.rst @@ -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 %} +

+ + {{ advert.text }} + +

+ {% 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 %} +

{{ advert_placement.advert.text }}

+ {% endfor %} + {% endif %} + + diff --git a/docs/static_site_generation.rst b/docs/static_site_generation.rst new file mode 100644 index 000000000..a9379cd67 --- /dev/null +++ b/docs/static_site_generation.rst @@ -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 `_ 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 diff --git a/docs/wagtail_search.rst b/docs/wagtail_search.rst index 1bef60724..642a5e741 100644 --- a/docs/wagtail_search.rst +++ b/docs/wagtail_search.rst @@ -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 + +
+ + +
+ +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 %} +

Search Results{% if request.GET.q %} for {{ request.GET.q }}{% endif %}

+
    + {% for result in search_results %} +
  • +

    {{ result.specific }}

    + {% if result.specific.search_description %} + {{ result.specific.search_description|safe }} + {% endif %} +
  • + {% empty %} +
  • No results found
  • + {% endfor %} +
+ {% 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 %} +
+

Editors picks

+ +
+ {% 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 + + + +Now add a simple interface for the search with a ```` element to gather search terms and a ``
`` to display the results: + +.. code-block:: html + +
+

Search

+ +
+
+ +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 += '

' + element.title + '

'; + }); + // 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 ``
``. 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 \ No newline at end of file +.. _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``. diff --git a/requirements-dev.txt b/requirements-dev.txt index 201f86ed8..008d24d2e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -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 diff --git a/runtests.py b/runtests.py index 027d61158..6fd37ebeb 100755 --- a/runtests.py +++ b/runtests.py @@ -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 ), diff --git a/scripts/install/debian.sh b/scripts/install/debian.sh index 93f513d16..ee08191ac 100644 --- a/scripts/install/debian.sh +++ b/scripts/install/debian.sh @@ -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 diff --git a/scripts/install/ubuntu.sh b/scripts/install/ubuntu.sh index 9f60e2e8e..299e67a8d 100644 --- a/scripts/install/ubuntu.sh +++ b/scripts/install/ubuntu.sh @@ -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 diff --git a/setup.py b/setup.py index eee178ed2..4f5b87c12 100644 --- a/setup.py +++ b/setup.py @@ -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 ], diff --git a/tox.ini b/tox.ini new file mode 100644 index 000000000..11b8bb3a2 --- /dev/null +++ b/tox.ini @@ -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 diff --git a/wagtail/vendor/django-treebeard/treebeard/tests/__init__.py b/wagtail/contrib/__init__.py similarity index 100% rename from wagtail/vendor/django-treebeard/treebeard/tests/__init__.py rename to wagtail/contrib/__init__.py diff --git a/wagtail/vendor/django-treebeard/NOTICE b/wagtail/contrib/wagtailmedusa/__init__.py similarity index 100% rename from wagtail/vendor/django-treebeard/NOTICE rename to wagtail/contrib/wagtailmedusa/__init__.py diff --git a/wagtail/contrib/wagtailmedusa/models.py b/wagtail/contrib/wagtailmedusa/models.py new file mode 100644 index 000000000..e69de29bb diff --git a/wagtail/contrib/wagtailmedusa/renderers.py b/wagtail/contrib/wagtailmedusa/renderers.py new file mode 100644 index 000000000..950e13a14 --- /dev/null +++ b/wagtail/contrib/wagtailmedusa/renderers.py @@ -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] diff --git a/wagtail/tests/fixtures/test.json b/wagtail/tests/fixtures/test.json index 571a9df3f..625c5618b 100644 --- a/wagtail/tests/fixtures/test.json +++ b/wagtail/tests/fixtures/test.json @@ -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": "

We are really good.

" + } +}, + +{ + "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" + } } ] diff --git a/wagtail/tests/models.py b/wagtail/tests/models.py index 9124cb253..844fb836f 100644 --- a/wagtail/tests/models.py +++ b/wagtail/tests/models.py @@ -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") +] diff --git a/wagtail/tests/templates/tests/form_page.html b/wagtail/tests/templates/tests/form_page.html new file mode 100644 index 000000000..5fbd2ae9f --- /dev/null +++ b/wagtail/tests/templates/tests/form_page.html @@ -0,0 +1,15 @@ +{% load pageurl %} + + + + {{ self.title }} + + +

{{ self.title }}

+
+ {% csrf_token %} + {{ form.as_p }} + +
+ + diff --git a/wagtail/tests/templates/tests/form_page_landing.html b/wagtail/tests/templates/tests/form_page_landing.html new file mode 100644 index 000000000..e29a6a942 --- /dev/null +++ b/wagtail/tests/templates/tests/form_page_landing.html @@ -0,0 +1,11 @@ +{% load pageurl %} + + + + {{ self.title }} + + +

{{ self.title }}

+

Thank you for your feedback.

+ + diff --git a/wagtail/tests/urls.py b/wagtail/tests/urls.py index d133a4bf8..d04c871a3 100644 --- a/wagtail/tests/urls.py +++ b/wagtail/tests/urls.py @@ -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 diff --git a/wagtail/tests/utils.py b/wagtail/tests/utils.py index 11cdb369d..7b6557a78 100644 --- a/wagtail/tests/utils.py +++ b/wagtail/tests/utils.py @@ -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') \ No newline at end of file + client.login(username='test', password='password') diff --git a/wagtail/vendor/django-treebeard/.coveragerc b/wagtail/vendor/django-treebeard/.coveragerc deleted file mode 100644 index 6f2f2bdf9..000000000 --- a/wagtail/vendor/django-treebeard/.coveragerc +++ /dev/null @@ -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 diff --git a/wagtail/vendor/django-treebeard/.hgignore b/wagtail/vendor/django-treebeard/.hgignore deleted file mode 100644 index cda07805f..000000000 --- a/wagtail/vendor/django-treebeard/.hgignore +++ /dev/null @@ -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 diff --git a/wagtail/vendor/django-treebeard/.hgtags b/wagtail/vendor/django-treebeard/.hgtags deleted file mode 100644 index fa292fb36..000000000 --- a/wagtail/vendor/django-treebeard/.hgtags +++ /dev/null @@ -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 diff --git a/wagtail/vendor/django-treebeard/AUTHORS b/wagtail/vendor/django-treebeard/AUTHORS deleted file mode 100644 index 680d9e5e9..000000000 --- a/wagtail/vendor/django-treebeard/AUTHORS +++ /dev/null @@ -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) diff --git a/wagtail/vendor/django-treebeard/CHANGES b/wagtail/vendor/django-treebeard/CHANGES deleted file mode 100644 index 9c0beebbe..000000000 --- a/wagtail/vendor/django-treebeard/CHANGES +++ /dev/null @@ -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 diff --git a/wagtail/vendor/django-treebeard/LICENSE b/wagtail/vendor/django-treebeard/LICENSE deleted file mode 100644 index 6b0b1270f..000000000 --- a/wagtail/vendor/django-treebeard/LICENSE +++ /dev/null @@ -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. - diff --git a/wagtail/vendor/django-treebeard/MANIFEST.in b/wagtail/vendor/django-treebeard/MANIFEST.in deleted file mode 100644 index fd87d7ac1..000000000 --- a/wagtail/vendor/django-treebeard/MANIFEST.in +++ /dev/null @@ -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 diff --git a/wagtail/vendor/django-treebeard/README.rst b/wagtail/vendor/django-treebeard/README.rst deleted file mode 100644 index 2c641703a..000000000 --- a/wagtail/vendor/django-treebeard/README.rst +++ /dev/null @@ -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/ diff --git a/wagtail/vendor/django-treebeard/UPDATING b/wagtail/vendor/django-treebeard/UPDATING deleted file mode 100644 index d8628bc45..000000000 --- a/wagtail/vendor/django-treebeard/UPDATING +++ /dev/null @@ -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. - diff --git a/wagtail/vendor/django-treebeard/docs/Makefile b/wagtail/vendor/django-treebeard/docs/Makefile deleted file mode 100644 index f325fb3f4..000000000 --- a/wagtail/vendor/django-treebeard/docs/Makefile +++ /dev/null @@ -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 ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - @echo " coverage Coverage" - -clean: - -rm -rf $(BUILDDIR)/* - -html: - mkdir -p _static - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-treebeard.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-treebeard.qhc" - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ - "run these through (pdf)latex." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -coverage: - $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage - @echo "Coverage, " \ - "results in $(BUILDDIR)/coverage" diff --git a/wagtail/vendor/django-treebeard/docs/README b/wagtail/vendor/django-treebeard/docs/README deleted file mode 100644 index 5f9004cde..000000000 --- a/wagtail/vendor/django-treebeard/docs/README +++ /dev/null @@ -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. diff --git a/wagtail/vendor/django-treebeard/docs/_ext/djangodocs.py b/wagtail/vendor/django-treebeard/docs/_ext/djangodocs.py deleted file mode 100644 index 3b59f3c4f..000000000 --- a/wagtail/vendor/django-treebeard/docs/_ext/djangodocs.py +++ /dev/null @@ -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", - ) diff --git a/wagtail/vendor/django-treebeard/docs/_static/treebeard-admin-advanced.png b/wagtail/vendor/django-treebeard/docs/_static/treebeard-admin-advanced.png deleted file mode 100644 index 28e95397f..000000000 Binary files a/wagtail/vendor/django-treebeard/docs/_static/treebeard-admin-advanced.png and /dev/null differ diff --git a/wagtail/vendor/django-treebeard/docs/_static/treebeard-admin-basic.png b/wagtail/vendor/django-treebeard/docs/_static/treebeard-admin-basic.png deleted file mode 100644 index b0d010fd8..000000000 Binary files a/wagtail/vendor/django-treebeard/docs/_static/treebeard-admin-basic.png and /dev/null differ diff --git a/wagtail/vendor/django-treebeard/docs/admin.rst b/wagtail/vendor/django-treebeard/docs/admin.rst deleted file mode 100644 index 739831048..000000000 --- a/wagtail/vendor/django-treebeard/docs/admin.rst +++ /dev/null @@ -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 ` and :doc:`Nested Sets ` 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 ` trees have a basic admin interface. - -.. image:: _static/treebeard-admin-basic.png - - -.. _FeinCMS: http://www.feincms.org diff --git a/wagtail/vendor/django-treebeard/docs/al_tree.rst b/wagtail/vendor/django-treebeard/docs/al_tree.rst deleted file mode 100644 index 054aa86c5..000000000 --- a/wagtail/vendor/django-treebeard/docs/al_tree.rst +++ /dev/null @@ -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: diff --git a/wagtail/vendor/django-treebeard/docs/api.rst b/wagtail/vendor/django-treebeard/docs/api.rst deleted file mode 100644 index 173ba37d3..000000000 --- a/wagtail/vendor/django-treebeard/docs/api.rst +++ /dev/null @@ -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 %} -
  • - {% else %} -
  • - {% endif %} - - {{ item }} - - {% for close in info.close %} -
- {% endfor %} - {% endfor %} - - .. note:: - - This method was contributed originally by - `Alexey Kinyov `_, using an idea borrowed from - `django-mptt`_. - - .. versionadded:: 1.55 - - - .. automethod:: get_database_vendor - - Example: - - .. code-block:: python - - MyNodeModel.get_database_vendor("write") - - - .. versionadded:: 1.61 - - -.. _django-mptt: https://github.com/django-mptt/django-mptt/ \ No newline at end of file diff --git a/wagtail/vendor/django-treebeard/docs/caveats.rst b/wagtail/vendor/django-treebeard/docs/caveats.rst deleted file mode 100644 index 73914c94b..000000000 --- a/wagtail/vendor/django-treebeard/docs/caveats.rst +++ /dev/null @@ -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. diff --git a/wagtail/vendor/django-treebeard/docs/changes.rst b/wagtail/vendor/django-treebeard/docs/changes.rst deleted file mode 100644 index 3b6a0a9cc..000000000 --- a/wagtail/vendor/django-treebeard/docs/changes.rst +++ /dev/null @@ -1,4 +0,0 @@ -Changelog -========= - -.. include:: ../CHANGES diff --git a/wagtail/vendor/django-treebeard/docs/conf.py b/wagtail/vendor/django-treebeard/docs/conf.py deleted file mode 100644 index 08584ae6b..000000000 --- a/wagtail/vendor/django-treebeard/docs/conf.py +++ /dev/null @@ -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/' - ), -} diff --git a/wagtail/vendor/django-treebeard/docs/exceptions.rst b/wagtail/vendor/django-treebeard/docs/exceptions.rst deleted file mode 100644 index ba5de3797..000000000 --- a/wagtail/vendor/django-treebeard/docs/exceptions.rst +++ /dev/null @@ -1,12 +0,0 @@ -Exceptions -========== - -.. module:: treebeard.exceptions - -.. autoexception:: InvalidPosition - -.. autoexception:: InvalidMoveToDescendant - -.. autoexception:: PathOverflow - -.. autoexception:: MissingNodeOrderBy diff --git a/wagtail/vendor/django-treebeard/docs/forms.rst b/wagtail/vendor/django-treebeard/docs/forms.rst deleted file mode 100644 index 0c6d9b3b9..000000000 --- a/wagtail/vendor/django-treebeard/docs/forms.rst +++ /dev/null @@ -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') - diff --git a/wagtail/vendor/django-treebeard/docs/index.rst b/wagtail/vendor/django-treebeard/docs/index.rst deleted file mode 100644 index 042c1b41d..000000000 --- a/wagtail/vendor/django-treebeard/docs/index.rst +++ /dev/null @@ -1,80 +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. :doc:`Adjacency List ` - 2. :doc:`Materialized Path ` - 3. :doc:`Nested Sets ` - -- **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` diff --git a/wagtail/vendor/django-treebeard/docs/install.rst b/wagtail/vendor/django-treebeard/docs/install.rst deleted file mode 100644 index 5025964ae..000000000 --- a/wagtail/vendor/django-treebeard/docs/install.rst +++ /dev/null @@ -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 `_ - -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/ - diff --git a/wagtail/vendor/django-treebeard/docs/mp_tree.rst b/wagtail/vendor/django-treebeard/docs/mp_tree.rst deleted file mode 100644 index e3a121c38..000000000 --- a/wagtail/vendor/django-treebeard/docs/mp_tree.rst +++ /dev/null @@ -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/ diff --git a/wagtail/vendor/django-treebeard/docs/ns_tree.rst b/wagtail/vendor/django-treebeard/docs/ns_tree.rst deleted file mode 100644 index dcbd88828..000000000 --- a/wagtail/vendor/django-treebeard/docs/ns_tree.rst +++ /dev/null @@ -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 diff --git a/wagtail/vendor/django-treebeard/docs/tests.rst b/wagtail/vendor/django-treebeard/docs/tests.rst deleted file mode 100644 index b34a84dd6..000000000 --- a/wagtail/vendor/django-treebeard/docs/tests.rst +++ /dev/null @@ -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/ diff --git a/wagtail/vendor/django-treebeard/docs/tutorial.rst b/wagtail/vendor/django-treebeard/docs/tutorial.rst deleted file mode 100644 index 86d5756ed..000000000 --- a/wagtail/vendor/django-treebeard/docs/tutorial.rst +++ /dev/null @@ -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') - - >>> get(node.pk).add_sibling(name='SSD') - - >>> get(node.pk).add_child(name='Desktop Memory') - - >>> get(node.pk).add_child(name='Laptop Memory') - - >>> get(node.pk).add_child(name='Server Memory') - - -.. note:: - - Why retrieving every node again after the first operation? Because - ``django-treebeard`` uses raw queries for most write operations, - and raw queries don't update the django objects of the db entries they - modify. 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() - [(, - {'close': [], 'level': 0, 'open': True}), - (, - {'close': [], 'level': 1, 'open': True}), - (, - {'close': [], 'level': 1, 'open': False}), - (, - {'close': [], 'level': 2, 'open': True}), - (, - {'close': [], 'level': 2, 'open': False}), - (, - {'close': [0], 'level': 2, 'open': False}), - (, - {'close': [0, 1], 'level': 1, 'open': False})] - - - -Read the :class:`treebeard.models.Node` API reference for detailed info. - -.. _`treebeard mercurial repository`: - http://code.tabo.pe/django-treebeard -.. _`latest treebeard version from PyPi`: - http://pypi.python.org/pypi/django-treebeard/ diff --git a/wagtail/vendor/django-treebeard/setup.py b/wagtail/vendor/django-treebeard/setup.py deleted file mode 100644 index 379f368cc..000000000 --- a/wagtail/vendor/django-treebeard/setup.py +++ /dev/null @@ -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) diff --git a/wagtail/vendor/django-treebeard/tox.ini b/wagtail/vendor/django-treebeard/tox.ini deleted file mode 100644 index dfec018a8..000000000 --- a/wagtail/vendor/django-treebeard/tox.ini +++ /dev/null @@ -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 - - diff --git a/wagtail/vendor/django-treebeard/treebeard/__init__.py b/wagtail/vendor/django-treebeard/treebeard/__init__.py deleted file mode 100644 index c35bfc233..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = '2.0b2' diff --git a/wagtail/vendor/django-treebeard/treebeard/admin.py b/wagtail/vendor/django-treebeard/treebeard/admin.py deleted file mode 100644 index a461d3754..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/admin.py +++ /dev/null @@ -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)) diff --git a/wagtail/vendor/django-treebeard/treebeard/al_tree.py b/wagtail/vendor/django-treebeard/treebeard/al_tree.py deleted file mode 100644 index 57a23200d..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/al_tree.py +++ /dev/null @@ -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 diff --git a/wagtail/vendor/django-treebeard/treebeard/exceptions.py b/wagtail/vendor/django-treebeard/treebeard/exceptions.py deleted file mode 100644 index 99bdcee1b..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/exceptions.py +++ /dev/null @@ -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) - """ diff --git a/wagtail/vendor/django-treebeard/treebeard/forms.py b/wagtail/vendor/django-treebeard/treebeard/forms.py deleted file mode 100644 index e5f7ebbcf..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/forms.py +++ /dev/null @@ -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 '    ' * (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 diff --git a/wagtail/vendor/django-treebeard/treebeard/locale/es/LC_MESSAGES/django.mo b/wagtail/vendor/django-treebeard/treebeard/locale/es/LC_MESSAGES/django.mo deleted file mode 100644 index 9c35c4b14..000000000 Binary files a/wagtail/vendor/django-treebeard/treebeard/locale/es/LC_MESSAGES/django.mo and /dev/null differ diff --git a/wagtail/vendor/django-treebeard/treebeard/locale/es/LC_MESSAGES/django.po b/wagtail/vendor/django-treebeard/treebeard/locale/es/LC_MESSAGES/django.po deleted file mode 100644 index c950ba585..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/locale/es/LC_MESSAGES/django.po +++ /dev/null @@ -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 \n" -"Language-Team: Spanish\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: admin.py:113 -#, python-format -msgid "Moved node \"%(node)s\" as child of \"%(other)s\"" -msgstr "" - -#: admin.py:119 -#, python-format -msgid "Moved node \"%(node)s\" as sibling of \"%(other)s\"" -msgstr "" - -#: admin.py:129 -#, python-format -msgid "Exception raised while moving node: %s" -msgstr "" - -#: al_tree.py:319 mp_tree.py:641 ns_tree.py:308 -msgid "Can't move node to a descendant." -msgstr "" - -#: forms.py:17 -msgid "Child of" -msgstr "Hijo de" - -#: forms.py:18 -msgid "Sibling of" -msgstr "Hermano de" - -#: forms.py:22 -msgid "First child of" -msgstr "Primer hijo de" - -#: forms.py:23 -msgid "Before" -msgstr "Antes" - -#: forms.py:24 -msgid "After" -msgstr "Después" - -#: forms.py:27 -msgid "Position" -msgstr "Posición" - -#: forms.py:31 -msgid "Relative to" -msgstr "Relativo a" - -#: forms.py:81 -msgid "-- root --" -msgstr "-- raíz --" - -#: mp_tree.py:521 -msgid "" -"The new node is too deep in the tree, try increasing the path.max_length " -"property and UPDATE your database" -msgstr "" - -#: mp_tree.py:702 -#, python-format -msgid "Path Overflow from: '%s'" -msgstr "" - -#: templatetags/admin_tree.py:148 -msgid "Return to ordered tree" -msgstr "" diff --git a/wagtail/vendor/django-treebeard/treebeard/locale/es/LC_MESSAGES/djangojs.mo b/wagtail/vendor/django-treebeard/treebeard/locale/es/LC_MESSAGES/djangojs.mo deleted file mode 100644 index 81f016d09..000000000 Binary files a/wagtail/vendor/django-treebeard/treebeard/locale/es/LC_MESSAGES/djangojs.mo and /dev/null differ diff --git a/wagtail/vendor/django-treebeard/treebeard/locale/es/LC_MESSAGES/djangojs.po b/wagtail/vendor/django-treebeard/treebeard/locale/es/LC_MESSAGES/djangojs.po deleted file mode 100644 index bf30b8658..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/locale/es/LC_MESSAGES/djangojs.po +++ /dev/null @@ -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 "" diff --git a/wagtail/vendor/django-treebeard/treebeard/locale/nl/LC_MESSAGES/django.mo b/wagtail/vendor/django-treebeard/treebeard/locale/nl/LC_MESSAGES/django.mo deleted file mode 100644 index c5aad5c26..000000000 Binary files a/wagtail/vendor/django-treebeard/treebeard/locale/nl/LC_MESSAGES/django.mo and /dev/null differ diff --git a/wagtail/vendor/django-treebeard/treebeard/locale/nl/LC_MESSAGES/django.po b/wagtail/vendor/django-treebeard/treebeard/locale/nl/LC_MESSAGES/django.po deleted file mode 100644 index 6cea35f1e..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/locale/nl/LC_MESSAGES/django.po +++ /dev/null @@ -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 \n" -"Language-Team: Dutch\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: admin.py:113 -#, python-format -msgid "Moved node \"%(node)s\" as child of \"%(other)s\"" -msgstr "\"%(node)s\" is nu onderdeel van \"%(other)s\"" - -#: admin.py:119 -#, python-format -msgid "Moved node \"%(node)s\" as sibling of \"%(other)s\"" -msgstr "\"%(node)s\" staat nu voor \"%(other)s\"" - -#: admin.py:129 -#, python-format -msgid "Exception raised while moving node: %s" -msgstr "Fatale fout tijdens het verplaatsen: %s" - -#: al_tree.py:319 mp_tree.py:641 ns_tree.py:308 -msgid "Can't move node to a descendant." -msgstr "Kan node niet naar eigen subnode verplaatsen" - -#: forms.py:17 -msgid "Child of" -msgstr "Onderdeel" - -#: forms.py:18 -msgid "Sibling of" -msgstr "Naast" - -#: forms.py:22 -msgid "First child of" -msgstr "1e onderdeel" - -#: forms.py:23 -msgid "Before" -msgstr "Voor" - -#: forms.py:24 -msgid "After" -msgstr "Na" - -#: forms.py:27 -msgid "Position" -msgstr "Positie" - -#: forms.py:31 -msgid "Relative to" -msgstr "Ten opzichte van" - -#: forms.py:81 -msgid "-- root --" -msgstr "-- hoofdniveau --" - -#: mp_tree.py:521 -msgid "" -"The new node is too deep in the tree, try increasing the path.max_length " -"property and UPDATE your database" -msgstr "" -"De nieuwe node bevindt zich te diep in de boom. Verhoog de path.max_lenght " -"waarde en UPDATE de database." - -#: mp_tree.py:702 -#, python-format -msgid "Path Overflow from: '%s'" -msgstr "Path overflow van: '%s'" - -#: templatetags/admin_tree.py:148 -msgid "Return to ordered tree" -msgstr "Als gesorteerde boom" diff --git a/wagtail/vendor/django-treebeard/treebeard/locale/nl/LC_MESSAGES/djangojs.mo b/wagtail/vendor/django-treebeard/treebeard/locale/nl/LC_MESSAGES/djangojs.mo deleted file mode 100644 index 7ef0783ed..000000000 Binary files a/wagtail/vendor/django-treebeard/treebeard/locale/nl/LC_MESSAGES/djangojs.mo and /dev/null differ diff --git a/wagtail/vendor/django-treebeard/treebeard/locale/nl/LC_MESSAGES/djangojs.po b/wagtail/vendor/django-treebeard/treebeard/locale/nl/LC_MESSAGES/djangojs.po deleted file mode 100644 index 773936342..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/locale/nl/LC_MESSAGES/djangojs.po +++ /dev/null @@ -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 \n" -"Language-Team: Dutch\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: static/treebeard/treebeard-admin.js:157 -msgid "Abort" -msgstr "Annuleren" - -#: static/treebeard/treebeard-admin.js:172 -msgid "As Sibling" -msgstr "Als naastliggend onderdeel" - -#: static/treebeard/treebeard-admin.js:190 -msgid "As child" -msgstr "Als subonderdeel" diff --git a/wagtail/vendor/django-treebeard/treebeard/locale/pl/LC_MESSAGES/django.mo b/wagtail/vendor/django-treebeard/treebeard/locale/pl/LC_MESSAGES/django.mo deleted file mode 100644 index 160f119d1..000000000 Binary files a/wagtail/vendor/django-treebeard/treebeard/locale/pl/LC_MESSAGES/django.mo and /dev/null differ diff --git a/wagtail/vendor/django-treebeard/treebeard/locale/pl/LC_MESSAGES/django.po b/wagtail/vendor/django-treebeard/treebeard/locale/pl/LC_MESSAGES/django.po deleted file mode 100644 index be741ab79..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/locale/pl/LC_MESSAGES/django.po +++ /dev/null @@ -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 \n" -"Language-Team: Polish\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: forms.py:16 -msgid "Child of" -msgstr "Dziecko kategorii" - -#: forms.py:17 -msgid "Sibling of" -msgstr "Sąsiad kategorii" - -#: forms.py:21 -msgid "First child of" -msgstr "Pierwsze dziecko kategorii" - -#: forms.py:22 -msgid "Before" -msgstr "Przed" - -#: forms.py:23 -msgid "After" -msgstr "Za" - -#: forms.py:26 -msgid "Position" -msgstr "Pozycja" - -#: forms.py:30 -msgid "Relative to" -msgstr "Względem" - -#: forms.py:80 -msgid "-- root --" -msgstr "-- kategoria główna --" diff --git a/wagtail/vendor/django-treebeard/treebeard/locale/ru/LC_MESSAGES/django.mo b/wagtail/vendor/django-treebeard/treebeard/locale/ru/LC_MESSAGES/django.mo deleted file mode 100644 index 5feefc1f2..000000000 Binary files a/wagtail/vendor/django-treebeard/treebeard/locale/ru/LC_MESSAGES/django.mo and /dev/null differ diff --git a/wagtail/vendor/django-treebeard/treebeard/locale/ru/LC_MESSAGES/django.po b/wagtail/vendor/django-treebeard/treebeard/locale/ru/LC_MESSAGES/django.po deleted file mode 100644 index f335c16e8..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/locale/ru/LC_MESSAGES/django.po +++ /dev/null @@ -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 \n" -"Language-Team: Russian\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%" -"10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" - -#: admin.py:113 -#, python-format -msgid "Moved node \"%(node)s\" as child of \"%(other)s\"" -msgstr "" - -#: admin.py:119 -#, python-format -msgid "Moved node \"%(node)s\" as sibling of \"%(other)s\"" -msgstr "" - -#: admin.py:129 -#, python-format -msgid "Exception raised while moving node: %s" -msgstr "" - -#: al_tree.py:319 mp_tree.py:641 ns_tree.py:308 -msgid "Can't move node to a descendant." -msgstr "" - -#: forms.py:17 -msgid "Child of" -msgstr "Вложенный" - -#: forms.py:18 -msgid "Sibling of" -msgstr "Соседний к" - -#: forms.py:22 -msgid "First child of" -msgstr "Первый вложенный" - -#: forms.py:23 -msgid "Before" -msgstr "До" - -#: forms.py:24 -msgid "After" -msgstr "После" - -#: forms.py:27 -msgid "Position" -msgstr "Позиция" - -#: forms.py:31 -msgid "Relative to" -msgstr "Относительно" - -#: forms.py:81 -msgid "-- root --" -msgstr "-- корень --" - -#: mp_tree.py:521 -msgid "" -"The new node is too deep in the tree, try increasing the path.max_length " -"property and UPDATE your database" -msgstr "" - -#: mp_tree.py:702 -#, python-format -msgid "Path Overflow from: '%s'" -msgstr "" - -#: templatetags/admin_tree.py:148 -msgid "Return to ordered tree" -msgstr "" diff --git a/wagtail/vendor/django-treebeard/treebeard/locale/ru/LC_MESSAGES/djangojs.mo b/wagtail/vendor/django-treebeard/treebeard/locale/ru/LC_MESSAGES/djangojs.mo deleted file mode 100644 index 4a2f52acc..000000000 Binary files a/wagtail/vendor/django-treebeard/treebeard/locale/ru/LC_MESSAGES/djangojs.mo and /dev/null differ diff --git a/wagtail/vendor/django-treebeard/treebeard/locale/ru/LC_MESSAGES/djangojs.po b/wagtail/vendor/django-treebeard/treebeard/locale/ru/LC_MESSAGES/djangojs.po deleted file mode 100644 index b02a2a141..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/locale/ru/LC_MESSAGES/djangojs.po +++ /dev/null @@ -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 "" diff --git a/wagtail/vendor/django-treebeard/treebeard/models.py b/wagtail/vendor/django-treebeard/treebeard/models.py deleted file mode 100644 index e84850287..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/models.py +++ /dev/null @@ -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 diff --git a/wagtail/vendor/django-treebeard/treebeard/mp_tree.py b/wagtail/vendor/django-treebeard/treebeard/mp_tree.py deleted file mode 100644 index 03e3b3f21..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/mp_tree.py +++ /dev/null @@ -1,1060 +0,0 @@ -"""Materialized Path Trees""" - -import sys -import operator - -if sys.version_info >= (3, 0): - from functools import reduce - -from django.core import serializers -from django.db import models, transaction, connection -from django.db.models import F, Q -from django.utils.translation import ugettext_noop as _ - -from treebeard.numconv import NumConv -from treebeard.models import Node -from treebeard.exceptions import InvalidMoveToDescendant, PathOverflow - - -def get_base_model_class(cls): - """ - Return the class in this model's inheritance chain which implements tree behaviour - (i.e. the one which defines the 'path' field). Necessary because a method like - get_children invoked on a multiple-table-inheritance subclass needs to perform - the query on the base class, in order to return child nodes that are not - the same subclass as that parent. - """ - return cls._meta.get_field('path').model - - -class MP_NodeQuerySet(models.query.QuerySet): - """ - Custom queryset for the tree node manager. - - Needed only for the custom delete method. - """ - - def delete(self): - """ - Custom delete method, will remove all descendant nodes to ensure a - consistent tree (no orphans) - - :returns: ``None`` - """ - # we'll have to manually run through all the nodes that are going - # to be deleted and remove nodes from the list if an ancestor is - # already getting removed, since that would be redundant - removed = {} - for node in self.order_by('depth', 'path'): - found = False - for depth in range(1, int(len(node.path) / node.steplen)): - path = node._get_basepath(node.path, depth) - if path in removed: - # we are already removing a parent of this node - # skip - found = True - break - if not found: - removed[node.path] = node - - # ok, got the minimal list of nodes to remove... - # we must also remove their children - # and update every parent node's numchild attribute - # LOTS OF FUN HERE! - parents = {} - toremove = [] - for path, node in removed.items(): - parentpath = node._get_basepath(node.path, node.depth - 1) - if parentpath: - if parentpath not in parents: - parents[parentpath] = node.get_parent(True) - parent = parents[parentpath] - if parent and parent.numchild > 0: - parent.numchild -= 1 - parent.save() - if node.is_leaf(): - toremove.append(Q(path=node.path)) - else: - toremove.append(Q(path__startswith=node.path)) - - # Django will handle this as a SELECT and then a DELETE of - # ids, and will deal with removing related objects - if toremove: - qset = self.model.objects.filter(reduce(operator.or_, toremove)) - super(MP_NodeQuerySet, qset).delete() - transaction.commit_unless_managed() - - -class MP_NodeManager(models.Manager): - """Custom manager for nodes in a Materialized Path tree.""" - - def get_query_set(self): - """Sets the custom queryset as the default.""" - return MP_NodeQuerySet(self.model).order_by('path') - - -class MP_AddHandler(object): - def __init__(self): - self.stmts = [] - - -class MP_ComplexAddMoveHandler(MP_AddHandler): - - def run_sql_stmts(self): - cursor = self.node_cls._get_database_cursor('write') - for sql, vals in self.stmts: - cursor.execute(sql, vals) - - def get_sql_update_numchild(self, path, incdec='inc'): - """:returns: The sql needed the numchild value of a node""" - sql = "UPDATE %s SET numchild=numchild%s1"\ - " WHERE path=%%s" % ( - connection.ops.quote_name(get_base_model_class(self.node_cls)._meta.db_table), - {'inc': '+', 'dec': '-'}[incdec]) - vals = [path] - return sql, vals - - def reorder_nodes_before_add_or_move(self, pos, newpos, newdepth, target, - siblings, oldpath=None, - movebranch=False): - """ - Handles the reordering of nodes and branches when adding/moving - nodes. - - :returns: A tuple containing the old path and the new path. - """ - if ( - (pos == 'last-sibling') or - (pos == 'right' and target == target.get_last_sibling()) - ): - # easy, the last node - last = target.get_last_sibling() - newpath = last._inc_path() - if movebranch: - self.stmts.append( - self.get_sql_newpath_in_branches(oldpath, newpath)) - else: - # do the UPDATE dance - - if newpos is None: - siblings = target.get_siblings() - siblings = {'left': siblings.filter(path__gte=target.path), - 'right': siblings.filter(path__gt=target.path), - 'first-sibling': siblings}[pos] - basenum = target._get_lastpos_in_path() - newpos = {'first-sibling': 1, - 'left': basenum, - 'right': basenum + 1}[pos] - - newpath = self.node_cls._get_path(target.path, newdepth, newpos) - - # If the move is amongst siblings and is to the left and there - # are siblings to the right of its new position then to be on - # the safe side we temporarily dump it on the end of the list - tempnewpath = None - if movebranch and len(oldpath) == len(newpath): - parentoldpath = self.node_cls._get_basepath( - oldpath, - int(len(oldpath) / self.node_cls.steplen) - 1 - ) - parentnewpath = self.node_cls._get_basepath( - newpath, newdepth - 1) - if ( - parentoldpath == parentnewpath and - siblings and - newpath < oldpath - ): - last = target.get_last_sibling() - basenum = last._get_lastpos_in_path() - tempnewpath = self.node_cls._get_path( - newpath, newdepth, basenum + 2) - self.stmts.append( - self.get_sql_newpath_in_branches( - oldpath, tempnewpath)) - - # Optimisation to only move siblings which need moving - # (i.e. if we've got holes, allow them to compress) - movesiblings = [] - priorpath = newpath - for node in siblings: - # If the path of the node is already greater than the path - # of the previous node it doesn't need shifting - if node.path > priorpath: - break - # It does need shifting, so add to the list - movesiblings.append(node) - # Calculate the path that it would be moved to, as that's - # the next "priorpath" - priorpath = node._inc_path() - movesiblings.reverse() - - for node in movesiblings: - # moving the siblings (and their branches) at the right of the - # related position one step to the right - sql, vals = self.get_sql_newpath_in_branches( - node.path, node._inc_path()) - self.stmts.append((sql, vals)) - - if movebranch: - if oldpath.startswith(node.path): - # if moving to a parent, update oldpath since we just - # increased the path of the entire branch - oldpath = vals[0] + oldpath[len(vals[0]):] - if target.path.startswith(node.path): - # and if we moved the target, update the object - # django made for us, since the update won't do it - # maybe useful in loops - target.path = vals[0] + target.path[len(vals[0]):] - if movebranch: - # node to move - if tempnewpath: - self.stmts.append( - self.get_sql_newpath_in_branches( - tempnewpath, newpath)) - else: - self.stmts.append( - self.get_sql_newpath_in_branches( - oldpath, newpath)) - return oldpath, newpath - - def get_sql_newpath_in_branches(self, oldpath, newpath): - """ - :returns: The sql needed to move a branch to another position. - - .. note:: - - The generated sql will only update the depth values if needed. - - """ - - vendor = self.node_cls.get_database_vendor('write') - sql1 = "UPDATE %s SET" % ( - connection.ops.quote_name(get_base_model_class(self.node_cls)._meta.db_table), ) - - # <3 "standard" sql - if vendor == 'sqlite': - # I know that the third argument in SUBSTR (LENGTH(path)) is - # awful, but sqlite fails without it: - # OperationalError: wrong number of arguments to function substr() - # even when the documentation says that 2 arguments are valid: - # http://www.sqlite.org/lang_corefunc.html - sqlpath = "%s||SUBSTR(path, %s, LENGTH(path))" - elif vendor == 'mysql': - # hooray for mysql ignoring standards in their default - # configuration! - # to make || work as it should, enable ansi mode - # http://dev.mysql.com/doc/refman/5.0/en/ansi-mode.html - sqlpath = "CONCAT(%s, SUBSTR(path, %s))" - else: - sqlpath = "%s||SUBSTR(path, %s)" - - sql2 = ["path=%s" % (sqlpath, )] - vals = [newpath, len(oldpath) + 1] - if len(oldpath) != len(newpath) and vendor != 'mysql': - # when using mysql, this won't update the depth and it has to be - # done in another query - # doesn't even work with sql_mode='ANSI,TRADITIONAL' - # TODO: FIND OUT WHY?!?? right now I'm just blaming mysql - sql2.append("depth=LENGTH(%s)/%%s" % (sqlpath, )) - vals.extend([newpath, len(oldpath) + 1, self.node_cls.steplen]) - sql3 = "WHERE path LIKE %s" - vals.extend([oldpath + '%']) - sql = '%s %s %s' % (sql1, ', '.join(sql2), sql3) - return sql, vals - - -class MP_AddRootHandler(MP_AddHandler): - def __init__(self, cls, instance, **kwargs): - super(MP_AddRootHandler, self).__init__() - self.cls = cls - self.instance = instance - self.kwargs = kwargs - - def process(self): - - # do we have a root node already? - last_root = self.cls.get_last_root_node() - - if last_root and last_root.node_order_by: - # there are root nodes and node_order_by has been set - # delegate sorted insertion to add_sibling - return last_root.add_sibling('sorted-sibling', self.instance, **self.kwargs) - - if last_root: - # adding the new root node as the last one - newpath = last_root._inc_path() - else: - # adding the first root node - newpath = self.cls._get_path(None, 1, 1) - - if self.instance: - if self.instance.pk: - raise ValueError("Attempted to add a tree node that is already in the database") - newobj = self.instance - else: - # creating the new object - newobj = self.cls(**self.kwargs) - - newobj.depth = 1 - newobj.path = newpath - # saving the instance before returning it - newobj.save() - transaction.commit_unless_managed() - return newobj - - -class MP_AddChildHandler(MP_AddHandler): - def __init__(self, node, instance, **kwargs): - super(MP_AddChildHandler, self).__init__() - self.node = node - self.node_cls = node.__class__ - self.instance = instance - self.kwargs = kwargs - - def process(self): - if self.node_cls.node_order_by and not self.node.is_leaf(): - # there are child nodes and node_order_by has been set - # delegate sorted insertion to add_sibling - self.node.numchild += 1 - return self.node.get_last_child().add_sibling( - 'sorted-sibling', self.instance, **self.kwargs) - - if self.instance: - if self.instance.pk: - raise ValueError("Attempted to add a tree node that is already in the database") - newobj = self.instance - else: - # creating a new object - newobj = self.node_cls(**self.kwargs) - - newobj.depth = self.node.depth + 1 - if self.node.is_leaf(): - # the node had no children, adding the first child - newobj.path = self.node_cls._get_path( - self.node.path, newobj.depth, 1) - max_length = self.node_cls._meta.get_field('path').max_length - if len(newobj.path) > max_length: - raise PathOverflow( - _('The new node is too deep in the tree, try' - ' increasing the path.max_length property' - ' and UPDATE your database')) - else: - # adding the new child as the last one - newobj.path = self.node.get_last_child()._inc_path() - # saving the instance before returning it - newobj.save() - newobj._cached_parent_obj = self.node - - get_base_model_class(self.node_cls).objects.filter( - path=self.node.path).update(numchild=F('numchild')+1) - - # we increase the numchild value of the object in memory - self.node.numchild += 1 - transaction.commit_unless_managed() - return newobj - - -class MP_AddSiblingHandler(MP_ComplexAddMoveHandler): - def __init__(self, node, pos, instance, **kwargs): - super(MP_AddSiblingHandler, self).__init__() - self.node = node - self.node_cls = node.__class__ - self.pos = pos - self.instance = instance - self.kwargs = kwargs - - def process(self): - self.pos = self.node._prepare_pos_var_for_add_sibling(self.pos) - - if self.instance: - if self.instance.pk: - raise ValueError("Attempted to add a tree node that is already in the database") - newobj = self.instance - else: - # creating a new object - newobj = self.node_cls(**self.kwargs) - - newobj.depth = self.node.depth - - if self.pos == 'sorted-sibling': - siblings = self.node.get_sorted_pos_queryset( - self.node.get_siblings(), newobj) - try: - newpos = siblings.all()[0]._get_lastpos_in_path() - except IndexError: - newpos = None - if newpos is None: - self.pos = 'last-sibling' - else: - newpos, siblings = None, [] - - _, newpath = self.reorder_nodes_before_add_or_move( - self.pos, newpos, self.node.depth, self.node, siblings, None, - False) - - parentpath = self.node._get_basepath(newpath, self.node.depth - 1) - if parentpath: - self.stmts.append( - self.get_sql_update_numchild(parentpath, 'inc')) - - self.run_sql_stmts() - - # saving the instance before returning it - newobj.path = newpath - newobj.save() - - transaction.commit_unless_managed() - return newobj - - -class MP_MoveHandler(MP_ComplexAddMoveHandler): - def __init__(self, node, target, pos=None): - super(MP_MoveHandler, self).__init__() - self.node = node - self.node_cls = node.__class__ - self.target = target - self.pos = pos - - def process(self): - - self.pos = self.node._prepare_pos_var_for_move(self.pos) - - oldpath = self.node.path - - # initialize variables and if moving to a child, updates "move to - # child" to become a "move to sibling" if possible (if it can't - # be done, it means that we are adding the first child) - newdepth, siblings, newpos = self.update_move_to_child_vars() - - if self.target.is_descendant_of(self.node): - raise InvalidMoveToDescendant( - _("Can't move node to a descendant.")) - - if ( - oldpath == self.target.path and - ( - (self.pos == 'left') or - ( - self.pos in ('right', 'last-sibling') and - self.target.path == self.target.get_last_sibling().path - ) or - ( - self.pos == 'first-sibling' and - self.target.path == self.target.get_first_sibling().path - ) - ) - ): - # special cases, not actually moving the node so no need to UPDATE - return - - if self.pos == 'sorted-sibling': - siblings = self.node.get_sorted_pos_queryset( - self.target.get_siblings(), self.node) - try: - newpos = siblings.all()[0]._get_lastpos_in_path() - except IndexError: - newpos = None - if newpos is None: - self.pos = 'last-sibling' - - # generate the sql that will do the actual moving of nodes - oldpath, newpath = self.reorder_nodes_before_add_or_move( - self.pos, newpos, newdepth, self.target, siblings, oldpath, True) - # updates needed for mysql and children count in parents - self.sanity_updates_after_move(oldpath, newpath) - - self.run_sql_stmts() - transaction.commit_unless_managed() - - def sanity_updates_after_move(self, oldpath, newpath): - """ - Updates the list of sql statements needed after moving nodes. - - 1. :attr:`depth` updates *ONLY* needed by mysql databases (*sigh*) - 2. update the number of children of parent nodes - """ - if ( - self.node_cls.get_database_vendor('write') == 'mysql' and - len(oldpath) != len(newpath) - ): - # no words can describe how dumb mysql is - # we must update the depth of the branch in a different query - self.stmts.append( - self.get_mysql_update_depth_in_branch(newpath)) - - oldparentpath = self.node_cls._get_parent_path_from_path(oldpath) - newparentpath = self.node_cls._get_parent_path_from_path(newpath) - if ( - (not oldparentpath and newparentpath) or - (oldparentpath and not newparentpath) or - (oldparentpath != newparentpath) - ): - # node changed parent, updating count - if oldparentpath: - self.stmts.append( - self.get_sql_update_numchild(oldparentpath, 'dec')) - if newparentpath: - self.stmts.append( - self.get_sql_update_numchild(newparentpath, 'inc')) - - def update_move_to_child_vars(self): - """Update preliminar vars in :meth:`move` when moving to a child""" - newdepth = self.target.depth - newpos = None - siblings = [] - if self.pos in ('first-child', 'last-child', 'sorted-child'): - # moving to a child - parent = self.target - newdepth += 1 - if self.target.is_leaf(): - # moving as a target's first child - newpos = 1 - self.pos = 'first-sibling' - siblings = get_base_model_class(self.node_cls).objects.none() - else: - self.target = self.target.get_last_child() - self.pos = { - 'first-child': 'first-sibling', - 'last-child': 'last-sibling', - 'sorted-child': 'sorted-sibling'}[self.pos] - - # this is not for save(), since if needed, will be handled with a - # custom UPDATE, this is only here to update django's object, - # should be useful in loops - parent.numchild += 1 - - return newdepth, siblings, newpos - - def get_mysql_update_depth_in_branch(self, path): - """ - :returns: The sql needed to update the depth of all the nodes in a - branch. - """ - sql = "UPDATE %s SET depth=LENGTH(path)/%%s WHERE path LIKE %%s" % ( - connection.ops.quote_name(get_base_model_class(self.node_cls)._meta.db_table), ) - vals = [self.node_cls.steplen, path + '%'] - return sql, vals - - -class MP_Node(Node): - """Abstract model to create your own Materialized Path Trees.""" - - steplen = 4 - alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' - node_order_by = [] - path = models.CharField(max_length=255, unique=True) - depth = models.PositiveIntegerField() - numchild = models.PositiveIntegerField(default=0) - gap = 1 - - objects = MP_NodeManager() - - numconv_obj_ = None - - @classmethod - def _int2str(cls, num): - return cls.numconv_obj().int2str(num) - - @classmethod - def _str2int(cls, num): - return cls.numconv_obj().str2int(num) - - @classmethod - def numconv_obj(cls): - if cls.numconv_obj_ is None: - cls.numconv_obj_ = NumConv(len(cls.alphabet), cls.alphabet) - return cls.numconv_obj_ - - @classmethod - def add_root(cls, instance=None, **kwargs): - """ - Adds a root node to the tree. - - :raise PathOverflow: when no more root objects can be added - """ - return MP_AddRootHandler(cls, instance, **kwargs).process() - - @classmethod - def dump_bulk(cls, parent=None, keep_ids=True): - """Dumps a tree branch to a python data structure.""" - - cls = get_base_model_class(cls) - - # Because of fix_tree, this method assumes that the depth - # and numchild properties in the nodes can be incorrect, - # so no helper methods are used - qset = cls._get_serializable_model().objects.all() - if parent: - qset = qset.filter(path__startswith=parent.path) - ret, lnk = [], {} - for pyobj in serializers.serialize('python', qset): - # django's serializer stores the attributes in 'fields' - fields = pyobj['fields'] - path = fields['path'] - depth = int(len(path) / cls.steplen) - # this will be useless in load_bulk - del fields['depth'] - del fields['path'] - del fields['numchild'] - if 'id' in fields: - # this happens immediately after a load_bulk - del fields['id'] - - newobj = {'data': fields} - if keep_ids: - newobj['id'] = pyobj['pk'] - - if (not parent and depth == 1) or\ - (parent and len(path) == len(parent.path)): - ret.append(newobj) - else: - parentpath = cls._get_basepath(path, depth - 1) - parentobj = lnk[parentpath] - if 'children' not in parentobj: - parentobj['children'] = [] - parentobj['children'].append(newobj) - lnk[path] = newobj - return ret - - @classmethod - def find_problems(cls): - """ - Checks for problems in the tree structure, problems can occur when: - - 1. your code breaks and you get incomplete transactions (always - use transactions!) - 2. changing the ``steplen`` value in a model (you must - :meth:`dump_bulk` first, change ``steplen`` and then - :meth:`load_bulk` - - :returns: A tuple of five lists: - - 1. a list of ids of nodes with characters not found in the - ``alphabet`` - 2. a list of ids of nodes when a wrong ``path`` length - according to ``steplen`` - 3. a list of ids of orphaned nodes - 4. a list of ids of nodes with the wrong depth value for - their path - 5. a list of ids nodes that report a wrong number of children - """ - cls = get_base_model_class(cls) - - evil_chars, bad_steplen, orphans = [], [], [] - wrong_depth, wrong_numchild = [], [] - for node in cls.objects.all(): - found_error = False - for char in node.path: - if char not in cls.alphabet: - evil_chars.append(node.pk) - found_error = True - break - if found_error: - continue - if len(node.path) % cls.steplen: - bad_steplen.append(node.pk) - continue - try: - node.get_parent(True) - except cls.DoesNotExist: - orphans.append(node.pk) - continue - - if node.depth != int(len(node.path) / cls.steplen): - wrong_depth.append(node.pk) - continue - - real_numchild = cls.objects.filter( - path__range=cls._get_children_path_interval(node.path) - ).extra( - where=['LENGTH(path)/%d=%d' % (cls.steplen, node.depth + 1)] - ).count() - if real_numchild != node.numchild: - wrong_numchild.append(node.pk) - continue - - return evil_chars, bad_steplen, orphans, wrong_depth, wrong_numchild - - @classmethod - def fix_tree(cls, destructive=False): - """ - Solves some problems that can appear when transactions are not used and - a piece of code breaks, leaving the tree in an inconsistent state. - - The problems this method solves are: - - 1. Nodes with an incorrect ``depth`` or ``numchild`` values due to - incorrect code and lack of database transactions. - 2. "Holes" in the tree. This is normal if you move/delete nodes a - lot. Holes in a tree don't affect performance, - 3. Incorrect ordering of nodes when ``node_order_by`` is enabled. - Ordering is enforced on *node insertion*, so if an attribute in - ``node_order_by`` is modified after the node is inserted, the - tree ordering will be inconsistent. - - :param destructive: - - A boolean value. If True, a more agressive fix_tree method will be - attemped. If False (the default), it will use a safe (and fast!) - fix approach, but it will only solve the ``depth`` and - ``numchild`` nodes, it won't fix the tree holes or broken path - ordering. - - .. warning:: - - Currently what the ``destructive`` method does is: - - 1. Backup the tree with :meth:`dump_data` - 2. Remove all nodes in the tree. - 3. Restore the tree with :meth:`load_data` - - So, even when the primary keys of your nodes will be preserved, - this method isn't foreign-key friendly. That needs complex - in-place tree reordering, not available at the moment (hint: - patches are welcome). - """ - cls = get_base_model_class(cls) - - if destructive: - dump = cls.dump_bulk(None, True) - cls.objects.all().delete() - cls.load_bulk(dump, None, True) - else: - cursor = cls._get_database_cursor('write') - - # fix the depth field - # we need the WHERE to speed up postgres - sql = "UPDATE %s "\ - "SET depth=LENGTH(path)/%%s "\ - "WHERE depth!=LENGTH(path)/%%s" % ( - connection.ops.quote_name(cls._meta.db_table), ) - vals = [cls.steplen, cls.steplen] - cursor.execute(sql, vals) - - # fix the numchild field - vals = ['_' * cls.steplen] - # the cake and sql portability are a lie - if cls.get_database_vendor('read') == 'mysql': - sql = "SELECT tbn1.path, tbn1.numchild, ("\ - "SELECT COUNT(1) "\ - "FROM %(table)s AS tbn2 "\ - "WHERE tbn2.path LIKE "\ - "CONCAT(tbn1.path, %%s)) AS real_numchild "\ - "FROM %(table)s AS tbn1 "\ - "HAVING tbn1.numchild != real_numchild" % { - 'table': connection.ops.quote_name( - cls._meta.db_table)} - else: - subquery = "(SELECT COUNT(1) FROM %(table)s AS tbn2"\ - " WHERE tbn2.path LIKE tbn1.path||%%s)" - sql = ("SELECT tbn1.path, tbn1.numchild, " + subquery + - " FROM %(table)s AS tbn1 WHERE tbn1.numchild != " + - subquery) - sql = sql % { - 'table': connection.ops.quote_name(cls._meta.db_table)} - # we include the subquery twice - vals *= 2 - cursor.execute(sql, vals) - sql = "UPDATE %(table)s "\ - "SET numchild=%%s "\ - "WHERE path=%%s" % { - 'table': connection.ops.quote_name(cls._meta.db_table)} - for node_data in cursor.fetchall(): - vals = [node_data[2], node_data[0]] - cursor.execute(sql, vals) - - transaction.commit_unless_managed() - - @classmethod - def get_tree(cls, parent=None): - """ - :returns: - - A *queryset* of nodes ordered as DFS, including the parent. - If no parent is given, the entire tree is returned. - """ - cls = get_base_model_class(cls) # method doesn't make sense when constrained to a subclass - - if parent is None: - # return the entire tree - return cls.objects.all() - if parent.is_leaf(): - return cls.objects.filter(pk=parent.pk) - return cls.objects.filter(path__startswith=parent.path, - depth__gte=parent.depth) - - @classmethod - def get_root_nodes(cls): - """:returns: A queryset containing the root nodes in the tree.""" - return get_base_model_class(cls).objects.filter(depth=1) - - @classmethod - def get_descendants_group_count(cls, parent=None): - """ - Helper for a very common case: get a group of siblings and the number - of *descendants* in every sibling. - """ - - #~ - # disclaimer: this is the FOURTH implementation I wrote for this - # function. I really tried to make it return a queryset, but doing so - # with a *single* query isn't trivial with Django's ORM. - - # ok, I DID manage to make Django's ORM return a queryset here, - # defining two querysets, passing one subquery in the tables parameters - # of .extra() of the second queryset, using the undocumented order_by - # feature, and using a HORRIBLE hack to avoid django quoting the - # subquery as a table, BUT (and there is always a but) the hack didn't - # survive turning the QuerySet into a ValuesQuerySet, so I just used - # good old SQL. - # NOTE: in case there is interest, the hack to avoid django quoting the - # subquery as a table, was adding the subquery to the alias cache of - # the queryset's query object: - # - # qset.query.quote_cache[subquery] = subquery - # - # If there is a better way to do this in an UNMODIFIED django 1.0, let - # me know. - #~ - - cls = get_base_model_class(cls) - - if parent: - depth = parent.depth + 1 - params = cls._get_children_path_interval(parent.path) - extrand = 'AND path BETWEEN %s AND %s' - else: - depth = 1 - params = [] - extrand = '' - - sql = 'SELECT * FROM %(table)s AS t1 INNER JOIN '\ - ' (SELECT '\ - ' SUBSTR(path, 1, %(subpathlen)s) AS subpath, '\ - ' COUNT(1)-1 AS count '\ - ' FROM %(table)s '\ - ' WHERE depth >= %(depth)s %(extrand)s'\ - ' GROUP BY subpath) AS t2 '\ - ' ON t1.path=t2.subpath '\ - ' ORDER BY t1.path' % { - 'table': connection.ops.quote_name(cls._meta.db_table), - 'subpathlen': depth * cls.steplen, - 'depth': depth, - 'extrand': extrand} - cursor = cls._get_database_cursor('write') - cursor.execute(sql, params) - - ret = [] - field_names = [field[0] for field in cursor.description] - for node_data in cursor.fetchall(): - node = cls(**dict(zip(field_names, node_data[:-2]))) - node.descendants_count = node_data[-1] - ret.append(node) - transaction.commit_unless_managed() - return ret - - def get_depth(self): - """:returns: the depth (level) of the node""" - return self.depth - - def get_siblings(self): - """ - :returns: A queryset of all the node's siblings, including the node - itself. - """ - qset = get_base_model_class(self.__class__).objects.filter(depth=self.depth) - if self.depth > 1: - # making sure the non-root nodes share a parent - parentpath = self._get_basepath(self.path, self.depth - 1) - qset = qset.filter( - path__range=self._get_children_path_interval(parentpath)) - return qset - - def get_children(self): - """:returns: A queryset of all the node's children""" - if self.is_leaf(): - return get_base_model_class(self.__class__).objects.none() - return get_base_model_class(self.__class__).objects.filter( - depth=self.depth + 1, - path__range=self._get_children_path_interval(self.path) - ) - - def get_next_sibling(self): - """ - :returns: The next node's sibling, or None if it was the rightmost - sibling. - """ - try: - return self.get_siblings().filter(path__gt=self.path)[0] - except IndexError: - return None - - def get_descendants(self): - """ - :returns: A queryset of all the node's descendants as DFS, doesn't - include the node itself - """ - return self.__class__.get_tree(self).exclude(pk=self.pk) - - def get_prev_sibling(self): - """ - :returns: The previous node's sibling, or None if it was the leftmost - sibling. - """ - try: - return self.get_siblings().filter(path__lt=self.path).reverse()[0] - except IndexError: - return None - - def get_children_count(self): - """ - :returns: The number the node's children, calculated in the most - efficient possible way. - """ - return self.numchild - - def is_sibling_of(self, node): - """ - :returns: ``True`` if the node is a sibling of another node given as an - argument, else, returns ``False`` - """ - aux = self.depth == node.depth - # Check non-root nodes share a parent only if they have the same depth - if aux and self.depth > 1: - # making sure the non-root nodes share a parent - parentpath = self._get_basepath(self.path, self.depth - 1) - return aux and node.path.startswith(parentpath) - return aux - - def is_child_of(self, node): - """ - :returns: ``True`` is the node if a child of another node given as an - argument, else, returns ``False`` - """ - return (self.path.startswith(node.path) and - self.depth == node.depth + 1) - - def is_descendant_of(self, node): - """ - :returns: ``True`` if the node is a descendant of another node given - as an argument, else, returns ``False`` - """ - return self.path.startswith(node.path) and self.depth > node.depth - - def add_child(self, instance=None, **kwargs): - """ - Adds a child to the node. - - :raise PathOverflow: when no more child nodes can be added - """ - return MP_AddChildHandler(self, instance, **kwargs).process() - - def add_sibling(self, pos=None, instance=None, **kwargs): - """ - Adds a new node as a sibling to the current node object. - - :raise PathOverflow: when the library can't make room for the - node's new position - """ - return MP_AddSiblingHandler(self, pos, instance, **kwargs).process() - - def get_root(self): - """:returns: the root node for the current node object.""" - return get_base_model_class(self.__class__).objects.get(path=self.path[0:self.steplen]) - - def is_leaf(self): - """:returns: True if the node is a leaf node (else, returns False)""" - return self.numchild == 0 - - def get_ancestors(self): - """ - :returns: A queryset containing the current node object's ancestors, - starting by the root node and descending to the parent. - """ - paths = [ - self.path[0:pos] - for pos in range(0, len(self.path), self.steplen)[1:] - ] - return get_base_model_class(self.__class__).objects.filter(path__in=paths).order_by('depth') - - def get_parent(self, update=False): - """ - :returns: the parent node of the current node object. - Caches the result in the object itself to help in loops. - """ - depth = int(len(self.path) / self.steplen) - if depth <= 1: - return - try: - if update: - del self._cached_parent_obj - else: - return self._cached_parent_obj - except AttributeError: - pass - parentpath = self._get_basepath(self.path, depth - 1) - self._cached_parent_obj = get_base_model_class(self.__class__).objects.get(path=parentpath) - return self._cached_parent_obj - - def move(self, target, pos=None): - """ - Moves the current node and all it's descendants to a new position - relative to another node. - - :raise PathOverflow: when the library can't make room for the - node's new position - """ - return MP_MoveHandler(self, target, pos).process() - - @classmethod - def _get_basepath(cls, path, depth): - """:returns: The base path of another path up to a given depth""" - if path: - return path[0:depth * cls.steplen] - return '' - - @classmethod - def _get_path(cls, path, depth, newstep): - """ - Builds a path given some values - - :param path: the base path - :param depth: the depth of the node - :param newstep: the value (integer) of the new step - """ - parentpath = cls._get_basepath(path, depth - 1) - key = cls._int2str(newstep) - return '%s%s%s' % (parentpath, - '0' * (cls.steplen - len(key)), - key) - - def _inc_path(self): - """:returns: The path of the next sibling of a given node path.""" - newpos = self._str2int(self.path[-self.steplen:]) + 1 - key = self._int2str(newpos) - if len(key) > self.steplen: - raise PathOverflow(_("Path Overflow from: '%s'" % (self.path, ))) - return '%s%s%s' % (self.path[:-self.steplen], - '0' * (self.steplen - len(key)), - key) - - def _get_lastpos_in_path(self): - """:returns: The integer value of the last step in a path.""" - return self._str2int(self.path[-self.steplen:]) - - @classmethod - def _get_parent_path_from_path(cls, path): - """:returns: The parent path for a given path""" - if path: - return path[0:len(path) - cls.steplen] - return '' - - @classmethod - def _get_children_path_interval(cls, path): - """:returns: An interval of all possible children paths for a node.""" - return (path + cls.alphabet[0] * cls.steplen, - path + cls.alphabet[-1] * cls.steplen) - - class Meta: - """Abstract model.""" - abstract = True diff --git a/wagtail/vendor/django-treebeard/treebeard/ns_tree.py b/wagtail/vendor/django-treebeard/treebeard/ns_tree.py deleted file mode 100644 index 20d184f7e..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/ns_tree.py +++ /dev/null @@ -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 diff --git a/wagtail/vendor/django-treebeard/treebeard/numconv.py b/wagtail/vendor/django-treebeard/treebeard/numconv.py deleted file mode 100644 index 4c8d81faa..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/numconv.py +++ /dev/null @@ -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) diff --git a/wagtail/vendor/django-treebeard/treebeard/static/treebeard/expand-collapse.png b/wagtail/vendor/django-treebeard/treebeard/static/treebeard/expand-collapse.png deleted file mode 100644 index 2f9da7e11..000000000 Binary files a/wagtail/vendor/django-treebeard/treebeard/static/treebeard/expand-collapse.png and /dev/null differ diff --git a/wagtail/vendor/django-treebeard/treebeard/static/treebeard/jquery-ui-1.8.5.custom.min.js b/wagtail/vendor/django-treebeard/treebeard/static/treebeard/jquery-ui-1.8.5.custom.min.js deleted file mode 100644 index 1461652d9..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/static/treebeard/jquery-ui-1.8.5.custom.min.js +++ /dev/null @@ -1,1816 +0,0 @@ -/*! - * jQuery UI 1.8.5 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI - */ -(function (c, j) { - function k(a) { - return!c(a).parents().andSelf().filter(function () { - return c.curCSS(this, "visibility") === "hidden" || c.expr.filters.hidden(this) - }).length - } - - c.ui = c.ui || {}; - if (!c.ui.version) { - c.extend(c.ui, {version: "1.8.5", keyCode: {ALT: 18, BACKSPACE: 8, CAPS_LOCK: 20, COMMA: 188, COMMAND: 91, COMMAND_LEFT: 91, COMMAND_RIGHT: 93, CONTROL: 17, DELETE: 46, DOWN: 40, END: 35, ENTER: 13, ESCAPE: 27, HOME: 36, INSERT: 45, LEFT: 37, MENU: 93, NUMPAD_ADD: 107, NUMPAD_DECIMAL: 110, NUMPAD_DIVIDE: 111, NUMPAD_ENTER: 108, NUMPAD_MULTIPLY: 106, - NUMPAD_SUBTRACT: 109, PAGE_DOWN: 34, PAGE_UP: 33, PERIOD: 190, RIGHT: 39, SHIFT: 16, SPACE: 32, TAB: 9, UP: 38, WINDOWS: 91}}); - c.fn.extend({_focus: c.fn.focus, focus: function (a, b) { - return typeof a === "number" ? this.each(function () { - var d = this; - setTimeout(function () { - c(d).focus(); - b && b.call(d) - }, a) - }) : this._focus.apply(this, arguments) - }, scrollParent: function () { - var a; - a = c.browser.msie && /(static|relative)/.test(this.css("position")) || /absolute/.test(this.css("position")) ? this.parents().filter(function () { - return/(relative|absolute|fixed)/.test(c.curCSS(this, - "position", 1)) && /(auto|scroll)/.test(c.curCSS(this, "overflow", 1) + c.curCSS(this, "overflow-y", 1) + c.curCSS(this, "overflow-x", 1)) - }).eq(0) : this.parents().filter(function () { - return/(auto|scroll)/.test(c.curCSS(this, "overflow", 1) + c.curCSS(this, "overflow-y", 1) + c.curCSS(this, "overflow-x", 1)) - }).eq(0); - return/fixed/.test(this.css("position")) || !a.length ? c(document) : a - }, zIndex: function (a) { - if (a !== j)return this.css("zIndex", a); - if (this.length) { - a = c(this[0]); - for (var b; a.length && a[0] !== document;) { - b = a.css("position"); - if (b === "absolute" || b === "relative" || b === "fixed") { - b = parseInt(a.css("zIndex")); - if (!isNaN(b) && b != 0)return b - } - a = a.parent() - } - } - return 0 - }, disableSelection: function () { - return this.bind("mousedown.ui-disableSelection selectstart.ui-disableSelection", function (a) { - a.preventDefault() - }) - }, enableSelection: function () { - return this.unbind(".ui-disableSelection") - }}); - c.each(["Width", "Height"], function (a, b) { - function d(f, g, l, m) { - c.each(e, function () { - g -= parseFloat(c.curCSS(f, "padding" + this, true)) || 0; - if (l)g -= parseFloat(c.curCSS(f, - "border" + this + "Width", true)) || 0; - if (m)g -= parseFloat(c.curCSS(f, "margin" + this, true)) || 0 - }); - return g - } - - var e = b === "Width" ? ["Left", "Right"] : ["Top", "Bottom"], h = b.toLowerCase(), i = {innerWidth: c.fn.innerWidth, innerHeight: c.fn.innerHeight, outerWidth: c.fn.outerWidth, outerHeight: c.fn.outerHeight}; - c.fn["inner" + b] = function (f) { - if (f === j)return i["inner" + b].call(this); - return this.each(function () { - c.style(this, h, d(this, f) + "px") - }) - }; - c.fn["outer" + b] = function (f, g) { - if (typeof f !== "number")return i["outer" + b].call(this, f); - return this.each(function () { - c.style(this, - h, d(this, f, true, g) + "px") - }) - } - }); - c.extend(c.expr[":"], {data: function (a, b, d) { - return!!c.data(a, d[3]) - }, focusable: function (a) { - var b = a.nodeName.toLowerCase(), d = c.attr(a, "tabindex"); - if ("area" === b) { - b = a.parentNode; - d = b.name; - if (!a.href || !d || b.nodeName.toLowerCase() !== "map")return false; - a = c("img[usemap=#" + d + "]")[0]; - return!!a && k(a) - } - return(/input|select|textarea|button|object/.test(b) ? !a.disabled : "a" == b ? a.href || !isNaN(d) : !isNaN(d)) && k(a) - }, tabbable: function (a) { - var b = c.attr(a, "tabindex"); - return(isNaN(b) || b >= 0) && c(a).is(":focusable") - }}); - c(function () { - var a = document.createElement("div"), b = document.body; - c.extend(a.style, {minHeight: "100px", height: "auto", padding: 0, borderWidth: 0}); - c.support.minHeight = b.appendChild(a).offsetHeight === 100; - b.removeChild(a).style.display = "none" - }); - c.extend(c.ui, {plugin: {add: function (a, b, d) { - a = c.ui[a].prototype; - for (var e in d) { - a.plugins[e] = a.plugins[e] || []; - a.plugins[e].push([b, d[e]]) - } - }, call: function (a, b, d) { - if ((b = a.plugins[b]) && a.element[0].parentNode)for (var e = 0; e < b.length; e++)a.options[b[e][0]] && b[e][1].apply(a.element, - d) - }}, contains: function (a, b) { - return document.compareDocumentPosition ? a.compareDocumentPosition(b) & 16 : a !== b && a.contains(b) - }, hasScroll: function (a, b) { - if (c(a).css("overflow") === "hidden")return false; - b = b && b === "left" ? "scrollLeft" : "scrollTop"; - var d = false; - if (a[b] > 0)return true; - a[b] = 1; - d = a[b] > 0; - a[b] = 0; - return d - }, isOverAxis: function (a, b, d) { - return a > b && a < b + d - }, isOver: function (a, b, d, e, h, i) { - return c.ui.isOverAxis(a, d, h) && c.ui.isOverAxis(b, e, i) - }}) - } -})(jQuery); -; -/*! - * jQuery UI Widget 1.8.5 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Widget - */ -(function (b, j) { - if (b.cleanData) { - var k = b.cleanData; - b.cleanData = function (a) { - for (var c = 0, d; (d = a[c]) != null; c++)b(d).triggerHandler("remove"); - k(a) - } - } else { - var l = b.fn.remove; - b.fn.remove = function (a, c) { - return this.each(function () { - if (!c)if (!a || b.filter(a, [this]).length)b("*", this).add([this]).each(function () { - b(this).triggerHandler("remove") - }); - return l.call(b(this), a, c) - }) - } - } - b.widget = function (a, c, d) { - var e = a.split(".")[0], f; - a = a.split(".")[1]; - f = e + "-" + a; - if (!d) { - d = c; - c = b.Widget - } - b.expr[":"][f] = function (h) { - return!!b.data(h, - a) - }; - b[e] = b[e] || {}; - b[e][a] = function (h, g) { - arguments.length && this._createWidget(h, g) - }; - c = new c; - c.options = b.extend(true, {}, c.options); - b[e][a].prototype = b.extend(true, c, {namespace: e, widgetName: a, widgetEventPrefix: b[e][a].prototype.widgetEventPrefix || a, widgetBaseClass: f}, d); - b.widget.bridge(a, b[e][a]) - }; - b.widget.bridge = function (a, c) { - b.fn[a] = function (d) { - var e = typeof d === "string", f = Array.prototype.slice.call(arguments, 1), h = this; - d = !e && f.length ? b.extend.apply(null, [true, d].concat(f)) : d; - if (e && d.substring(0, 1) === - "_")return h; - e ? this.each(function () { - var g = b.data(this, a); - if (!g)throw"cannot call methods on " + a + " prior to initialization; attempted to call method '" + d + "'"; - if (!b.isFunction(g[d]))throw"no such method '" + d + "' for " + a + " widget instance"; - var i = g[d].apply(g, f); - if (i !== g && i !== j) { - h = i; - return false - } - }) : this.each(function () { - var g = b.data(this, a); - g ? g.option(d || {})._init() : b.data(this, a, new c(d, this)) - }); - return h - } - }; - b.Widget = function (a, c) { - arguments.length && this._createWidget(a, c) - }; - b.Widget.prototype = {widgetName: "widget", - widgetEventPrefix: "", options: {disabled: false}, _createWidget: function (a, c) { - b.data(c, this.widgetName, this); - this.element = b(c); - this.options = b.extend(true, {}, this.options, b.metadata && b.metadata.get(c)[this.widgetName], a); - var d = this; - this.element.bind("remove." + this.widgetName, function () { - d.destroy() - }); - this._create(); - this._init() - }, _create: function () { - }, _init: function () { - }, destroy: function () { - this.element.unbind("." + this.widgetName).removeData(this.widgetName); - this.widget().unbind("." + this.widgetName).removeAttr("aria-disabled").removeClass(this.widgetBaseClass + - "-disabled ui-state-disabled") - }, widget: function () { - return this.element - }, option: function (a, c) { - var d = a, e = this; - if (arguments.length === 0)return b.extend({}, e.options); - if (typeof a === "string") { - if (c === j)return this.options[a]; - d = {}; - d[a] = c - } - b.each(d, function (f, h) { - e._setOption(f, h) - }); - return e - }, _setOption: function (a, c) { - this.options[a] = c; - if (a === "disabled")this.widget()[c ? "addClass" : "removeClass"](this.widgetBaseClass + "-disabled ui-state-disabled").attr("aria-disabled", c); - return this - }, enable: function () { - return this._setOption("disabled", - false) - }, disable: function () { - return this._setOption("disabled", true) - }, _trigger: function (a, c, d) { - var e = this.options[a]; - c = b.Event(c); - c.type = (a === this.widgetEventPrefix ? a : this.widgetEventPrefix + a).toLowerCase(); - d = d || {}; - if (c.originalEvent) { - a = b.event.props.length; - for (var f; a;) { - f = b.event.props[--a]; - c[f] = c.originalEvent[f] - } - } - this.element.trigger(c, d); - return!(b.isFunction(e) && e.call(this.element[0], c, d) === false || c.isDefaultPrevented()) - }} -})(jQuery); -; -/*! - * jQuery UI Mouse 1.8.5 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Mouse - * - * Depends: - * jquery.ui.widget.js - */ -(function (c) { - c.widget("ui.mouse", {options: {cancel: ":input,option", distance: 1, delay: 0}, _mouseInit: function () { - var a = this; - this.element.bind("mousedown." + this.widgetName,function (b) { - return a._mouseDown(b) - }).bind("click." + this.widgetName, function (b) { - if (a._preventClickEvent) { - a._preventClickEvent = false; - b.stopImmediatePropagation(); - return false - } - }); - this.started = false - }, _mouseDestroy: function () { - this.element.unbind("." + this.widgetName) - }, _mouseDown: function (a) { - a.originalEvent = a.originalEvent || {}; - if (!a.originalEvent.mouseHandled) { - this._mouseStarted && - this._mouseUp(a); - this._mouseDownEvent = a; - var b = this, e = a.which == 1, f = typeof this.options.cancel == "string" ? c(a.target).parents().add(a.target).filter(this.options.cancel).length : false; - if (!e || f || !this._mouseCapture(a))return true; - this.mouseDelayMet = !this.options.delay; - if (!this.mouseDelayMet)this._mouseDelayTimer = setTimeout(function () { - b.mouseDelayMet = true - }, this.options.delay); - if (this._mouseDistanceMet(a) && this._mouseDelayMet(a)) { - this._mouseStarted = this._mouseStart(a) !== false; - if (!this._mouseStarted) { - a.preventDefault(); - return true - } - } - this._mouseMoveDelegate = function (d) { - return b._mouseMove(d) - }; - this._mouseUpDelegate = function (d) { - return b._mouseUp(d) - }; - c(document).bind("mousemove." + this.widgetName, this._mouseMoveDelegate).bind("mouseup." + this.widgetName, this._mouseUpDelegate); - c.browser.safari || a.preventDefault(); - return a.originalEvent.mouseHandled = true - } - }, _mouseMove: function (a) { - if (c.browser.msie && !a.button)return this._mouseUp(a); - if (this._mouseStarted) { - this._mouseDrag(a); - return a.preventDefault() - } - if (this._mouseDistanceMet(a) && - this._mouseDelayMet(a))(this._mouseStarted = this._mouseStart(this._mouseDownEvent, a) !== false) ? this._mouseDrag(a) : this._mouseUp(a); - return!this._mouseStarted - }, _mouseUp: function (a) { - c(document).unbind("mousemove." + this.widgetName, this._mouseMoveDelegate).unbind("mouseup." + this.widgetName, this._mouseUpDelegate); - if (this._mouseStarted) { - this._mouseStarted = false; - this._preventClickEvent = a.target == this._mouseDownEvent.target; - this._mouseStop(a) - } - return false - }, _mouseDistanceMet: function (a) { - return Math.max(Math.abs(this._mouseDownEvent.pageX - - a.pageX), Math.abs(this._mouseDownEvent.pageY - a.pageY)) >= this.options.distance - }, _mouseDelayMet: function () { - return this.mouseDelayMet - }, _mouseStart: function () { - }, _mouseDrag: function () { - }, _mouseStop: function () { - }, _mouseCapture: function () { - return true - }}) -})(jQuery); -; -/* - * jQuery UI Position 1.8.5 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Position - */ -(function (c) { - c.ui = c.ui || {}; - var n = /left|center|right/, o = /top|center|bottom/, t = c.fn.position, u = c.fn.offset; - c.fn.position = function (b) { - if (!b || !b.of)return t.apply(this, arguments); - b = c.extend({}, b); - var a = c(b.of), d = a[0], g = (b.collision || "flip").split(" "), e = b.offset ? b.offset.split(" ") : [0, 0], h, k, j; - if (d.nodeType === 9) { - h = a.width(); - k = a.height(); - j = {top: 0, left: 0} - } else if (d.scrollTo && d.document) { - h = a.width(); - k = a.height(); - j = {top: a.scrollTop(), left: a.scrollLeft()} - } else if (d.preventDefault) { - b.at = "left top"; - h = k = 0; - j = - {top: b.of.pageY, left: b.of.pageX} - } else { - h = a.outerWidth(); - k = a.outerHeight(); - j = a.offset() - } - c.each(["my", "at"], function () { - var f = (b[this] || "").split(" "); - if (f.length === 1)f = n.test(f[0]) ? f.concat(["center"]) : o.test(f[0]) ? ["center"].concat(f) : ["center", "center"]; - f[0] = n.test(f[0]) ? f[0] : "center"; - f[1] = o.test(f[1]) ? f[1] : "center"; - b[this] = f - }); - if (g.length === 1)g[1] = g[0]; - e[0] = parseInt(e[0], 10) || 0; - if (e.length === 1)e[1] = e[0]; - e[1] = parseInt(e[1], 10) || 0; - if (b.at[0] === "right")j.left += h; else if (b.at[0] === "center")j.left += h / - 2; - if (b.at[1] === "bottom")j.top += k; else if (b.at[1] === "center")j.top += k / 2; - j.left += e[0]; - j.top += e[1]; - return this.each(function () { - var f = c(this), l = f.outerWidth(), m = f.outerHeight(), p = parseInt(c.curCSS(this, "marginLeft", true)) || 0, q = parseInt(c.curCSS(this, "marginTop", true)) || 0, v = l + p + parseInt(c.curCSS(this, "marginRight", true)) || 0, w = m + q + parseInt(c.curCSS(this, "marginBottom", true)) || 0, i = c.extend({}, j), r; - if (b.my[0] === "right")i.left -= l; else if (b.my[0] === "center")i.left -= l / 2; - if (b.my[1] === "bottom")i.top -= m; else if (b.my[1] === - "center")i.top -= m / 2; - i.left = parseInt(i.left); - i.top = parseInt(i.top); - r = {left: i.left - p, top: i.top - q}; - c.each(["left", "top"], function (s, x) { - c.ui.position[g[s]] && c.ui.position[g[s]][x](i, {targetWidth: h, targetHeight: k, elemWidth: l, elemHeight: m, collisionPosition: r, collisionWidth: v, collisionHeight: w, offset: e, my: b.my, at: b.at}) - }); - c.fn.bgiframe && f.bgiframe(); - f.offset(c.extend(i, {using: b.using})) - }) - }; - c.ui.position = {fit: {left: function (b, a) { - var d = c(window); - d = a.collisionPosition.left + a.collisionWidth - d.width() - d.scrollLeft(); - b.left = d > 0 ? b.left - d : Math.max(b.left - a.collisionPosition.left, b.left) - }, top: function (b, a) { - var d = c(window); - d = a.collisionPosition.top + a.collisionHeight - d.height() - d.scrollTop(); - b.top = d > 0 ? b.top - d : Math.max(b.top - a.collisionPosition.top, b.top) - }}, flip: {left: function (b, a) { - if (a.at[0] !== "center") { - var d = c(window); - d = a.collisionPosition.left + a.collisionWidth - d.width() - d.scrollLeft(); - var g = a.my[0] === "left" ? -a.elemWidth : a.my[0] === "right" ? a.elemWidth : 0, e = a.at[0] === "left" ? a.targetWidth : -a.targetWidth, h = -2 * a.offset[0]; - b.left += a.collisionPosition.left < 0 ? g + e + h : d > 0 ? g + e + h : 0 - } - }, top: function (b, a) { - if (a.at[1] !== "center") { - var d = c(window); - d = a.collisionPosition.top + a.collisionHeight - d.height() - d.scrollTop(); - var g = a.my[1] === "top" ? -a.elemHeight : a.my[1] === "bottom" ? a.elemHeight : 0, e = a.at[1] === "top" ? a.targetHeight : -a.targetHeight, h = -2 * a.offset[1]; - b.top += a.collisionPosition.top < 0 ? g + e + h : d > 0 ? g + e + h : 0 - } - }}}; - if (!c.offset.setOffset) { - c.offset.setOffset = function (b, a) { - if (/static/.test(c.curCSS(b, "position")))b.style.position = "relative"; - var d = - c(b), g = d.offset(), e = parseInt(c.curCSS(b, "top", true), 10) || 0, h = parseInt(c.curCSS(b, "left", true), 10) || 0; - g = {top: a.top - g.top + e, left: a.left - g.left + h}; - "using"in a ? a.using.call(b, g) : d.css(g) - }; - c.fn.offset = function (b) { - var a = this[0]; - if (!a || !a.ownerDocument)return null; - if (b)return this.each(function () { - c.offset.setOffset(this, b) - }); - return u.call(this) - } - } -})(jQuery); -; -/* - * jQuery UI Slider 1.8.5 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Slider - * - * Depends: - * jquery.ui.core.js - * jquery.ui.mouse.js - * jquery.ui.widget.js - */ -(function (d) { - d.widget("ui.slider", d.ui.mouse, {widgetEventPrefix: "slide", options: {animate: false, distance: 0, max: 100, min: 0, orientation: "horizontal", range: false, step: 1, value: 0, values: null}, _create: function () { - var a = this, b = this.options; - this._mouseSliding = this._keySliding = false; - this._animateOff = true; - this._handleIndex = null; - this._detectOrientation(); - this._mouseInit(); - this.element.addClass("ui-slider ui-slider-" + this.orientation + " ui-widget ui-widget-content ui-corner-all"); - b.disabled && this.element.addClass("ui-slider-disabled ui-disabled"); - this.range = d([]); - if (b.range) { - if (b.range === true) { - this.range = d("
"); - if (!b.values)b.values = [this._valueMin(), this._valueMin()]; - if (b.values.length && b.values.length !== 2)b.values = [b.values[0], b.values[0]] - } else this.range = d("
"); - this.range.appendTo(this.element).addClass("ui-slider-range"); - if (b.range === "min" || b.range === "max")this.range.addClass("ui-slider-range-" + b.range); - this.range.addClass("ui-widget-header") - } - d(".ui-slider-handle", this.element).length === 0 && d("").appendTo(this.element).addClass("ui-slider-handle"); - if (b.values && b.values.length)for (; d(".ui-slider-handle", this.element).length < b.values.length;)d("").appendTo(this.element).addClass("ui-slider-handle"); - this.handles = d(".ui-slider-handle", this.element).addClass("ui-state-default ui-corner-all"); - this.handle = this.handles.eq(0); - this.handles.add(this.range).filter("a").click(function (c) { - c.preventDefault() - }).hover(function () { - b.disabled || d(this).addClass("ui-state-hover") - },function () { - d(this).removeClass("ui-state-hover") - }).focus(function () { - if (b.disabled)d(this).blur(); - else { - d(".ui-slider .ui-state-focus").removeClass("ui-state-focus"); - d(this).addClass("ui-state-focus") - } - }).blur(function () { - d(this).removeClass("ui-state-focus") - }); - this.handles.each(function (c) { - d(this).data("index.ui-slider-handle", c) - }); - this.handles.keydown(function (c) { - var e = true, f = d(this).data("index.ui-slider-handle"), h, g, i; - if (!a.options.disabled) { - switch (c.keyCode) { - case d.ui.keyCode.HOME: - case d.ui.keyCode.END: - case d.ui.keyCode.PAGE_UP: - case d.ui.keyCode.PAGE_DOWN: - case d.ui.keyCode.UP: - case d.ui.keyCode.RIGHT: - case d.ui.keyCode.DOWN: - case d.ui.keyCode.LEFT: - e = - false; - if (!a._keySliding) { - a._keySliding = true; - d(this).addClass("ui-state-active"); - h = a._start(c, f); - if (h === false)return - } - break - } - i = a.options.step; - h = a.options.values && a.options.values.length ? (g = a.values(f)) : (g = a.value()); - switch (c.keyCode) { - case d.ui.keyCode.HOME: - g = a._valueMin(); - break; - case d.ui.keyCode.END: - g = a._valueMax(); - break; - case d.ui.keyCode.PAGE_UP: - g = a._trimAlignValue(h + (a._valueMax() - a._valueMin()) / 5); - break; - case d.ui.keyCode.PAGE_DOWN: - g = a._trimAlignValue(h - (a._valueMax() - a._valueMin()) / 5); - break; - case d.ui.keyCode.UP: - case d.ui.keyCode.RIGHT: - if (h === - a._valueMax())return; - g = a._trimAlignValue(h + i); - break; - case d.ui.keyCode.DOWN: - case d.ui.keyCode.LEFT: - if (h === a._valueMin())return; - g = a._trimAlignValue(h - i); - break - } - a._slide(c, f, g); - return e - } - }).keyup(function (c) { - var e = d(this).data("index.ui-slider-handle"); - if (a._keySliding) { - a._keySliding = false; - a._stop(c, e); - a._change(c, e); - d(this).removeClass("ui-state-active") - } - }); - this._refreshValue(); - this._animateOff = false - }, destroy: function () { - this.handles.remove(); - this.range.remove(); - this.element.removeClass("ui-slider ui-slider-horizontal ui-slider-vertical ui-slider-disabled ui-widget ui-widget-content ui-corner-all").removeData("slider").unbind(".slider"); - this._mouseDestroy(); - return this - }, _mouseCapture: function (a) { - var b = this.options, c, e, f, h, g; - if (b.disabled)return false; - this.elementSize = {width: this.element.outerWidth(), height: this.element.outerHeight()}; - this.elementOffset = this.element.offset(); - c = this._normValueFromMouse({x: a.pageX, y: a.pageY}); - e = this._valueMax() - this._valueMin() + 1; - h = this; - this.handles.each(function (i) { - var j = Math.abs(c - h.values(i)); - if (e > j) { - e = j; - f = d(this); - g = i - } - }); - if (b.range === true && this.values(1) === b.min) { - g += 1; - f = d(this.handles[g]) - } - if (this._start(a, - g) === false)return false; - this._mouseSliding = true; - h._handleIndex = g; - f.addClass("ui-state-active").focus(); - b = f.offset(); - this._clickOffset = !d(a.target).parents().andSelf().is(".ui-slider-handle") ? {left: 0, top: 0} : {left: a.pageX - b.left - f.width() / 2, top: a.pageY - b.top - f.height() / 2 - (parseInt(f.css("borderTopWidth"), 10) || 0) - (parseInt(f.css("borderBottomWidth"), 10) || 0) + (parseInt(f.css("marginTop"), 10) || 0)}; - this._slide(a, g, c); - return this._animateOff = true - }, _mouseStart: function () { - return true - }, _mouseDrag: function (a) { - var b = - this._normValueFromMouse({x: a.pageX, y: a.pageY}); - this._slide(a, this._handleIndex, b); - return false - }, _mouseStop: function (a) { - this.handles.removeClass("ui-state-active"); - this._mouseSliding = false; - this._stop(a, this._handleIndex); - this._change(a, this._handleIndex); - this._clickOffset = this._handleIndex = null; - return this._animateOff = false - }, _detectOrientation: function () { - this.orientation = this.options.orientation === "vertical" ? "vertical" : "horizontal" - }, _normValueFromMouse: function (a) { - var b; - if (this.orientation === "horizontal") { - b = - this.elementSize.width; - a = a.x - this.elementOffset.left - (this._clickOffset ? this._clickOffset.left : 0) - } else { - b = this.elementSize.height; - a = a.y - this.elementOffset.top - (this._clickOffset ? this._clickOffset.top : 0) - } - b = a / b; - if (b > 1)b = 1; - if (b < 0)b = 0; - if (this.orientation === "vertical")b = 1 - b; - a = this._valueMax() - this._valueMin(); - return this._trimAlignValue(this._valueMin() + b * a) - }, _start: function (a, b) { - var c = {handle: this.handles[b], value: this.value()}; - if (this.options.values && this.options.values.length) { - c.value = this.values(b); - c.values = this.values() - } - return this._trigger("start", a, c) - }, _slide: function (a, b, c) { - var e; - if (this.options.values && this.options.values.length) { - e = this.values(b ? 0 : 1); - if (this.options.values.length === 2 && this.options.range === true && (b === 0 && c > e || b === 1 && c < e))c = e; - if (c !== this.values(b)) { - e = this.values(); - e[b] = c; - a = this._trigger("slide", a, {handle: this.handles[b], value: c, values: e}); - this.values(b ? 0 : 1); - a !== false && this.values(b, c, true) - } - } else if (c !== this.value()) { - a = this._trigger("slide", a, {handle: this.handles[b], value: c}); - a !== false && this.value(c) - } - }, _stop: function (a, b) { - var c = {handle: this.handles[b], value: this.value()}; - if (this.options.values && this.options.values.length) { - c.value = this.values(b); - c.values = this.values() - } - this._trigger("stop", a, c) - }, _change: function (a, b) { - if (!this._keySliding && !this._mouseSliding) { - var c = {handle: this.handles[b], value: this.value()}; - if (this.options.values && this.options.values.length) { - c.value = this.values(b); - c.values = this.values() - } - this._trigger("change", a, c) - } - }, value: function (a) { - if (arguments.length) { - this.options.value = - this._trimAlignValue(a); - this._refreshValue(); - this._change(null, 0) - } - return this._value() - }, values: function (a, b) { - var c, e, f; - if (arguments.length > 1) { - this.options.values[a] = this._trimAlignValue(b); - this._refreshValue(); - this._change(null, a) - } - if (arguments.length)if (d.isArray(arguments[0])) { - c = this.options.values; - e = arguments[0]; - for (f = 0; f < c.length; f += 1) { - c[f] = this._trimAlignValue(e[f]); - this._change(null, f) - } - this._refreshValue() - } else return this.options.values && this.options.values.length ? this._values(a) : this.value(); - else return this._values() - }, _setOption: function (a, b) { - var c, e = 0; - if (d.isArray(this.options.values))e = this.options.values.length; - d.Widget.prototype._setOption.apply(this, arguments); - switch (a) { - case "disabled": - if (b) { - this.handles.filter(".ui-state-focus").blur(); - this.handles.removeClass("ui-state-hover"); - this.handles.attr("disabled", "disabled"); - this.element.addClass("ui-disabled") - } else { - this.handles.removeAttr("disabled"); - this.element.removeClass("ui-disabled") - } - break; - case "orientation": - this._detectOrientation(); - this.element.removeClass("ui-slider-horizontal ui-slider-vertical").addClass("ui-slider-" + this.orientation); - this._refreshValue(); - break; - case "value": - this._animateOff = true; - this._refreshValue(); - this._change(null, 0); - this._animateOff = false; - break; - case "values": - this._animateOff = true; - this._refreshValue(); - for (c = 0; c < e; c += 1)this._change(null, c); - this._animateOff = false; - break - } - }, _value: function () { - var a = this.options.value; - return a = this._trimAlignValue(a) - }, _values: function (a) { - var b, c; - if (arguments.length) { - b = this.options.values[a]; - return b = this._trimAlignValue(b) - } else { - b = this.options.values.slice(); - for (c = 0; c < b.length; c += 1)b[c] = this._trimAlignValue(b[c]); - return b - } - }, _trimAlignValue: function (a) { - if (a < this._valueMin())return this._valueMin(); - if (a > this._valueMax())return this._valueMax(); - var b = this.options.step > 0 ? this.options.step : 1, c = a % b; - a = a - c; - if (Math.abs(c) * 2 >= b)a += c > 0 ? b : -b; - return parseFloat(a.toFixed(5)) - }, _valueMin: function () { - return this.options.min - }, _valueMax: function () { - return this.options.max - }, _refreshValue: function () { - var a = - this.options.range, b = this.options, c = this, e = !this._animateOff ? b.animate : false, f, h = {}, g, i, j, l; - if (this.options.values && this.options.values.length)this.handles.each(function (k) { - f = (c.values(k) - c._valueMin()) / (c._valueMax() - c._valueMin()) * 100; - h[c.orientation === "horizontal" ? "left" : "bottom"] = f + "%"; - d(this).stop(1, 1)[e ? "animate" : "css"](h, b.animate); - if (c.options.range === true)if (c.orientation === "horizontal") { - if (k === 0)c.range.stop(1, 1)[e ? "animate" : "css"]({left: f + "%"}, b.animate); - if (k === 1)c.range[e ? "animate" : "css"]({width: f - - g + "%"}, {queue: false, duration: b.animate}) - } else { - if (k === 0)c.range.stop(1, 1)[e ? "animate" : "css"]({bottom: f + "%"}, b.animate); - if (k === 1)c.range[e ? "animate" : "css"]({height: f - g + "%"}, {queue: false, duration: b.animate}) - } - g = f - }); else { - i = this.value(); - j = this._valueMin(); - l = this._valueMax(); - f = l !== j ? (i - j) / (l - j) * 100 : 0; - h[c.orientation === "horizontal" ? "left" : "bottom"] = f + "%"; - this.handle.stop(1, 1)[e ? "animate" : "css"](h, b.animate); - if (a === "min" && this.orientation === "horizontal")this.range.stop(1, 1)[e ? "animate" : "css"]({width: f + "%"}, - b.animate); - if (a === "max" && this.orientation === "horizontal")this.range[e ? "animate" : "css"]({width: 100 - f + "%"}, {queue: false, duration: b.animate}); - if (a === "min" && this.orientation === "vertical")this.range.stop(1, 1)[e ? "animate" : "css"]({height: f + "%"}, b.animate); - if (a === "max" && this.orientation === "vertical")this.range[e ? "animate" : "css"]({height: 100 - f + "%"}, {queue: false, duration: b.animate}) - } - }}); - d.extend(d.ui.slider, {version: "1.8.5"}) -})(jQuery); -; -/* - * jQuery UI Datepicker 1.8.5 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Datepicker - * - * Depends: - * jquery.ui.core.js - */ -(function (d, G) { - function L() { - this.debug = false; - this._curInst = null; - this._keyEvent = false; - this._disabledInputs = []; - this._inDialog = this._datepickerShowing = false; - this._mainDivId = "ui-datepicker-div"; - this._inlineClass = "ui-datepicker-inline"; - this._appendClass = "ui-datepicker-append"; - this._triggerClass = "ui-datepicker-trigger"; - this._dialogClass = "ui-datepicker-dialog"; - this._disableClass = "ui-datepicker-disabled"; - this._unselectableClass = "ui-datepicker-unselectable"; - this._currentClass = "ui-datepicker-current-day"; - this._dayOverClass = - "ui-datepicker-days-cell-over"; - this.regional = []; - this.regional[""] = {closeText: "Done", prevText: "Prev", nextText: "Next", currentText: "Today", monthNames: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], monthNamesShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], dayNames: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], dayNamesShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], dayNamesMin: ["Su", - "Mo", "Tu", "We", "Th", "Fr", "Sa"], weekHeader: "Wk", dateFormat: "mm/dd/yy", firstDay: 0, isRTL: false, showMonthAfterYear: false, yearSuffix: ""}; - this._defaults = {showOn: "focus", showAnim: "fadeIn", showOptions: {}, defaultDate: null, appendText: "", buttonText: "...", buttonImage: "", buttonImageOnly: false, hideIfNoPrevNext: false, navigationAsDateFormat: false, gotoCurrent: false, changeMonth: false, changeYear: false, yearRange: "c-10:c+10", showOtherMonths: false, selectOtherMonths: false, showWeek: false, calculateWeek: this.iso8601Week, shortYearCutoff: "+10", - minDate: null, maxDate: null, duration: "fast", beforeShowDay: null, beforeShow: null, onSelect: null, onChangeMonthYear: null, onClose: null, numberOfMonths: 1, showCurrentAtPos: 0, stepMonths: 1, stepBigMonths: 12, altField: "", altFormat: "", constrainInput: true, showButtonPanel: false, autoSize: false}; - d.extend(this._defaults, this.regional[""]); - this.dpDiv = d('
') - } - - function E(a, b) { - d.extend(a, - b); - for (var c in b)if (b[c] == null || b[c] == G)a[c] = b[c]; - return a - } - - d.extend(d.ui, {datepicker: {version: "1.8.5"}}); - var y = (new Date).getTime(); - d.extend(L.prototype, {markerClassName: "hasDatepicker", log: function () { - this.debug && console.log.apply("", arguments) - }, _widgetDatepicker: function () { - return this.dpDiv - }, setDefaults: function (a) { - E(this._defaults, a || {}); - return this - }, _attachDatepicker: function (a, b) { - var c = null; - for (var e in this._defaults) { - var f = a.getAttribute("date:" + e); - if (f) { - c = c || {}; - try { - c[e] = eval(f) - } catch (h) { - c[e] = - f - } - } - } - e = a.nodeName.toLowerCase(); - f = e == "div" || e == "span"; - if (!a.id) { - this.uuid += 1; - a.id = "dp" + this.uuid - } - var i = this._newInst(d(a), f); - i.settings = d.extend({}, b || {}, c || {}); - if (e == "input")this._connectDatepicker(a, i); else f && this._inlineDatepicker(a, i) - }, _newInst: function (a, b) { - return{id: a[0].id.replace(/([^A-Za-z0-9_])/g, "\\\\$1"), input: a, selectedDay: 0, selectedMonth: 0, selectedYear: 0, drawMonth: 0, drawYear: 0, inline: b, dpDiv: !b ? this.dpDiv : d('
')} - }, - _connectDatepicker: function (a, b) { - var c = d(a); - b.append = d([]); - b.trigger = d([]); - if (!c.hasClass(this.markerClassName)) { - this._attachments(c, b); - c.addClass(this.markerClassName).keydown(this._doKeyDown).keypress(this._doKeyPress).keyup(this._doKeyUp).bind("setData.datepicker",function (e, f, h) { - b.settings[f] = h - }).bind("getData.datepicker", function (e, f) { - return this._get(b, f) - }); - this._autoSize(b); - d.data(a, "datepicker", b) - } - }, _attachments: function (a, b) { - var c = this._get(b, "appendText"), e = this._get(b, "isRTL"); - b.append && - b.append.remove(); - if (c) { - b.append = d('' + c + ""); - a[e ? "before" : "after"](b.append) - } - a.unbind("focus", this._showDatepicker); - b.trigger && b.trigger.remove(); - c = this._get(b, "showOn"); - if (c == "focus" || c == "both")a.focus(this._showDatepicker); - if (c == "button" || c == "both") { - c = this._get(b, "buttonText"); - var f = this._get(b, "buttonImage"); - b.trigger = d(this._get(b, "buttonImageOnly") ? d("").addClass(this._triggerClass).attr({src: f, alt: c, title: c}) : d('').addClass(this._triggerClass).html(f == - "" ? c : d("").attr({src: f, alt: c, title: c}))); - a[e ? "before" : "after"](b.trigger); - b.trigger.click(function () { - d.datepicker._datepickerShowing && d.datepicker._lastInput == a[0] ? d.datepicker._hideDatepicker() : d.datepicker._showDatepicker(a[0]); - return false - }) - } - }, _autoSize: function (a) { - if (this._get(a, "autoSize") && !a.inline) { - var b = new Date(2009, 11, 20), c = this._get(a, "dateFormat"); - if (c.match(/[DM]/)) { - var e = function (f) { - for (var h = 0, i = 0, g = 0; g < f.length; g++)if (f[g].length > h) { - h = f[g].length; - i = g - } - return i - }; - b.setMonth(e(this._get(a, - c.match(/MM/) ? "monthNames" : "monthNamesShort"))); - b.setDate(e(this._get(a, c.match(/DD/) ? "dayNames" : "dayNamesShort")) + 20 - b.getDay()) - } - a.input.attr("size", this._formatDate(a, b).length) - } - }, _inlineDatepicker: function (a, b) { - var c = d(a); - if (!c.hasClass(this.markerClassName)) { - c.addClass(this.markerClassName).append(b.dpDiv).bind("setData.datepicker",function (e, f, h) { - b.settings[f] = h - }).bind("getData.datepicker", function (e, f) { - return this._get(b, f) - }); - d.data(a, "datepicker", b); - this._setDate(b, this._getDefaultDate(b), - true); - this._updateDatepicker(b); - this._updateAlternate(b) - } - }, _dialogDatepicker: function (a, b, c, e, f) { - a = this._dialogInst; - if (!a) { - this.uuid += 1; - this._dialogInput = d(''); - this._dialogInput.keydown(this._doKeyDown); - d("body").append(this._dialogInput); - a = this._dialogInst = this._newInst(this._dialogInput, false); - a.settings = {}; - d.data(this._dialogInput[0], "datepicker", a) - } - E(a.settings, e || {}); - b = b && b.constructor == - Date ? this._formatDate(a, b) : b; - this._dialogInput.val(b); - this._pos = f ? f.length ? f : [f.pageX, f.pageY] : null; - if (!this._pos)this._pos = [document.documentElement.clientWidth / 2 - 100 + (document.documentElement.scrollLeft || document.body.scrollLeft), document.documentElement.clientHeight / 2 - 150 + (document.documentElement.scrollTop || document.body.scrollTop)]; - this._dialogInput.css("left", this._pos[0] + 20 + "px").css("top", this._pos[1] + "px"); - a.settings.onSelect = c; - this._inDialog = true; - this.dpDiv.addClass(this._dialogClass); - this._showDatepicker(this._dialogInput[0]); - d.blockUI && d.blockUI(this.dpDiv); - d.data(this._dialogInput[0], "datepicker", a); - return this - }, _destroyDatepicker: function (a) { - var b = d(a), c = d.data(a, "datepicker"); - if (b.hasClass(this.markerClassName)) { - var e = a.nodeName.toLowerCase(); - d.removeData(a, "datepicker"); - if (e == "input") { - c.append.remove(); - c.trigger.remove(); - b.removeClass(this.markerClassName).unbind("focus", this._showDatepicker).unbind("keydown", this._doKeyDown).unbind("keypress", this._doKeyPress).unbind("keyup", this._doKeyUp) - } else if (e == "div" || e == "span")b.removeClass(this.markerClassName).empty() - } - }, - _enableDatepicker: function (a) { - var b = d(a), c = d.data(a, "datepicker"); - if (b.hasClass(this.markerClassName)) { - var e = a.nodeName.toLowerCase(); - if (e == "input") { - a.disabled = false; - c.trigger.filter("button").each(function () { - this.disabled = false - }).end().filter("img").css({opacity: "1.0", cursor: ""}) - } else if (e == "div" || e == "span")b.children("." + this._inlineClass).children().removeClass("ui-state-disabled"); - this._disabledInputs = d.map(this._disabledInputs, function (f) { - return f == a ? null : f - }) - } - }, _disableDatepicker: function (a) { - var b = - d(a), c = d.data(a, "datepicker"); - if (b.hasClass(this.markerClassName)) { - var e = a.nodeName.toLowerCase(); - if (e == "input") { - a.disabled = true; - c.trigger.filter("button").each(function () { - this.disabled = true - }).end().filter("img").css({opacity: "0.5", cursor: "default"}) - } else if (e == "div" || e == "span")b.children("." + this._inlineClass).children().addClass("ui-state-disabled"); - this._disabledInputs = d.map(this._disabledInputs, function (f) { - return f == a ? null : f - }); - this._disabledInputs[this._disabledInputs.length] = a - } - }, _isDisabledDatepicker: function (a) { - if (!a)return false; - for (var b = 0; b < this._disabledInputs.length; b++)if (this._disabledInputs[b] == a)return true; - return false - }, _getInst: function (a) { - try { - return d.data(a, "datepicker") - } catch (b) { - throw"Missing instance data for this datepicker"; - } - }, _optionDatepicker: function (a, b, c) { - var e = this._getInst(a); - if (arguments.length == 2 && typeof b == "string")return b == "defaults" ? d.extend({}, d.datepicker._defaults) : e ? b == "all" ? d.extend({}, e.settings) : this._get(e, b) : null; - var f = b || {}; - if (typeof b == "string") { - f = {}; - f[b] = c - } - if (e) { - this._curInst == e && - this._hideDatepicker(); - var h = this._getDateDatepicker(a, true); - E(e.settings, f); - this._attachments(d(a), e); - this._autoSize(e); - this._setDateDatepicker(a, h); - this._updateDatepicker(e) - } - }, _changeDatepicker: function (a, b, c) { - this._optionDatepicker(a, b, c) - }, _refreshDatepicker: function (a) { - (a = this._getInst(a)) && this._updateDatepicker(a) - }, _setDateDatepicker: function (a, b) { - if (a = this._getInst(a)) { - this._setDate(a, b); - this._updateDatepicker(a); - this._updateAlternate(a) - } - }, _getDateDatepicker: function (a, b) { - (a = this._getInst(a)) && !a.inline && this._setDateFromField(a, b); - return a ? this._getDate(a) : null - }, _doKeyDown: function (a) { - var b = d.datepicker._getInst(a.target), c = true, e = b.dpDiv.is(".ui-datepicker-rtl"); - b._keyEvent = true; - if (d.datepicker._datepickerShowing)switch (a.keyCode) { - case 9: - d.datepicker._hideDatepicker(); - c = false; - break; - case 13: - c = d("td." + d.datepicker._dayOverClass, b.dpDiv).add(d("td." + d.datepicker._currentClass, b.dpDiv)); - c[0] ? d.datepicker._selectDay(a.target, b.selectedMonth, b.selectedYear, c[0]) : d.datepicker._hideDatepicker(); - return false; - case 27: - d.datepicker._hideDatepicker(); - break; - case 33: - d.datepicker._adjustDate(a.target, a.ctrlKey ? -d.datepicker._get(b, "stepBigMonths") : -d.datepicker._get(b, "stepMonths"), "M"); - break; - case 34: - d.datepicker._adjustDate(a.target, a.ctrlKey ? +d.datepicker._get(b, "stepBigMonths") : +d.datepicker._get(b, "stepMonths"), "M"); - break; - case 35: - if (a.ctrlKey || a.metaKey)d.datepicker._clearDate(a.target); - c = a.ctrlKey || a.metaKey; - break; - case 36: - if (a.ctrlKey || a.metaKey)d.datepicker._gotoToday(a.target); - c = a.ctrlKey || - a.metaKey; - break; - case 37: - if (a.ctrlKey || a.metaKey)d.datepicker._adjustDate(a.target, e ? +1 : -1, "D"); - c = a.ctrlKey || a.metaKey; - if (a.originalEvent.altKey)d.datepicker._adjustDate(a.target, a.ctrlKey ? -d.datepicker._get(b, "stepBigMonths") : -d.datepicker._get(b, "stepMonths"), "M"); - break; - case 38: - if (a.ctrlKey || a.metaKey)d.datepicker._adjustDate(a.target, -7, "D"); - c = a.ctrlKey || a.metaKey; - break; - case 39: - if (a.ctrlKey || a.metaKey)d.datepicker._adjustDate(a.target, e ? -1 : +1, "D"); - c = a.ctrlKey || a.metaKey; - if (a.originalEvent.altKey)d.datepicker._adjustDate(a.target, - a.ctrlKey ? +d.datepicker._get(b, "stepBigMonths") : +d.datepicker._get(b, "stepMonths"), "M"); - break; - case 40: - if (a.ctrlKey || a.metaKey)d.datepicker._adjustDate(a.target, +7, "D"); - c = a.ctrlKey || a.metaKey; - break; - default: - c = false - } else if (a.keyCode == 36 && a.ctrlKey)d.datepicker._showDatepicker(this); else c = false; - if (c) { - a.preventDefault(); - a.stopPropagation() - } - }, _doKeyPress: function (a) { - var b = d.datepicker._getInst(a.target); - if (d.datepicker._get(b, "constrainInput")) { - b = d.datepicker._possibleChars(d.datepicker._get(b, "dateFormat")); - var c = String.fromCharCode(a.charCode == G ? a.keyCode : a.charCode); - return a.ctrlKey || c < " " || !b || b.indexOf(c) > -1 - } - }, _doKeyUp: function (a) { - a = d.datepicker._getInst(a.target); - if (a.input.val() != a.lastVal)try { - if (d.datepicker.parseDate(d.datepicker._get(a, "dateFormat"), a.input ? a.input.val() : null, d.datepicker._getFormatConfig(a))) { - d.datepicker._setDateFromField(a); - d.datepicker._updateAlternate(a); - d.datepicker._updateDatepicker(a) - } - } catch (b) { - d.datepicker.log(b) - } - return true - }, _showDatepicker: function (a) { - a = a.target || - a; - if (a.nodeName.toLowerCase() != "input")a = d("input", a.parentNode)[0]; - if (!(d.datepicker._isDisabledDatepicker(a) || d.datepicker._lastInput == a)) { - var b = d.datepicker._getInst(a); - d.datepicker._curInst && d.datepicker._curInst != b && d.datepicker._curInst.dpDiv.stop(true, true); - var c = d.datepicker._get(b, "beforeShow"); - E(b.settings, c ? c.apply(a, [a, b]) : {}); - b.lastVal = null; - d.datepicker._lastInput = a; - d.datepicker._setDateFromField(b); - if (d.datepicker._inDialog)a.value = ""; - if (!d.datepicker._pos) { - d.datepicker._pos = d.datepicker._findPos(a); - d.datepicker._pos[1] += a.offsetHeight - } - var e = false; - d(a).parents().each(function () { - e |= d(this).css("position") == "fixed"; - return!e - }); - if (e && d.browser.opera) { - d.datepicker._pos[0] -= document.documentElement.scrollLeft; - d.datepicker._pos[1] -= document.documentElement.scrollTop - } - c = {left: d.datepicker._pos[0], top: d.datepicker._pos[1]}; - d.datepicker._pos = null; - b.dpDiv.css({position: "absolute", display: "block", top: "-1000px"}); - d.datepicker._updateDatepicker(b); - c = d.datepicker._checkOffset(b, c, e); - b.dpDiv.css({position: d.datepicker._inDialog && - d.blockUI ? "static" : e ? "fixed" : "absolute", display: "none", left: c.left + "px", top: c.top + "px"}); - if (!b.inline) { - c = d.datepicker._get(b, "showAnim"); - var f = d.datepicker._get(b, "duration"), h = function () { - d.datepicker._datepickerShowing = true; - var i = d.datepicker._getBorders(b.dpDiv); - b.dpDiv.find("iframe.ui-datepicker-cover").css({left: -i[0], top: -i[1], width: b.dpDiv.outerWidth(), height: b.dpDiv.outerHeight()}) - }; - b.dpDiv.zIndex(d(a).zIndex() + 1); - d.effects && d.effects[c] ? b.dpDiv.show(c, d.datepicker._get(b, "showOptions"), f, - h) : b.dpDiv[c || "show"](c ? f : null, h); - if (!c || !f)h(); - b.input.is(":visible") && !b.input.is(":disabled") && b.input.focus(); - d.datepicker._curInst = b - } - } - }, _updateDatepicker: function (a) { - var b = this, c = d.datepicker._getBorders(a.dpDiv); - a.dpDiv.empty().append(this._generateHTML(a)).find("iframe.ui-datepicker-cover").css({left: -c[0], top: -c[1], width: a.dpDiv.outerWidth(), height: a.dpDiv.outerHeight()}).end().find("button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a").bind("mouseout",function () { - d(this).removeClass("ui-state-hover"); - this.className.indexOf("ui-datepicker-prev") != -1 && d(this).removeClass("ui-datepicker-prev-hover"); - this.className.indexOf("ui-datepicker-next") != -1 && d(this).removeClass("ui-datepicker-next-hover") - }).bind("mouseover",function () { - if (!b._isDisabledDatepicker(a.inline ? a.dpDiv.parent()[0] : a.input[0])) { - d(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"); - d(this).addClass("ui-state-hover"); - this.className.indexOf("ui-datepicker-prev") != -1 && d(this).addClass("ui-datepicker-prev-hover"); - this.className.indexOf("ui-datepicker-next") != -1 && d(this).addClass("ui-datepicker-next-hover") - } - }).end().find("." + this._dayOverClass + " a").trigger("mouseover").end(); - c = this._getNumberOfMonths(a); - var e = c[1]; - e > 1 ? a.dpDiv.addClass("ui-datepicker-multi-" + e).css("width", 17 * e + "em") : a.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width(""); - a.dpDiv[(c[0] != 1 || c[1] != 1 ? "add" : "remove") + "Class"]("ui-datepicker-multi"); - a.dpDiv[(this._get(a, "isRTL") ? "add" : "remove") + "Class"]("ui-datepicker-rtl"); - a == d.datepicker._curInst && d.datepicker._datepickerShowing && a.input && a.input.is(":visible") && !a.input.is(":disabled") && a.input.focus() - }, _getBorders: function (a) { - var b = function (c) { - return{thin: 1, medium: 2, thick: 3}[c] || c - }; - return[parseFloat(b(a.css("border-left-width"))), parseFloat(b(a.css("border-top-width")))] - }, _checkOffset: function (a, b, c) { - var e = a.dpDiv.outerWidth(), f = a.dpDiv.outerHeight(), h = a.input ? a.input.outerWidth() : 0, i = a.input ? a.input.outerHeight() : 0, g = document.documentElement.clientWidth + d(document).scrollLeft(), - k = document.documentElement.clientHeight + d(document).scrollTop(); - b.left -= this._get(a, "isRTL") ? e - h : 0; - b.left -= c && b.left == a.input.offset().left ? d(document).scrollLeft() : 0; - b.top -= c && b.top == a.input.offset().top + i ? d(document).scrollTop() : 0; - b.left -= Math.min(b.left, b.left + e > g && g > e ? Math.abs(b.left + e - g) : 0); - b.top -= Math.min(b.top, b.top + f > k && k > f ? Math.abs(f + i) : 0); - return b - }, _findPos: function (a) { - for (var b = this._get(this._getInst(a), "isRTL"); a && (a.type == "hidden" || a.nodeType != 1);)a = a[b ? "previousSibling" : "nextSibling"]; - a = d(a).offset(); - return[a.left, a.top] - }, _hideDatepicker: function (a) { - var b = this._curInst; - if (!(!b || a && b != d.data(a, "datepicker")))if (this._datepickerShowing) { - a = this._get(b, "showAnim"); - var c = this._get(b, "duration"), e = function () { - d.datepicker._tidyDialog(b); - this._curInst = null - }; - d.effects && d.effects[a] ? b.dpDiv.hide(a, d.datepicker._get(b, "showOptions"), c, e) : b.dpDiv[a == "slideDown" ? "slideUp" : a == "fadeIn" ? "fadeOut" : "hide"](a ? c : null, e); - a || e(); - if (a = this._get(b, "onClose"))a.apply(b.input ? b.input[0] : null, [b.input ? b.input.val() : - "", b]); - this._datepickerShowing = false; - this._lastInput = null; - if (this._inDialog) { - this._dialogInput.css({position: "absolute", left: "0", top: "-100px"}); - if (d.blockUI) { - d.unblockUI(); - d("body").append(this.dpDiv) - } - } - this._inDialog = false - } - }, _tidyDialog: function (a) { - a.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar") - }, _checkExternalClick: function (a) { - if (d.datepicker._curInst) { - a = d(a.target); - a[0].id != d.datepicker._mainDivId && a.parents("#" + d.datepicker._mainDivId).length == 0 && !a.hasClass(d.datepicker.markerClassName) && !a.hasClass(d.datepicker._triggerClass) && d.datepicker._datepickerShowing && !(d.datepicker._inDialog && d.blockUI) && d.datepicker._hideDatepicker() - } - }, _adjustDate: function (a, b, c) { - a = d(a); - var e = this._getInst(a[0]); - if (!this._isDisabledDatepicker(a[0])) { - this._adjustInstDate(e, b + (c == "M" ? this._get(e, "showCurrentAtPos") : 0), c); - this._updateDatepicker(e) - } - }, _gotoToday: function (a) { - a = d(a); - var b = this._getInst(a[0]); - if (this._get(b, "gotoCurrent") && b.currentDay) { - b.selectedDay = b.currentDay; - b.drawMonth = b.selectedMonth = b.currentMonth; - b.drawYear = b.selectedYear = b.currentYear - } else { - var c = new Date; - b.selectedDay = c.getDate(); - b.drawMonth = b.selectedMonth = c.getMonth(); - b.drawYear = b.selectedYear = c.getFullYear() - } - this._notifyChange(b); - this._adjustDate(a) - }, _selectMonthYear: function (a, b, c) { - a = d(a); - var e = this._getInst(a[0]); - e._selectingMonthYear = false; - e["selected" + (c == "M" ? "Month" : "Year")] = e["draw" + (c == "M" ? "Month" : "Year")] = parseInt(b.options[b.selectedIndex].value, 10); - this._notifyChange(e); - this._adjustDate(a) - }, _clickMonthYear: function (a) { - var b = - this._getInst(d(a)[0]); - b.input && b._selectingMonthYear && setTimeout(function () { - b.input.focus() - }, 0); - b._selectingMonthYear = !b._selectingMonthYear - }, _selectDay: function (a, b, c, e) { - var f = d(a); - if (!(d(e).hasClass(this._unselectableClass) || this._isDisabledDatepicker(f[0]))) { - f = this._getInst(f[0]); - f.selectedDay = f.currentDay = d("a", e).html(); - f.selectedMonth = f.currentMonth = b; - f.selectedYear = f.currentYear = c; - this._selectDate(a, this._formatDate(f, f.currentDay, f.currentMonth, f.currentYear)) - } - }, _clearDate: function (a) { - a = - d(a); - this._getInst(a[0]); - this._selectDate(a, "") - }, _selectDate: function (a, b) { - a = this._getInst(d(a)[0]); - b = b != null ? b : this._formatDate(a); - a.input && a.input.val(b); - this._updateAlternate(a); - var c = this._get(a, "onSelect"); - if (c)c.apply(a.input ? a.input[0] : null, [b, a]); else a.input && a.input.trigger("change"); - if (a.inline)this._updateDatepicker(a); else { - this._hideDatepicker(); - this._lastInput = a.input[0]; - typeof a.input[0] != "object" && a.input.focus(); - this._lastInput = null - } - }, _updateAlternate: function (a) { - var b = this._get(a, - "altField"); - if (b) { - var c = this._get(a, "altFormat") || this._get(a, "dateFormat"), e = this._getDate(a), f = this.formatDate(c, e, this._getFormatConfig(a)); - d(b).each(function () { - d(this).val(f) - }) - } - }, noWeekends: function (a) { - a = a.getDay(); - return[a > 0 && a < 6, ""] - }, iso8601Week: function (a) { - a = new Date(a.getTime()); - a.setDate(a.getDate() + 4 - (a.getDay() || 7)); - var b = a.getTime(); - a.setMonth(0); - a.setDate(1); - return Math.floor(Math.round((b - a) / 864E5) / 7) + 1 - }, parseDate: function (a, b, c) { - if (a == null || b == null)throw"Invalid arguments"; - b = typeof b == - "object" ? b.toString() : b + ""; - if (b == "")return null; - for (var e = (c ? c.shortYearCutoff : null) || this._defaults.shortYearCutoff, f = (c ? c.dayNamesShort : null) || this._defaults.dayNamesShort, h = (c ? c.dayNames : null) || this._defaults.dayNames, i = (c ? c.monthNamesShort : null) || this._defaults.monthNamesShort, g = (c ? c.monthNames : null) || this._defaults.monthNames, k = c = -1, l = -1, u = -1, j = false, o = function (p) { - (p = z + 1 < a.length && a.charAt(z + 1) == p) && z++; - return p - }, m = function (p) { - o(p); - p = new RegExp("^\\d{1," + (p == "@" ? 14 : p == "!" ? 20 : p == "y" ? 4 : p == "o" ? - 3 : 2) + "}"); - p = b.substring(s).match(p); - if (!p)throw"Missing number at position " + s; - s += p[0].length; - return parseInt(p[0], 10) - }, n = function (p, w, H) { - p = o(p) ? H : w; - for (w = 0; w < p.length; w++)if (b.substr(s, p[w].length).toLowerCase() == p[w].toLowerCase()) { - s += p[w].length; - return w + 1 - } - throw"Unknown name at position " + s; - }, r = function () { - if (b.charAt(s) != a.charAt(z))throw"Unexpected literal at position " + s; - s++ - }, s = 0, z = 0; z < a.length; z++)if (j)if (a.charAt(z) == "'" && !o("'"))j = false; else r(); else switch (a.charAt(z)) { - case "d": - l = m("d"); - break; - case "D": - n("D", f, h); - break; - case "o": - u = m("o"); - break; - case "m": - k = m("m"); - break; - case "M": - k = n("M", i, g); - break; - case "y": - c = m("y"); - break; - case "@": - var v = new Date(m("@")); - c = v.getFullYear(); - k = v.getMonth() + 1; - l = v.getDate(); - break; - case "!": - v = new Date((m("!") - this._ticksTo1970) / 1E4); - c = v.getFullYear(); - k = v.getMonth() + 1; - l = v.getDate(); - break; - case "'": - if (o("'"))r(); else j = true; - break; - default: - r() - } - if (c == -1)c = (new Date).getFullYear(); else if (c < 100)c += (new Date).getFullYear() - (new Date).getFullYear() % 100 + (c <= e ? 0 : -100); - if (u > -1) { - k = 1; - l = u; - do { - e = this._getDaysInMonth(c, k - 1); - if (l <= e)break; - k++; - l -= e - } while (1) - } - v = this._daylightSavingAdjust(new Date(c, k - 1, l)); - if (v.getFullYear() != c || v.getMonth() + 1 != k || v.getDate() != l)throw"Invalid date"; - return v - }, ATOM: "yy-mm-dd", COOKIE: "D, dd M yy", ISO_8601: "yy-mm-dd", RFC_822: "D, d M y", RFC_850: "DD, dd-M-y", RFC_1036: "D, d M y", RFC_1123: "D, d M yy", RFC_2822: "D, d M yy", RSS: "D, d M y", TICKS: "!", TIMESTAMP: "@", W3C: "yy-mm-dd", _ticksTo1970: (718685 + Math.floor(492.5) - Math.floor(19.7) + Math.floor(4.925)) * 24 * - 60 * 60 * 1E7, formatDate: function (a, b, c) { - if (!b)return""; - var e = (c ? c.dayNamesShort : null) || this._defaults.dayNamesShort, f = (c ? c.dayNames : null) || this._defaults.dayNames, h = (c ? c.monthNamesShort : null) || this._defaults.monthNamesShort; - c = (c ? c.monthNames : null) || this._defaults.monthNames; - var i = function (o) { - (o = j + 1 < a.length && a.charAt(j + 1) == o) && j++; - return o - }, g = function (o, m, n) { - m = "" + m; - if (i(o))for (; m.length < n;)m = "0" + m; - return m - }, k = function (o, m, n, r) { - return i(o) ? r[m] : n[m] - }, l = "", u = false; - if (b)for (var j = 0; j < a.length; j++)if (u)if (a.charAt(j) == - "'" && !i("'"))u = false; else l += a.charAt(j); else switch (a.charAt(j)) { - case "d": - l += g("d", b.getDate(), 2); - break; - case "D": - l += k("D", b.getDay(), e, f); - break; - case "o": - l += g("o", (b.getTime() - (new Date(b.getFullYear(), 0, 0)).getTime()) / 864E5, 3); - break; - case "m": - l += g("m", b.getMonth() + 1, 2); - break; - case "M": - l += k("M", b.getMonth(), h, c); - break; - case "y": - l += i("y") ? b.getFullYear() : (b.getYear() % 100 < 10 ? "0" : "") + b.getYear() % 100; - break; - case "@": - l += b.getTime(); - break; - case "!": - l += b.getTime() * 1E4 + this._ticksTo1970; - break; - case "'": - if (i("'"))l += - "'"; else u = true; - break; - default: - l += a.charAt(j) - } - return l - }, _possibleChars: function (a) { - for (var b = "", c = false, e = function (h) { - (h = f + 1 < a.length && a.charAt(f + 1) == h) && f++; - return h - }, f = 0; f < a.length; f++)if (c)if (a.charAt(f) == "'" && !e("'"))c = false; else b += a.charAt(f); else switch (a.charAt(f)) { - case "d": - case "m": - case "y": - case "@": - b += "0123456789"; - break; - case "D": - case "M": - return null; - case "'": - if (e("'"))b += "'"; else c = true; - break; - default: - b += a.charAt(f) - } - return b - }, _get: function (a, b) { - return a.settings[b] !== G ? a.settings[b] : this._defaults[b] - }, - _setDateFromField: function (a, b) { - if (a.input.val() != a.lastVal) { - var c = this._get(a, "dateFormat"), e = a.lastVal = a.input ? a.input.val() : null, f, h; - f = h = this._getDefaultDate(a); - var i = this._getFormatConfig(a); - try { - f = this.parseDate(c, e, i) || h - } catch (g) { - this.log(g); - e = b ? "" : e - } - a.selectedDay = f.getDate(); - a.drawMonth = a.selectedMonth = f.getMonth(); - a.drawYear = a.selectedYear = f.getFullYear(); - a.currentDay = e ? f.getDate() : 0; - a.currentMonth = e ? f.getMonth() : 0; - a.currentYear = e ? f.getFullYear() : 0; - this._adjustInstDate(a) - } - }, _getDefaultDate: function (a) { - return this._restrictMinMax(a, - this._determineDate(a, this._get(a, "defaultDate"), new Date)) - }, _determineDate: function (a, b, c) { - var e = function (h) { - var i = new Date; - i.setDate(i.getDate() + h); - return i - }, f = function (h) { - try { - return d.datepicker.parseDate(d.datepicker._get(a, "dateFormat"), h, d.datepicker._getFormatConfig(a)) - } catch (i) { - } - var g = (h.toLowerCase().match(/^c/) ? d.datepicker._getDate(a) : null) || new Date, k = g.getFullYear(), l = g.getMonth(); - g = g.getDate(); - for (var u = /([+-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g, j = u.exec(h); j;) { - switch (j[2] || "d") { - case "d": - case "D": - g += - parseInt(j[1], 10); - break; - case "w": - case "W": - g += parseInt(j[1], 10) * 7; - break; - case "m": - case "M": - l += parseInt(j[1], 10); - g = Math.min(g, d.datepicker._getDaysInMonth(k, l)); - break; - case "y": - case "Y": - k += parseInt(j[1], 10); - g = Math.min(g, d.datepicker._getDaysInMonth(k, l)); - break - } - j = u.exec(h) - } - return new Date(k, l, g) - }; - if (b = (b = b == null ? c : typeof b == "string" ? f(b) : typeof b == "number" ? isNaN(b) ? c : e(b) : b) && b.toString() == "Invalid Date" ? c : b) { - b.setHours(0); - b.setMinutes(0); - b.setSeconds(0); - b.setMilliseconds(0) - } - return this._daylightSavingAdjust(b) - }, - _daylightSavingAdjust: function (a) { - if (!a)return null; - a.setHours(a.getHours() > 12 ? a.getHours() + 2 : 0); - return a - }, _setDate: function (a, b, c) { - var e = !b, f = a.selectedMonth, h = a.selectedYear; - b = this._restrictMinMax(a, this._determineDate(a, b, new Date)); - a.selectedDay = a.currentDay = b.getDate(); - a.drawMonth = a.selectedMonth = a.currentMonth = b.getMonth(); - a.drawYear = a.selectedYear = a.currentYear = b.getFullYear(); - if ((f != a.selectedMonth || h != a.selectedYear) && !c)this._notifyChange(a); - this._adjustInstDate(a); - if (a.input)a.input.val(e ? - "" : this._formatDate(a)) - }, _getDate: function (a) { - return!a.currentYear || a.input && a.input.val() == "" ? null : this._daylightSavingAdjust(new Date(a.currentYear, a.currentMonth, a.currentDay)) - }, _generateHTML: function (a) { - var b = new Date; - b = this._daylightSavingAdjust(new Date(b.getFullYear(), b.getMonth(), b.getDate())); - var c = this._get(a, "isRTL"), e = this._get(a, "showButtonPanel"), f = this._get(a, "hideIfNoPrevNext"), h = this._get(a, "navigationAsDateFormat"), i = this._getNumberOfMonths(a), g = this._get(a, "showCurrentAtPos"), k = - this._get(a, "stepMonths"), l = i[0] != 1 || i[1] != 1, u = this._daylightSavingAdjust(!a.currentDay ? new Date(9999, 9, 9) : new Date(a.currentYear, a.currentMonth, a.currentDay)), j = this._getMinMaxDate(a, "min"), o = this._getMinMaxDate(a, "max"); - g = a.drawMonth - g; - var m = a.drawYear; - if (g < 0) { - g += 12; - m-- - } - if (o) { - var n = this._daylightSavingAdjust(new Date(o.getFullYear(), o.getMonth() - i[0] * i[1] + 1, o.getDate())); - for (n = j && n < j ? j : n; this._daylightSavingAdjust(new Date(m, g, 1)) > n;) { - g--; - if (g < 0) { - g = 11; - m-- - } - } - } - a.drawMonth = g; - a.drawYear = m; - n = this._get(a, - "prevText"); - n = !h ? n : this.formatDate(n, this._daylightSavingAdjust(new Date(m, g - k, 1)), this._getFormatConfig(a)); - n = this._canAdjustMonth(a, -1, m, g) ? '' + n + "" : f ? "" : '' + - n + ""; - var r = this._get(a, "nextText"); - r = !h ? r : this.formatDate(r, this._daylightSavingAdjust(new Date(m, g + k, 1)), this._getFormatConfig(a)); - f = this._canAdjustMonth(a, +1, m, g) ? '' + r + "" : f ? "" : '' + r + ""; - k = this._get(a, "currentText"); - r = this._get(a, "gotoCurrent") && a.currentDay ? u : b; - k = !h ? k : this.formatDate(k, r, this._getFormatConfig(a)); - h = !a.inline ? '" : ""; - e = e ? '
' + (c ? h : "") + (this._isInRange(a, r) ? '" : "") + (c ? "" : h) + "
" : ""; - h = parseInt(this._get(a, "firstDay"), 10); - h = isNaN(h) ? 0 : h; - k = this._get(a, "showWeek"); - r = this._get(a, "dayNames"); - this._get(a, "dayNamesShort"); - var s = this._get(a, "dayNamesMin"), z = this._get(a, "monthNames"), v = this._get(a, "monthNamesShort"), p = this._get(a, "beforeShowDay"), w = this._get(a, "showOtherMonths"), H = this._get(a, "selectOtherMonths"); - this._get(a, "calculateWeek"); - for (var M = this._getDefaultDate(a), I = "", C = 0; C < i[0]; C++) { - for (var N = - "", D = 0; D < i[1]; D++) { - var J = this._daylightSavingAdjust(new Date(m, g, a.selectedDay)), t = " ui-corner-all", x = ""; - if (l) { - x += '
' + (/all|left/.test(t) && C == 0 ? c ? - f : n : "") + (/all|right/.test(t) && C == 0 ? c ? n : f : "") + this._generateMonthYearHeader(a, g, m, j, o, C > 0 || D > 0, z, v) + '
'; - var A = k ? '" : ""; - for (t = 0; t < 7; t++) { - var q = (t + h) % 7; - A += "= 5 ? ' class="ui-datepicker-week-end"' : "") + '>' + s[q] + "" - } - x += A + ""; - A = this._getDaysInMonth(m, g); - if (m == a.selectedYear && g == a.selectedMonth)a.selectedDay = Math.min(a.selectedDay, - A); - t = (this._getFirstDayOfMonth(m, g) - h + 7) % 7; - A = l ? 6 : Math.ceil((t + A) / 7); - q = this._daylightSavingAdjust(new Date(m, g, 1 - t)); - for (var O = 0; O < A; O++) { - x += ""; - var P = !k ? "" : '"; - for (t = 0; t < 7; t++) { - var F = p ? p.apply(a.input ? a.input[0] : null, [q]) : [true, ""], B = q.getMonth() != g, K = B && !H || !F[0] || j && q < j || o && q > o; - P += '"; - q.setDate(q.getDate() + 1); - q = this._daylightSavingAdjust(q) - } - x += P + "" - } - g++; - if (g > 11) { - g = 0; - m++ - } - x += "
' + this._get(a, "weekHeader") + "
' + this._get(a, "calculateWeek")(q) + "" + (B && !w ? " " : K ? '' + q.getDate() + - "" : '' + q.getDate() + "") + "
" + (l ? "
" + (i[0] > 0 && D == i[1] - 1 ? '
' : "") : ""); - N += x - } - I += N - } - I += e + (d.browser.msie && parseInt(d.browser.version, 10) < 7 && !a.inline ? '' : - ""); - a._keyEvent = false; - return I - }, _generateMonthYearHeader: function (a, b, c, e, f, h, i, g) { - var k = this._get(a, "changeMonth"), l = this._get(a, "changeYear"), u = this._get(a, "showMonthAfterYear"), j = '
', o = ""; - if (h || !k)o += '' + i[b] + ""; else { - i = e && e.getFullYear() == c; - var m = f && f.getFullYear() == c; - o += '" - } - u || (j += o + (h || !(k && l) ? " " : "")); - if (h || !l)j += '' + c + ""; else { - g = this._get(a, "yearRange").split(":"); - var r = (new Date).getFullYear(); - i = function (s) { - s = s.match(/c[+-].*/) ? c + parseInt(s.substring(1), 10) : s.match(/[+-].*/) ? r + parseInt(s, 10) : parseInt(s, 10); - return isNaN(s) ? r : s - }; - b = i(g[0]); - g = Math.max(b, - i(g[1] || "")); - b = e ? Math.max(b, e.getFullYear()) : b; - g = f ? Math.min(g, f.getFullYear()) : g; - for (j += '" - } - j += this._get(a, "yearSuffix"); - if (u)j += (h || !(k && l) ? " " : "") + o; - j += "
"; - return j - }, _adjustInstDate: function (a, b, c) { - var e = - a.drawYear + (c == "Y" ? b : 0), f = a.drawMonth + (c == "M" ? b : 0); - b = Math.min(a.selectedDay, this._getDaysInMonth(e, f)) + (c == "D" ? b : 0); - e = this._restrictMinMax(a, this._daylightSavingAdjust(new Date(e, f, b))); - a.selectedDay = e.getDate(); - a.drawMonth = a.selectedMonth = e.getMonth(); - a.drawYear = a.selectedYear = e.getFullYear(); - if (c == "M" || c == "Y")this._notifyChange(a) - }, _restrictMinMax: function (a, b) { - var c = this._getMinMaxDate(a, "min"); - a = this._getMinMaxDate(a, "max"); - b = c && b < c ? c : b; - return b = a && b > a ? a : b - }, _notifyChange: function (a) { - var b = this._get(a, - "onChangeMonthYear"); - if (b)b.apply(a.input ? a.input[0] : null, [a.selectedYear, a.selectedMonth + 1, a]) - }, _getNumberOfMonths: function (a) { - a = this._get(a, "numberOfMonths"); - return a == null ? [1, 1] : typeof a == "number" ? [1, a] : a - }, _getMinMaxDate: function (a, b) { - return this._determineDate(a, this._get(a, b + "Date"), null) - }, _getDaysInMonth: function (a, b) { - return 32 - (new Date(a, b, 32)).getDate() - }, _getFirstDayOfMonth: function (a, b) { - return(new Date(a, b, 1)).getDay() - }, _canAdjustMonth: function (a, b, c, e) { - var f = this._getNumberOfMonths(a); - c = this._daylightSavingAdjust(new Date(c, e + (b < 0 ? b : f[0] * f[1]), 1)); - b < 0 && c.setDate(this._getDaysInMonth(c.getFullYear(), c.getMonth())); - return this._isInRange(a, c) - }, _isInRange: function (a, b) { - var c = this._getMinMaxDate(a, "min"); - a = this._getMinMaxDate(a, "max"); - return(!c || b.getTime() >= c.getTime()) && (!a || b.getTime() <= a.getTime()) - }, _getFormatConfig: function (a) { - var b = this._get(a, "shortYearCutoff"); - b = typeof b != "string" ? b : (new Date).getFullYear() % 100 + parseInt(b, 10); - return{shortYearCutoff: b, dayNamesShort: this._get(a, - "dayNamesShort"), dayNames: this._get(a, "dayNames"), monthNamesShort: this._get(a, "monthNamesShort"), monthNames: this._get(a, "monthNames")} - }, _formatDate: function (a, b, c, e) { - if (!b) { - a.currentDay = a.selectedDay; - a.currentMonth = a.selectedMonth; - a.currentYear = a.selectedYear - } - b = b ? typeof b == "object" ? b : this._daylightSavingAdjust(new Date(e, c, b)) : this._daylightSavingAdjust(new Date(a.currentYear, a.currentMonth, a.currentDay)); - return this.formatDate(this._get(a, "dateFormat"), b, this._getFormatConfig(a)) - }}); - d.fn.datepicker = - function (a) { - if (!d.datepicker.initialized) { - d(document).mousedown(d.datepicker._checkExternalClick).find("body").append(d.datepicker.dpDiv); - d.datepicker.initialized = true - } - var b = Array.prototype.slice.call(arguments, 1); - if (typeof a == "string" && (a == "isDisabled" || a == "getDate" || a == "widget"))return d.datepicker["_" + a + "Datepicker"].apply(d.datepicker, [this[0]].concat(b)); - if (a == "option" && arguments.length == 2 && typeof arguments[1] == "string")return d.datepicker["_" + a + "Datepicker"].apply(d.datepicker, [this[0]].concat(b)); - return this.each(function () { - typeof a == "string" ? d.datepicker["_" + a + "Datepicker"].apply(d.datepicker, [this].concat(b)) : d.datepicker._attachDatepicker(this, a) - }) - }; - d.datepicker = new L; - d.datepicker.initialized = false; - d.datepicker.uuid = (new Date).getTime(); - d.datepicker.version = "1.8.5"; - window["DP_jQuery_" + y] = d -})(jQuery); -; -/* - * jQuery UI Progressbar 1.8.5 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Progressbar - * - * Depends: - * jquery.ui.core.js - * jquery.ui.widget.js - */ -(function (b, c) { - b.widget("ui.progressbar", {options: {value: 0}, min: 0, max: 100, _create: function () { - this.element.addClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").attr({role: "progressbar", "aria-valuemin": this.min, "aria-valuemax": this.max, "aria-valuenow": this._value()}); - this.valueDiv = b("
").appendTo(this.element); - this._refreshValue() - }, destroy: function () { - this.element.removeClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow"); - this.valueDiv.remove(); - b.Widget.prototype.destroy.apply(this, arguments) - }, value: function (a) { - if (a === c)return this._value(); - this._setOption("value", a); - return this - }, _setOption: function (a, d) { - if (a === "value") { - this.options.value = d; - this._refreshValue(); - this._trigger("change") - } - b.Widget.prototype._setOption.apply(this, arguments) - }, _value: function () { - var a = this.options.value; - if (typeof a !== "number")a = 0; - return Math.min(this.max, Math.max(this.min, a)) - }, _refreshValue: function () { - var a = this.value(); - this.valueDiv.toggleClass("ui-corner-right", - a === this.max).width(a + "%"); - this.element.attr("aria-valuenow", a) - }}); - b.extend(b.ui.progressbar, {version: "1.8.5"}) -})(jQuery); -; \ No newline at end of file diff --git a/wagtail/vendor/django-treebeard/treebeard/static/treebeard/treebeard-admin.css b/wagtail/vendor/django-treebeard/treebeard/static/treebeard/treebeard-admin.css deleted file mode 100644 index dba330b58..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/static/treebeard/treebeard-admin.css +++ /dev/null @@ -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; -}*/ diff --git a/wagtail/vendor/django-treebeard/treebeard/static/treebeard/treebeard-admin.js b/wagtail/vendor/django-treebeard/treebeard/static/treebeard/treebeard-admin.js deleted file mode 100644 index b8daa6b68..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/static/treebeard/treebeard-admin.js +++ /dev/null @@ -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 = $('
'); - $drag_line = $('
'); - $ghost.appendTo($body); - $drag_line.appendTo($body); - - var stop_drag = function () { - $ghost.remove(); - $drag_line.remove(); - $body.enableSelection().unbind('mousemove').unbind('mouseup'); - node.elem.removeAttribute('style'); - }; - - // Create a clone create the illusion that we're moving the node - var node = new Node($(this).closest('tr')[0]); - cloned_node = node.clone(); - node.$elem.css({ - 'background': ACTIVE_NODE_BG_COLOR - }); - - $targetRow = null; - as_child = false; - - // Now make the new clone move with the mouse - $body.disableSelection().bind('mousemove',function (evt2) { - $ghost.html(cloned_node).css({ // from FeinCMS :P - 'opacity': .8, - 'position': 'absolute', - 'top': evt2.pageY, - 'left': evt2.pageX - 30, - 'width': 600 - }); - // Iterate through all rows and see where am I moving so I can place - // the drag line accordingly - rowHeight = node.$elem.height(); - $('tr', node.$elem.parent()).each(function (index, element) { - $row = $(element); - rtop = $row.offset().top; - // The tooltop will display whether I'm droping the element as - // child or sibling - $tooltip = $drag_line.find('span'); - $tooltip.css({ - 'left': node.$elem.width() - $tooltip.width(), - 'height': rowHeight, - }); - node_top = node.$elem.offset().top; - // Check if you are dragging over the same node - if (evt2.pageY >= node_top && evt2.pageY <= node_top + rowHeight) { - $targetRow = null; - $tooltip.text(gettext('Abort')); - $drag_line.css({ - 'top': node_top, - 'height': rowHeight, - 'borderWidth': 0, - 'opacity': 0.8, - 'backgroundColor': ABORT_COLOR - }); - } else - // Check if mouse is over this row - if (evt2.pageY >= rtop && evt2.pageY <= rtop + rowHeight / 2) { - // The mouse is positioned on the top half of a $row - $targetRow = $row; - as_child = false; - $drag_line.css({ - 'left': node.$elem.offset().left, - 'width': node.$elem.width(), - 'top': rtop, - 'borderWidth': '5px', - 'height': 0, - 'opacity': 1 - }); - $tooltip.text(gettext('As Sibling')); - } else if (evt2.pageY >= rtop + rowHeight / 2 && evt2.pageY <= rtop + rowHeight) { - // The mouse is positioned on the bottom half of a row - $targetRow = $row; - target_node = new Node($targetRow[0]); - if (target_node.is_collapsed()) { - target_node.expand(); - } - as_child = true; - $drag_line.css({ - 'top': rtop, - 'left': node.$elem.offset().left, - 'height': rowHeight, - 'opacity': 0.4, - 'width': node.$elem.width(), - 'borderWidth': 0, - 'backgroundColor': DRAG_LINE_COLOR - }); - $tooltip.text(gettext('As child')); - } - }); - }).bind('mouseup',function () { - if ($targetRow !== null) { - target_node = new Node($targetRow[0]); - if (target_node.node_id !== node.node_id) { - /*alert('Insert node ' + node.node_name() + ' as child of: ' - + target_node.parent_node().node_name() + '\n and sibling of: ' - + target_node.node_name());*/ - // Call $.ajax so we can handle the error - // On Drop, make an XHR call to perform the node move - $.ajax({ - url: window.MOVE_NODE_ENDPOINT, - type: 'POST', - data: { - node_id: node.node_id, - parent_id: target_node.parent_id, - sibling_id: target_node.node_id, - as_child: as_child ? 1 : 0 - }, - complete: function (req, status) { - // http://stackoverflow.com/questions/1439895/add-a-hash-with-javascript-to-url-without-scrolling-page/1439910#1439910 - node.$elem.remove(); - window.location.hash = 'node-' + node.node_id; - window.location.reload(); - }, - error: function (req, status, error) { - // On error (!200) also reload to display - // the message - node.$elem.remove(); - window.location.hash = 'node-' + node.node_id; - window.location.reload(); - } - }); - } - } - stop_drag(); - }).bind('keyup', function (kbevt) { - // Cancel drag on escape - if (kbevt.keyCode === 27) { - stop_drag(); - } - }); - }); - - $('a.collapse').click(function () { - var node = new Node($(this).closest('tr')[0]); // send the DOM node, not jQ - node.toggle(); - return false; - }); - var hash = window.location.hash; - // This is a hack, the actual element's id ends in '-id' but the url's hash - // doesn't, I'm doing this to avoid scrolling the page... is that a good thing? - if (hash) { - $(hash + '-id').animate({ - backgroundColor: RECENTLY_MOVED_COLOR - }, RECENTLY_FADE_DURATION, function () { - $(this).animate({ - backgroundColor: RECENTLY_MOVED_FADEOUT - }, RECENTLY_FADE_DURATION, function () { - this.removeAttribute('style'); - }); - }); - } - }); -})(django.jQuery); - -// http://stackoverflow.com/questions/190560/jquery-animate-backgroundcolor/2302005#2302005 -(function (d) { - d.each(["backgroundColor", "borderBottomColor", "borderLeftColor", "borderRightColor", "borderTopColor", "color", "outlineColor"], function (f, e) { - d.fx.step[e] = function (g) { - if (!g.colorInit) { - g.start = c(g.elem, e); - g.end = b(g.end); - g.colorInit = true - } - g.elem.style[e] = "rgb(" + [Math.max(Math.min(parseInt((g.pos * (g.end[0] - g.start[0])) + g.start[0]), 255), 0), Math.max(Math.min(parseInt((g.pos * (g.end[1] - g.start[1])) + g.start[1]), 255), 0), Math.max(Math.min(parseInt((g.pos * (g.end[2] - g.start[2])) + g.start[2]), 255), 0)].join(",") + ")" - } - }); - function b(f) { - var e; - if (f && f.constructor == Array && f.length == 3) { - return f - } - if (e = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(f)) { - return[parseInt(e[1]), parseInt(e[2]), parseInt(e[3])] - } - if (e = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(f)) { - return[parseFloat(e[1]) * 2.55, parseFloat(e[2]) * 2.55, parseFloat(e[3]) * 2.55] - } - if (e = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(f)) { - return[parseInt(e[1], 16), parseInt(e[2], 16), parseInt(e[3], 16)] - } - if (e = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(f)) { - return[parseInt(e[1] + e[1], 16), parseInt(e[2] + e[2], 16), parseInt(e[3] + e[3], 16)] - } - if (e = /rgba\(0, 0, 0, 0\)/.exec(f)) { - return a.transparent - } - return a[d.trim(f).toLowerCase()] - } - - function c(g, e) { - var f; - do { - f = d.css(g, e); - if (f != "" && f != "transparent" || d.nodeName(g, "body")) { - break - } - e = "backgroundColor" - } while (g = g.parentNode); - return b(f) - } - - var a = {aqua: [0, 255, 255], azure: [240, 255, 255], beige: [245, 245, 220], black: [0, 0, 0], blue: [0, 0, 255], brown: [165, 42, 42], cyan: [0, 255, 255], darkblue: [0, 0, 139], darkcyan: [0, 139, 139], darkgrey: [169, 169, 169], darkgreen: [0, 100, 0], darkkhaki: [189, 183, 107], darkmagenta: [139, 0, 139], darkolivegreen: [85, 107, 47], darkorange: [255, 140, 0], darkorchid: [153, 50, 204], darkred: [139, 0, 0], darksalmon: [233, 150, 122], darkviolet: [148, 0, 211], fuchsia: [255, 0, 255], gold: [255, 215, 0], green: [0, 128, 0], indigo: [75, 0, 130], khaki: [240, 230, 140], lightblue: [173, 216, 230], lightcyan: [224, 255, 255], lightgreen: [144, 238, 144], lightgrey: [211, 211, 211], lightpink: [255, 182, 193], lightyellow: [255, 255, 224], lime: [0, 255, 0], magenta: [255, 0, 255], maroon: [128, 0, 0], navy: [0, 0, 128], olive: [128, 128, 0], orange: [255, 165, 0], pink: [255, 192, 203], purple: [128, 0, 128], violet: [128, 0, 128], red: [255, 0, 0], silver: [192, 192, 192], white: [255, 255, 255], yellow: [255, 255, 0], transparent: [255, 255, 255]} -})(django.jQuery); diff --git a/wagtail/vendor/django-treebeard/treebeard/templates/admin/tree_change_list.html b/wagtail/vendor/django-treebeard/treebeard/templates/admin/tree_change_list.html deleted file mode 100644 index d98e819ca..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/templates/admin/tree_change_list.html +++ /dev/null @@ -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 %} diff --git a/wagtail/vendor/django-treebeard/treebeard/templates/admin/tree_change_list_results.html b/wagtail/vendor/django-treebeard/treebeard/templates/admin/tree_change_list_results.html deleted file mode 100644 index 716e6db82..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/templates/admin/tree_change_list_results.html +++ /dev/null @@ -1,38 +0,0 @@ -{% if result_hidden_fields %} -
{# DIV for HTML validation #} - {% for item in result_hidden_fields %}{{ item }}{% endfor %} -
-{% endif %} -{% if results %} - - - - {% for header in result_headers %} - - {% if header.sortable %}{% endif %} - {{ header.text|capfirst }} - {% if header.sortable %}{% endif %}{% endfor %} - - - - {% for node_id, parent_id, node_level, has_children, result in results %} - - {% for item in result %} - {% if forloop.counter == 1 %} - {% for spacer in item.depth %}  - {% endfor %} - {% endif %} - {{ item }} - {% endfor %} - {% endfor %} - -
- - -{% endif %} - diff --git a/wagtail/vendor/django-treebeard/treebeard/templates/admin/tree_list.html b/wagtail/vendor/django-treebeard/treebeard/templates/admin/tree_list.html deleted file mode 100644 index 9a7a27ff2..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/templates/admin/tree_list.html +++ /dev/null @@ -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 %} diff --git a/wagtail/vendor/django-treebeard/treebeard/templates/admin/tree_list_results.html b/wagtail/vendor/django-treebeard/treebeard/templates/admin/tree_list_results.html deleted file mode 100644 index a4fe2c225..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/templates/admin/tree_list_results.html +++ /dev/null @@ -1,7 +0,0 @@ -{% if results %} -
    - {% for result in results %} -
  • {{ result }}
  • - {% endfor %} -
-{% endif %} diff --git a/wagtail/vendor/django-treebeard/treebeard/templatetags/__init__.py b/wagtail/vendor/django-treebeard/treebeard/templatetags/__init__.py deleted file mode 100644 index 217335b91..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/templatetags/__init__.py +++ /dev/null @@ -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)) \ No newline at end of file diff --git a/wagtail/vendor/django-treebeard/treebeard/templatetags/admin_tree.py b/wagtail/vendor/django-treebeard/treebeard/templatetags/admin_tree.py deleted file mode 100644 index 17234788b..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/templatetags/admin_tree.py +++ /dev/null @@ -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(' ') - return result_repr, row_class - - -def get_spacer(first, result): - if first: - spacer = ' ' * ( - result.get_depth() - 1) - else: - spacer = '' - - return spacer - - -def get_collapse(result): - if result.get_children_count(): - collapse = ('' - '-') - else: - collapse = ' ' - - return collapse - - -def get_drag_handler(first): - drag_handler = '' - if first: - drag_handler = ('' - ' ') - return drag_handler - - -def items_for_result(cl, result, form): - """ - Generates the actual list of data. - - @jjdelc: - This has been shamelessly copied from original - django.contrib.admin.templatetags.admin_list.items_for_result - in order to alter the dispay for the first element - """ - first = True - pk = cl.lookup_opts.pk.attname - for field_name in cl.list_display: - result_repr, row_class = get_result_and_row_class(cl, field_name, - result) - # If list_display_links not defined, add the link tag to the - # first field - if (first and not cl.list_display_links) or \ - field_name in cl.list_display_links: - table_tag = {True: 'th', False: 'td'}[first] - # This spacer indents the nodes based on their depth - spacer = get_spacer(first, result) - # This shows a collapse or expand link for nodes with childs - collapse = get_collapse(result) - # Add a before the first col to show the drag handler - drag_handler = get_drag_handler(first) - first = False - url = cl.url_for_result(result) - # Convert the pk to something that can be used in Javascript. - # Problem cases are long ints (23L) and non-ASCII strings. - if cl.to_field: - attr = str(cl.to_field) - else: - attr = pk - value = result.serializable_value(attr) - result_id = repr(force_str(value))[1:] - onclickstr = ( - ' onclick="opener.dismissRelatedLookupPopup(window, %s);' - ' return false;"') - yield mark_safe( - u('%s<%s%s>%s %s %s') % ( - drag_handler, table_tag, row_class, spacer, collapse, url, - (cl.is_popup and onclickstr % result_id or ''), - conditional_escape(result_repr), table_tag)) - else: - # By default the fields come from ModelAdmin.list_editable, but if - # we pull the fields out of the form instead of list_editable - # custom admins can provide fields on a per request basis - if (form and field_name in form.fields and not ( - field_name == cl.model._meta.pk.name and - form[cl.model._meta.pk.name].is_hidden)): - bf = form[field_name] - result_repr = mark_safe(force_str(bf.errors) + force_str(bf)) - yield format_html(u('{1}'), row_class, result_repr) - if form and not form[cl.model._meta.pk.name].is_hidden: - yield format_html(u('{0}'), - force_str(form[cl.model._meta.pk.name])) - - -def get_parent_id(node): - """Return the node's parent id or 0 if node is a root node.""" - if node.is_root(): - return 0 - return node.get_parent().pk - - -def results(cl): - if cl.formset: - for res, form in zip(cl.result_list, cl.formset.forms): - yield (res.pk, get_parent_id(res), res.get_depth(), - res.get_children_count(), - list(items_for_result(cl, res, form))) - else: - for res in cl.result_list: - yield (res.pk, get_parent_id(res), res.get_depth(), - res.get_children_count(), - list(items_for_result(cl, res, None))) - - -def check_empty_dict(GET_dict): - """ - Returns True if the GET querstring contains on values, but it can contain - empty keys. - This is better than doing not bool(request.GET) as an empty key will return - True - """ - empty = True - for k, v in GET_dict.items(): - # Don't disable on p(age) or 'all' GET param - if v and k != 'p' and k != 'all': - empty = False - return empty - - -@register.inclusion_tag( - 'admin/tree_change_list_results.html', takes_context=True) -def result_tree(context, cl, request): - """ - Added 'filtered' param, so the template's js knows whether the results have - been affected by a GET param or not. Only when the results are not filtered - you can drag and sort the tree - """ - - # Here I'm adding an extra col on pos 2 for the drag handlers - headers = list(result_headers(cl)) - headers.insert(1 if needs_checkboxes(context) else 0, { - 'text': '+', - 'sortable': True, - 'url': request.path, - 'tooltip': _('Return to ordered tree'), - 'class_attrib': mark_safe(' class="oder-grabber"') - }) - return { - 'filtered': not check_empty_dict(request.GET), - 'result_hidden_fields': list(result_hidden_fields(cl)), - 'result_headers': headers, - 'results': list(results(cl)), - } - - -def get_static_url(): - """Return a base static url, always ending with a /""" - path = getattr(settings, 'STATIC_URL', None) - if not path: - path = getattr(settings, 'MEDIA_URL', None) - if not path: - path = '/' - return path - - -@register.simple_tag -def treebeard_css(): - """ - Template tag to print out the proper tag to include a custom .css - """ - LINK_HTML = """""" - css_file = urljoin(get_static_url(), 'treebeard/treebeard-admin.css') - return LINK_HTML % css_file - - -@register.simple_tag -def treebeard_js(): - """ - Template tag to print out the proper """ - js_file = '/'.join([path.rstrip('/'), 'treebeard', 'treebeard-admin.js']) - - # Jquery UI is needed to call disableSelection() on drag and drop so - # text selections arent marked while dragging a table row - # http://www.lokkju.com/blog/archives/143 - JQUERY_UI = ("" - "") - jquery_ui = urljoin(path, 'treebeard/jquery-ui-1.8.5.custom.min.js') - - scripts = [SCRIPT_HTML % 'jsi18n', - SCRIPT_HTML % js_file, - JQUERY_UI % jquery_ui] - return ''.join(scripts) diff --git a/wagtail/vendor/django-treebeard/treebeard/templatetags/admin_tree_list.py b/wagtail/vendor/django-treebeard/treebeard/templatetags/admin_tree_list.py deleted file mode 100644 index 9272dd60a..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/templatetags/admin_tree_list.py +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- - -from django.template import Library -from treebeard.templatetags import needs_checkboxes - - -register = Library() -CHECKBOX_TMPL = ('') - - -def _line(context, node, request): - if 't' in request.GET and request.GET['t'] == 'id': - raw_id_fields = """ - onclick="opener.dismissRelatedLookupPopup(window, '%d'); return false;" - """ % (node.pk,) - else: - raw_id_fields = '' - output = '' - if needs_checkboxes(context): - output += CHECKBOX_TMPL % node.pk - return output + '%s' % ( - node.pk, raw_id_fields, str(node)) - - -def _subtree(context, node, request): - tree = '' - for subnode in node.get_children(): - tree += '
  • %s
  • ' % _subtree(context, subnode, request) - if tree: - tree = '
      %s
    ' % tree - return _line(context, node, request) + tree - - -@register.simple_tag(takes_context=True) -def result_tree(context, cl, request): - tree = '' - for root_node in cl.model.get_root_nodes(): - tree += '
  • %s
  • ' % _subtree(context, root_node, request) - return "
      %s
    " % tree diff --git a/wagtail/vendor/django-treebeard/treebeard/tests/admin.py b/wagtail/vendor/django-treebeard/treebeard/tests/admin.py deleted file mode 100644 index d673b9f17..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/tests/admin.py +++ /dev/null @@ -1,21 +0,0 @@ -import datetime - -from django.contrib import admin -from treebeard.admin import admin_factory -from treebeard.forms import movenodeform_factory - -from treebeard.tests.models import BASE_MODELS, UNICODE_MODELS - - -def register(model): - form_class = movenodeform_factory(model) - admin_class = admin_factory(form_class) - admin.site.register(model, admin_class) - - -for model in BASE_MODELS: - register(model) - - -for model in UNICODE_MODELS: - register(model) diff --git a/wagtail/vendor/django-treebeard/treebeard/tests/conftest.py b/wagtail/vendor/django-treebeard/treebeard/tests/conftest.py deleted file mode 100644 index cf79cf5b6..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/tests/conftest.py +++ /dev/null @@ -1,80 +0,0 @@ -import os -import sys -import time - - -os.environ['DJANGO_SETTINGS_MODULE'] = 'treebeard.tests.settings' - -import django -from django.conf import settings -from django.test.utils import (setup_test_environment, - teardown_test_environment) -from django.test.client import Client -from django.core.management import call_command -from django.core import mail -from django.db import connection -from django.db.models.base import ModelBase -from _pytest import python as _pytest_python - - -def idmaker(argnames, argvalues): - idlist = [] - for valindex, valset in enumerate(argvalues): - this_id = [] - for nameindex, val in enumerate(valset): - argname = argnames[nameindex] - if isinstance(val, (float, int, str)): - this_id.append(str(val)) - elif isinstance(val, ModelBase): - this_id.append(val.__name__) - else: - this_id.append("{0}-{1}={2!s}".format(argname, valindex)) - idlist.append("][".join(this_id)) - return idlist -_pytest_python.idmaker = idmaker - - -def pytest_report_header(config): - return 'Django: ' + django.get_version() - - -def pytest_configure(config): - setup_test_environment() - connection.creation.create_test_db(verbosity=2, autoclobber=True) - - -def pytest_unconfigure(config): - dbsettings = settings.DATABASES['default'] - dbtestname = dbsettings['TEST_NAME'] - connection.close() - if dbsettings['ENGINE'].split('.')[-1] == 'postgresql_psycopg2': - connection.connection = None - connection.settings_dict['NAME'] = dbtestname.split('_')[1] - cursor = connection.cursor() - connection.autocommit = True - if django.VERSION < (1, 6): - connection._set_isolation_level(0) - else: - connection._set_autocommit(True) - time.sleep(1) - sys.stdout.write( - "Destroying test database for alias '%s' (%s)...\n" % ( - connection.alias, dbtestname) - ) - sys.stdout.flush() - cursor.execute( - 'DROP DATABASE %s' % connection.ops.quote_name(dbtestname)) - else: - connection.creation.destroy_test_db(dbtestname, verbosity=2) - teardown_test_environment() - - -def pytest_funcarg__client(request): - def setup(): - mail.outbox = [] - return Client() - - def teardown(client): - call_command('flush', verbosity=0, interactive=False) - - return request.cached_setup(setup, teardown, 'function') diff --git a/wagtail/vendor/django-treebeard/treebeard/tests/jenkins/quality.sh b/wagtail/vendor/django-treebeard/treebeard/tests/jenkins/quality.sh deleted file mode 100644 index db98d4a65..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/tests/jenkins/quality.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/sh - -# Script that takes source code quality measurements. -# In shell because it will run in a *NIX node. - -pip install pylint pep8 pytest coverage -coverage erase - -# Combining the coverage data of all the test runs -# in the different OS/Python combinations. -find $WORKSPACE -mindepth 1 -maxdepth 2 -name '.coverage.*' -exec cp -v \{\} . \; -coverage combine -coverage report -coverage xml -coverage erase - -SRCFILES=treebeard/*.py -PYFILES=`find . -name '*.py'` -ALLFILES=`find . -type f` -sloccount --duplicates --wide --details $ALLFILES > sloccount.sc 2>&1 -pep8 $PYFILES > violations-pep8.txt 2>&1 -pylint -f parseable $SRCFILES > violations-pylint.txt 2>&1 diff --git a/wagtail/vendor/django-treebeard/treebeard/tests/jenkins/rm_workspace_coverage.py b/wagtail/vendor/django-treebeard/treebeard/tests/jenkins/rm_workspace_coverage.py deleted file mode 100644 index d56bb050d..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/tests/jenkins/rm_workspace_coverage.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Remove `.coverage.$HOST.$ID` files from previous runs. - -In Python because of portability with Windows. -""" - -import sys - -import os - - -def main(): - workspace = os.environ['WORKSPACE'] - for filename in os.listdir(workspace): - if filename.startswith('.coverage.'): - file_full_name = os.path.join(workspace, filename) - sys.stdout.write( - '* Removing old .coverage file: `%s`\n' % file_full_name) - os.unlink(file_full_name) - sys.stdout.flush() - -if __name__ == '__main__': - main() diff --git a/wagtail/vendor/django-treebeard/treebeard/tests/jenkins/toxhelper.py b/wagtail/vendor/django-treebeard/treebeard/tests/jenkins/toxhelper.py deleted file mode 100644 index b0a170449..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/tests/jenkins/toxhelper.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python -""" toxhelper is a simple wrapper of pytest and coverage to be used with tox. - -It is specially useful to avoid path and interpreter problems while running -tests with jenkins in OS X, Linux and Windows using the same configuration. - -See https://tabo.pe/jenkins/ for the results. -""" - -import sys - -import os -import pytest - -from coverage import coverage - - -def run_the_tests(): - if 'TOX_DB' in os.environ: - os.environ['DATABASE_HOST'], os.environ['DATABASE_PORT'] = { - 'pgsql': ('dummy_test_database_server', '5434'), - 'mysql': ('dummy_test_database_server', '3308'), - 'sqlite': ('', ''), - }[os.environ['TOX_DB']] - cov = coverage() - cov.start() - test_result = pytest.main(sys.argv[1:]) - cov.stop() - cov.save() - return test_result - -if __name__ == '__main__': - sys.exit(run_the_tests()) diff --git a/wagtail/vendor/django-treebeard/treebeard/tests/models.py b/wagtail/vendor/django-treebeard/treebeard/tests/models.py deleted file mode 100644 index 043fe1c4f..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/tests/models.py +++ /dev/null @@ -1,246 +0,0 @@ -from django.db import models, connection -from django.contrib.auth.models import User - -from treebeard.mp_tree import MP_Node -from treebeard.al_tree import AL_Node -from treebeard.ns_tree import NS_Node - - -class RelatedModel(models.Model): - desc = models.CharField(max_length=255) - - def __str__(self): - return self.desc - - -class MP_TestNode(MP_Node): - steplen = 3 - - desc = models.CharField(max_length=255) - - def __str__(self): # pragma: no cover - return 'Node %d' % self.pk - - -class MP_UnicodeNode(MP_Node): - steplen = 3 - - desc = models.CharField(max_length=255) - - def __str__(self): # pragma: no cover - return self.desc - - -class MP_TestNodeSomeDep(models.Model): - node = models.ForeignKey(MP_TestNode) - - def __str__(self): # pragma: no cover - return 'Node %d' % self.pk - - -class MP_TestNodeRelated(MP_Node): - steplen = 3 - - desc = models.CharField(max_length=255) - related = models.ForeignKey(RelatedModel) - - def __str__(self): # pragma: no cover - return 'Node %d' % self.pk - - -class NS_TestNode(NS_Node): - desc = models.CharField(max_length=255) - - def __str__(self): # pragma: no cover - return 'Node %d' % self.pk - - -class NS_UnicodetNode(NS_Node): - desc = models.CharField(max_length=255) - - def __str__(self): # pragma: no cover - return self.desc - - -class NS_TestNodeSomeDep(models.Model): - node = models.ForeignKey(NS_TestNode) - - def __str__(self): # pragma: no cover - return 'Node %d' % self.pk - - -class NS_TestNodeRelated(NS_Node): - desc = models.CharField(max_length=255) - related = models.ForeignKey(RelatedModel) - - def __str__(self): # pragma: no cover - return 'Node %d' % self.pk - - -class AL_TestNode(AL_Node): - parent = models.ForeignKey('self', - related_name='children_set', - null=True, - db_index=True) - sib_order = models.PositiveIntegerField() - desc = models.CharField(max_length=255) - - def __str__(self): # pragma: no cover - return 'Node %d' % self.pk - - -class AL_UnicodeNode(AL_Node): - parent = models.ForeignKey('self', - related_name='children_set', - null=True, - db_index=True) - sib_order = models.PositiveIntegerField() - desc = models.CharField(max_length=255) - - def __str__(self): # pragma: no cover - return self.desc - - -class AL_TestNodeSomeDep(models.Model): - node = models.ForeignKey(AL_TestNode) - - def __str__(self): # pragma: no cover - return 'Node %d' % self.pk - - -class AL_TestNodeRelated(AL_Node): - parent = models.ForeignKey('self', - related_name='children_set', - null=True, - db_index=True) - sib_order = models.PositiveIntegerField() - desc = models.CharField(max_length=255) - related = models.ForeignKey(RelatedModel) - - def __str__(self): # pragma: no cover - return 'Node %d' % self.pk - - -class MP_TestNodeSorted(MP_Node): - steplen = 1 - node_order_by = ['val1', 'val2', 'desc'] - val1 = models.IntegerField() - val2 = models.IntegerField() - desc = models.CharField(max_length=255) - - def __str__(self): # pragma: no cover - return 'Node %d' % self.pk - - -class NS_TestNodeSorted(NS_Node): - node_order_by = ['val1', 'val2', 'desc'] - val1 = models.IntegerField() - val2 = models.IntegerField() - desc = models.CharField(max_length=255) - - def __str__(self): # pragma: no cover - return 'Node %d' % self.pk - - -class AL_TestNodeSorted(AL_Node): - parent = models.ForeignKey('self', - related_name='children_set', - null=True, - db_index=True) - node_order_by = ['val1', 'val2', 'desc'] - val1 = models.IntegerField() - val2 = models.IntegerField() - desc = models.CharField(max_length=255) - - def __str__(self): # pragma: no cover - return 'Node %d' % self.pk - - -class MP_TestNodeAlphabet(MP_Node): - steplen = 2 - - numval = models.IntegerField() - - def __str__(self): # pragma: no cover - return 'Node %d' % self.pk - - -class MP_TestNodeSmallStep(MP_Node): - steplen = 1 - alphabet = '0123456789' - - def __str__(self): # pragma: no cover - return 'Node %d' % self.pk - - -class MP_TestNodeSortedAutoNow(MP_Node): - desc = models.CharField(max_length=255) - created = models.DateTimeField(auto_now_add=True) - - node_order_by = ['created'] - - def __str__(self): # pragma: no cover - return 'Node %d' % self.pk - - -class MP_TestNodeShortPath(MP_Node): - steplen = 1 - alphabet = '01234' - desc = models.CharField(max_length=255) - - def __str__(self): # pragma: no cover - return 'Node %d' % self.pk - - -# This is how you change the default fields defined in a Django abstract class -# (in this case, MP_Node), since Django doesn't allow overriding fields, only -# mehods and attributes -MP_TestNodeShortPath._meta.get_field('path').max_length = 4 - - -class MP_TestNode_Proxy(MP_TestNode): - class Meta: - proxy = True - - -class NS_TestNode_Proxy(NS_TestNode): - class Meta: - proxy = True - - -class AL_TestNode_Proxy(AL_TestNode): - class Meta: - proxy = True - - -class MP_TestSortedNodeShortPath(MP_Node): - steplen = 1 - alphabet = '01234' - desc = models.CharField(max_length=255) - - node_order_by = ['desc'] - - def __str__(self): # pragma: no cover - return 'Node %d' % self.pk - - -MP_TestSortedNodeShortPath._meta.get_field('path').max_length = 4 - - -class MP_TestManyToManyWithUser(MP_Node): - name = models.CharField(max_length=255) - users = models.ManyToManyField(User) - - -BASE_MODELS = AL_TestNode, MP_TestNode, NS_TestNode -PROXY_MODELS = AL_TestNode_Proxy, MP_TestNode_Proxy, NS_TestNode_Proxy -SORTED_MODELS = AL_TestNodeSorted, MP_TestNodeSorted, NS_TestNodeSorted -DEP_MODELS = AL_TestNodeSomeDep, MP_TestNodeSomeDep, NS_TestNodeSomeDep -MP_SHORTPATH_MODELS = MP_TestNodeShortPath, MP_TestSortedNodeShortPath -RELATED_MODELS = AL_TestNodeRelated, MP_TestNodeRelated, NS_TestNodeRelated -UNICODE_MODELS = AL_UnicodeNode, MP_UnicodeNode, NS_UnicodetNode - - -def empty_models_tables(models): - for model in models: - model.objects.all().delete() diff --git a/wagtail/vendor/django-treebeard/treebeard/tests/settings.py b/wagtail/vendor/django-treebeard/treebeard/tests/settings.py deleted file mode 100644 index 5b941136b..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/tests/settings.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Django settings for testing treebeard""" - -import random -import string - -import os - - -def get_db_conf(): - conf, options = {}, {} - for name in ('ENGINE', 'NAME', 'USER', 'PASSWORD', 'HOST', 'PORT'): - conf[name] = os.environ.get('DATABASE_' + name, '') - engine = conf['ENGINE'] - if engine == '': - engine = 'sqlite3' - elif engine in ('pgsql', 'postgres', 'postgresql', 'psycopg2'): - engine = 'postgresql_psycopg2' - if '.' not in engine: - engine = 'django.db.backends.' + engine - conf['ENGINE'] = engine - - if engine == 'django.db.backends.sqlite3': - conf['TEST_NAME'] = conf['NAME'] = ':memory:' - elif engine in ('django.db.backends.mysql', - 'django.db.backends.postgresql_psycopg2'): - if not conf['NAME']: - conf['NAME'] = 'treebeard' - - # randomizing the test db name, - # so we can safely run multiple - # tests at the same time - conf['TEST_NAME'] = "test_%s_%s" % ( - conf['NAME'], - ''.join(random.choice(string.ascii_letters) for _ in range(15)) - ) - - if conf['USER'] == '': - conf['USER'] = { - 'django.db.backends.mysql': 'root', - 'django.db.backends.postgresql_psycopg2': 'postgres' - }[engine] - if engine == 'django.db.backends.mysql': - conf['OPTIONS'] = { - 'init_command': 'SET storage_engine=INNODB,' - 'character_set_connection=utf8,' - 'collation_connection=utf8_unicode_ci'} - return conf - -DATABASES = {'default': get_db_conf()} -SECRET_KEY = '7r33b34rd' - -INSTALLED_APPS = [ - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.admin', - 'django.contrib.messages', - 'treebeard', - 'treebeard.tests'] - -ROOT_URLCONF = 'treebeard.tests.urls' \ No newline at end of file diff --git a/wagtail/vendor/django-treebeard/treebeard/tests/test_treebeard.py b/wagtail/vendor/django-treebeard/treebeard/tests/test_treebeard.py deleted file mode 100644 index 33ab829e2..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/tests/test_treebeard.py +++ /dev/null @@ -1,2423 +0,0 @@ -# -*- coding: utf-8 -*- -"""Unit/Functional tests""" - -from __future__ import with_statement, unicode_literals -import datetime -import os -import sys - -from django.contrib.admin.sites import AdminSite -from django.contrib.admin.views.main import ChangeList -from django.contrib.auth.models import User -from django.contrib.messages.storage.fallback import FallbackStorage -from django.db.models import Q -from django.template import Template, Context -from django.test import TestCase -from django.test.client import RequestFactory -import pytest - -from treebeard import numconv -from treebeard.admin import admin_factory -from treebeard.exceptions import InvalidPosition, InvalidMoveToDescendant,\ - PathOverflow, MissingNodeOrderBy -from treebeard.forms import movenodeform_factory -from treebeard.templatetags.admin_tree import get_static_url -from treebeard.tests import models - - -BASE_DATA = [ - {'data': {'desc': '1'}}, - {'data': {'desc': '2'}, 'children': [ - {'data': {'desc': '21'}}, - {'data': {'desc': '22'}}, - {'data': {'desc': '23'}, 'children': [ - {'data': {'desc': '231'}}, - ]}, - {'data': {'desc': '24'}}, - ]}, - {'data': {'desc': '3'}}, - {'data': {'desc': '4'}, 'children': [ - {'data': {'desc': '41'}}, - ]}] -UNCHANGED = [ - ('1', 1, 0), - ('2', 1, 4), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 1), - ('231', 3, 0), - ('24', 2, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - - -def _prepare_db_test(request): - case = TestCase(methodName='__init__') - case._pre_setup() - request.addfinalizer(case._post_teardown) - return request.param - - -@pytest.fixture(scope='function', - params=models.BASE_MODELS + models.PROXY_MODELS) -def model(request): - return _prepare_db_test(request) - - -@pytest.fixture(scope='function', params=models.BASE_MODELS) -def model_without_proxy(request): - return _prepare_db_test(request) - - -@pytest.fixture(scope='function', params=models.UNICODE_MODELS) -def model_with_unicode(request): - return _prepare_db_test(request) - - -@pytest.fixture(scope='function', params=models.SORTED_MODELS) -def sorted_model(request): - return _prepare_db_test(request) - - -@pytest.fixture(scope='function', params=models.RELATED_MODELS) -def related_model(request): - return _prepare_db_test(request) - - -@pytest.fixture(scope='function', params=models.MP_SHORTPATH_MODELS) -def mpshort_model(request): - return _prepare_db_test(request) - - -@pytest.fixture(scope='function', params=[models.MP_TestNodeShortPath]) -def mpshortnotsorted_model(request): - return _prepare_db_test(request) - - -@pytest.fixture(scope='function', params=[models.MP_TestNodeAlphabet]) -def mpalphabet_model(request): - return _prepare_db_test(request) - - -@pytest.fixture(scope='function', params=[models.MP_TestNodeSortedAutoNow]) -def mpsortedautonow_model(request): - return _prepare_db_test(request) - - -@pytest.fixture(scope='function', params=[models.MP_TestNodeSmallStep]) -def mpsmallstep_model(request): - return _prepare_db_test(request) - - -@pytest.fixture(scope='function', params=[models.MP_TestManyToManyWithUser]) -def mpm2muser_model(request): - return _prepare_db_test(request) - - -class TestTreeBase(object): - def got(self, model): - if model in [models.NS_TestNode, models.NS_TestNode_Proxy]: - # this slows down nested sets tests quite a bit, but it has the - # advantage that we'll check the node edges are correct - d = {} - for tree_id, lft, rgt in model.objects.values_list('tree_id', - 'lft', - 'rgt'): - d.setdefault(tree_id, []).extend([lft, rgt]) - for tree_id, got_edges in d.items(): - assert len(got_edges) == max(got_edges) - good_edges = list(range(1, len(got_edges) + 1)) - assert sorted(got_edges) == good_edges - - return [(o.desc, o.get_depth(), o.get_children_count()) - for o in model.get_tree()] - - def _assert_get_annotated_list(self, model, expected, parent=None): - got = [ - (obj[0].desc, obj[1]['open'], obj[1]['close'], obj[1]['level']) - for obj in model.get_annotated_list(parent) - ] - assert expected == got - - -class TestEmptyTree(TestTreeBase): - - def test_load_bulk_empty(self, model): - ids = model.load_bulk(BASE_DATA) - got_descs = [obj.desc - for obj in model.objects.filter(id__in=ids)] - expected_descs = [x[0] for x in UNCHANGED] - assert sorted(got_descs) == sorted(expected_descs) - assert self.got(model) == UNCHANGED - - def test_dump_bulk_empty(self, model): - assert model.dump_bulk() == [] - - def test_add_root_empty(self, model): - model.add_root(desc='1') - expected = [('1', 1, 0)] - assert self.got(model) == expected - - def test_get_root_nodes_empty(self, model): - got = model.get_root_nodes() - expected = [] - assert [node.desc for node in got] == expected - - def test_get_first_root_node_empty(self, model): - got = model.get_first_root_node() - assert got is None - - def test_get_last_root_node_empty(self, model): - got = model.get_last_root_node() - assert got is None - - def test_get_tree(self, model): - got = list(model.get_tree()) - assert got == [] - - def test_get_annotated_list(self, model): - expected = [] - self._assert_get_annotated_list(model, expected) - - -class TestNonEmptyTree(TestTreeBase): - - @classmethod - def setup_class(cls): - for model in models.BASE_MODELS: - model.load_bulk(BASE_DATA) - - @classmethod - def teardown_class(cls): - models.empty_models_tables(models.BASE_MODELS) - - -class TestClassMethods(TestNonEmptyTree): - - def test_load_bulk_existing(self, model): - # inserting on an existing node - node = model.objects.get(desc='231') - ids = model.load_bulk(BASE_DATA, node) - expected = [('1', 1, 0), - ('2', 1, 4), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 1), - ('231', 3, 4), - ('1', 4, 0), - ('2', 4, 4), - ('21', 5, 0), - ('22', 5, 0), - ('23', 5, 1), - ('231', 6, 0), - ('24', 5, 0), - ('3', 4, 0), - ('4', 4, 1), - ('41', 5, 0), - ('24', 2, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - expected_descs = ['1', '2', '21', '22', '23', '231', '24', - '3', '4', '41'] - got_descs = [obj.desc for obj in model.objects.filter(id__in=ids)] - assert sorted(got_descs) == sorted(expected_descs) - assert self.got(model) == expected - - def test_get_tree_all(self, model): - got = [(o.desc, o.get_depth(), o.get_children_count()) - for o in model.get_tree()] - assert got == UNCHANGED - - def test_dump_bulk_all(self, model): - assert model.dump_bulk(keep_ids=False) == BASE_DATA - - def test_get_tree_node(self, model): - node = model.objects.get(desc='231') - model.load_bulk(BASE_DATA, node) - - # the tree was modified by load_bulk, so we reload our node object - node = model.objects.get(pk=node.pk) - - got = [(o.desc, o.get_depth(), o.get_children_count()) - for o in model.get_tree(node)] - expected = [('231', 3, 4), - ('1', 4, 0), - ('2', 4, 4), - ('21', 5, 0), - ('22', 5, 0), - ('23', 5, 1), - ('231', 6, 0), - ('24', 5, 0), - ('3', 4, 0), - ('4', 4, 1), - ('41', 5, 0)] - assert got == expected - - def test_get_tree_leaf(self, model): - node = model.objects.get(desc='1') - - assert 0 == node.get_children_count() - got = [(o.desc, o.get_depth(), o.get_children_count()) - for o in model.get_tree(node)] - expected = [('1', 1, 0)] - assert got == expected - - def test_get_annotated_list_all(self, model): - expected = [('1', True, [], 0), ('2', False, [], 0), - ('21', True, [], 1), ('22', False, [], 1), - ('23', False, [], 1), ('231', True, [0], 2), - ('24', False, [0], 1), ('3', False, [], 0), - ('4', False, [], 0), ('41', True, [0, 1], 1)] - self._assert_get_annotated_list(model, expected) - - def test_get_annotated_list_node(self, model): - node = model.objects.get(desc='2') - expected = [('2', True, [], 0), ('21', True, [], 1), - ('22', False, [], 1), ('23', False, [], 1), - ('231', True, [0], 2), ('24', False, [0, 1], 1)] - self._assert_get_annotated_list(model, expected, node) - - def test_get_annotated_list_leaf(self, model): - node = model.objects.get(desc='1') - expected = [('1', True, [0], 0)] - self._assert_get_annotated_list(model, expected, node) - - def test_dump_bulk_node(self, model): - node = model.objects.get(desc='231') - model.load_bulk(BASE_DATA, node) - - # the tree was modified by load_bulk, so we reload our node object - node = model.objects.get(pk=node.pk) - - got = model.dump_bulk(node, False) - expected = [{'data': {'desc': '231'}, 'children': BASE_DATA}] - assert got == expected - - def test_load_and_dump_bulk_keeping_ids(self, model): - exp = model.dump_bulk(keep_ids=True) - model.objects.all().delete() - model.load_bulk(exp, None, True) - got = model.dump_bulk(keep_ids=True) - assert got == exp - # do we really have an unchaged tree after the dump/delete/load? - got = [(o.desc, o.get_depth(), o.get_children_count()) - for o in model.get_tree()] - assert got == UNCHANGED - - def test_load_and_dump_bulk_with_fk(self, related_model): - # https://bitbucket.org/tabo/django-treebeard/issue/48/ - related_model.objects.all().delete() - related, created = models.RelatedModel.objects.get_or_create( - desc="Test %s" % related_model.__name__) - - related_data = [ - {'data': {'desc': '1', 'related': related.pk}}, - {'data': {'desc': '2', 'related': related.pk}, 'children': [ - {'data': {'desc': '21', 'related': related.pk}}, - {'data': {'desc': '22', 'related': related.pk}}, - {'data': {'desc': '23', 'related': related.pk}, 'children': [ - {'data': {'desc': '231', 'related': related.pk}}, - ]}, - {'data': {'desc': '24', 'related': related.pk}}, - ]}, - {'data': {'desc': '3', 'related': related.pk}}, - {'data': {'desc': '4', 'related': related.pk}, 'children': [ - {'data': {'desc': '41', 'related': related.pk}}, - ]}] - related_model.load_bulk(related_data) - got = related_model.dump_bulk(keep_ids=False) - assert got == related_data - - def test_get_root_nodes(self, model): - got = model.get_root_nodes() - expected = ['1', '2', '3', '4'] - assert [node.desc for node in got] == expected - - def test_get_first_root_node(self, model): - got = model.get_first_root_node() - assert got.desc == '1' - - def test_get_last_root_node(self, model): - got = model.get_last_root_node() - assert got.desc == '4' - - def test_add_root(self, model): - obj = model.add_root(desc='5') - assert obj.get_depth() == 1 - assert model.get_last_root_node().desc == '5' - - -class TestSimpleNodeMethods(TestNonEmptyTree): - def test_is_root(self, model): - data = [ - ('2', True), - ('1', True), - ('4', True), - ('21', False), - ('24', False), - ('22', False), - ('231', False), - ] - for desc, expected in data: - got = model.objects.get(desc=desc).is_root() - assert got == expected - - def test_is_leaf(self, model): - data = [ - ('2', False), - ('23', False), - ('231', True), - ] - for desc, expected in data: - got = model.objects.get(desc=desc).is_leaf() - assert got == expected - - def test_get_root(self, model): - data = [ - ('2', '2'), - ('1', '1'), - ('4', '4'), - ('21', '2'), - ('24', '2'), - ('22', '2'), - ('231', '2'), - ] - for desc, expected in data: - node = model.objects.get(desc=desc).get_root() - assert node.desc == expected - - def test_get_parent(self, model): - data = [ - ('2', None), - ('1', None), - ('4', None), - ('21', '2'), - ('24', '2'), - ('22', '2'), - ('231', '23'), - ] - data = dict(data) - objs = {} - for desc, expected in data.items(): - node = model.objects.get(desc=desc) - parent = node.get_parent() - if expected: - assert parent.desc == expected - else: - assert parent is None - objs[desc] = node - # corrupt the objects' parent cache - node._parent_obj = 'CORRUPTED!!!' - - for desc, expected in data.items(): - node = objs[desc] - # asking get_parent to not use the parent cache (since we - # corrupted it in the previous loop) - parent = node.get_parent(True) - if expected: - assert parent.desc == expected - else: - assert parent is None - - def test_get_children(self, model): - data = [ - ('2', ['21', '22', '23', '24']), - ('23', ['231']), - ('231', []), - ] - for desc, expected in data: - children = model.objects.get(desc=desc).get_children() - assert [node.desc for node in children] == expected - - def test_get_children_count(self, model): - data = [ - ('2', 4), - ('23', 1), - ('231', 0), - ] - for desc, expected in data: - got = model.objects.get(desc=desc).get_children_count() - assert got == expected - - def test_get_siblings(self, model): - data = [ - ('2', ['1', '2', '3', '4']), - ('21', ['21', '22', '23', '24']), - ('231', ['231']), - ] - for desc, expected in data: - siblings = model.objects.get(desc=desc).get_siblings() - assert [node.desc for node in siblings] == expected - - def test_get_first_sibling(self, model): - data = [ - ('2', '1'), - ('1', '1'), - ('4', '1'), - ('21', '21'), - ('24', '21'), - ('22', '21'), - ('231', '231'), - ] - for desc, expected in data: - node = model.objects.get(desc=desc).get_first_sibling() - assert node.desc == expected - - def test_get_prev_sibling(self, model): - data = [ - ('2', '1'), - ('1', None), - ('4', '3'), - ('21', None), - ('24', '23'), - ('22', '21'), - ('231', None), - ] - for desc, expected in data: - node = model.objects.get(desc=desc).get_prev_sibling() - if expected is None: - assert node is None - else: - assert node.desc == expected - - def test_get_next_sibling(self, model): - data = [ - ('2', '3'), - ('1', '2'), - ('4', None), - ('21', '22'), - ('24', None), - ('22', '23'), - ('231', None), - ] - for desc, expected in data: - node = model.objects.get(desc=desc).get_next_sibling() - if expected is None: - assert node is None - else: - assert node.desc == expected - - def test_get_last_sibling(self, model): - data = [ - ('2', '4'), - ('1', '4'), - ('4', '4'), - ('21', '24'), - ('24', '24'), - ('22', '24'), - ('231', '231'), - ] - for desc, expected in data: - node = model.objects.get(desc=desc).get_last_sibling() - assert node.desc == expected - - def test_get_first_child(self, model): - data = [ - ('2', '21'), - ('21', None), - ('23', '231'), - ('231', None), - ] - for desc, expected in data: - node = model.objects.get(desc=desc).get_first_child() - if expected is None: - assert node is None - else: - assert node.desc == expected - - def test_get_last_child(self, model): - data = [ - ('2', '24'), - ('21', None), - ('23', '231'), - ('231', None), - ] - for desc, expected in data: - node = model.objects.get(desc=desc).get_last_child() - if expected is None: - assert node is None - else: - assert node.desc == expected - - def test_get_ancestors(self, model): - data = [ - ('2', []), - ('21', ['2']), - ('231', ['2', '23']), - ] - for desc, expected in data: - nodes = model.objects.get(desc=desc).get_ancestors() - assert [node.desc for node in nodes] == expected - - def test_get_descendants(self, model): - data = [ - ('2', ['21', '22', '23', '231', '24']), - ('23', ['231']), - ('231', []), - ('1', []), - ('4', ['41']), - ] - for desc, expected in data: - nodes = model.objects.get(desc=desc).get_descendants() - assert [node.desc for node in nodes] == expected - - def test_get_descendant_count(self, model): - data = [ - ('2', 5), - ('23', 1), - ('231', 0), - ('1', 0), - ('4', 1), - ] - for desc, expected in data: - got = model.objects.get(desc=desc).get_descendant_count() - assert got == expected - - def test_is_sibling_of(self, model): - data = [ - ('2', '2', True), - ('2', '1', True), - ('21', '2', False), - ('231', '2', False), - ('22', '23', True), - ('231', '23', False), - ('231', '231', True), - ] - for desc1, desc2, expected in data: - node1 = model.objects.get(desc=desc1) - node2 = model.objects.get(desc=desc2) - assert node1.is_sibling_of(node2) == expected - - def test_is_child_of(self, model): - data = [ - ('2', '2', False), - ('2', '1', False), - ('21', '2', True), - ('231', '2', False), - ('231', '23', True), - ('231', '231', False), - ] - for desc1, desc2, expected in data: - node1 = model.objects.get(desc=desc1) - node2 = model.objects.get(desc=desc2) - assert node1.is_child_of(node2) == expected - - def test_is_descendant_of(self, model): - data = [ - ('2', '2', False), - ('2', '1', False), - ('21', '2', True), - ('231', '2', True), - ('231', '23', True), - ('231', '231', False), - ] - for desc1, desc2, expected in data: - node1 = model.objects.get(desc=desc1) - node2 = model.objects.get(desc=desc2) - assert node1.is_descendant_of(node2) == expected - - -class TestAddChild(TestNonEmptyTree): - def test_add_child_to_leaf(self, model): - model.objects.get(desc='231').add_child(desc='2311') - expected = [('1', 1, 0), - ('2', 1, 4), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 1), - ('231', 3, 1), - ('2311', 4, 0), - ('24', 2, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - def test_add_child_to_node(self, model): - model.objects.get(desc='2').add_child(desc='25') - expected = [('1', 1, 0), - ('2', 1, 5), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 1), - ('231', 3, 0), - ('24', 2, 0), - ('25', 2, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - -class TestAddSibling(TestNonEmptyTree): - def test_add_sibling_invalid_pos(self, model): - with pytest.raises(InvalidPosition): - model.objects.get(desc='231').add_sibling('invalid_pos') - - def test_add_sibling_missing_nodeorderby(self, model): - node_wchildren = model.objects.get(desc='2') - with pytest.raises(MissingNodeOrderBy): - node_wchildren.add_sibling('sorted-sibling', desc='aaa') - - def test_add_sibling_last_root(self, model): - node_wchildren = model.objects.get(desc='2') - obj = node_wchildren.add_sibling('last-sibling', desc='5') - assert obj.get_depth() == 1 - assert node_wchildren.get_last_sibling().desc == '5' - - def test_add_sibling_last(self, model): - node = model.objects.get(desc='231') - obj = node.add_sibling('last-sibling', desc='232') - assert obj.get_depth() == 3 - assert node.get_last_sibling().desc == '232' - - def test_add_sibling_first_root(self, model): - node_wchildren = model.objects.get(desc='2') - obj = node_wchildren.add_sibling('first-sibling', desc='new') - assert obj.get_depth() == 1 - expected = [('new', 1, 0), - ('1', 1, 0), - ('2', 1, 4), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 1), - ('231', 3, 0), - ('24', 2, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - def test_add_sibling_first(self, model): - node_wchildren = model.objects.get(desc='23') - obj = node_wchildren.add_sibling('first-sibling', desc='new') - assert obj.get_depth() == 2 - expected = [('1', 1, 0), - ('2', 1, 5), - ('new', 2, 0), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 1), - ('231', 3, 0), - ('24', 2, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - def test_add_sibling_left_root(self, model): - node_wchildren = model.objects.get(desc='2') - obj = node_wchildren.add_sibling('left', desc='new') - assert obj.get_depth() == 1 - expected = [('1', 1, 0), - ('new', 1, 0), - ('2', 1, 4), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 1), - ('231', 3, 0), - ('24', 2, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - def test_add_sibling_left(self, model): - node_wchildren = model.objects.get(desc='23') - obj = node_wchildren.add_sibling('left', desc='new') - assert obj.get_depth() == 2 - expected = [('1', 1, 0), - ('2', 1, 5), - ('21', 2, 0), - ('22', 2, 0), - ('new', 2, 0), - ('23', 2, 1), - ('231', 3, 0), - ('24', 2, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - def test_add_sibling_left_noleft_root(self, model): - node = model.objects.get(desc='1') - obj = node.add_sibling('left', desc='new') - assert obj.get_depth() == 1 - expected = [('new', 1, 0), - ('1', 1, 0), - ('2', 1, 4), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 1), - ('231', 3, 0), - ('24', 2, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - def test_add_sibling_left_noleft(self, model): - node = model.objects.get(desc='231') - obj = node.add_sibling('left', desc='new') - assert obj.get_depth() == 3 - expected = [('1', 1, 0), - ('2', 1, 4), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 2), - ('new', 3, 0), - ('231', 3, 0), - ('24', 2, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - def test_add_sibling_right_root(self, model): - node_wchildren = model.objects.get(desc='2') - obj = node_wchildren.add_sibling('right', desc='new') - assert obj.get_depth() == 1 - expected = [('1', 1, 0), - ('2', 1, 4), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 1), - ('231', 3, 0), - ('24', 2, 0), - ('new', 1, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - def test_add_sibling_right(self, model): - node_wchildren = model.objects.get(desc='23') - obj = node_wchildren.add_sibling('right', desc='new') - assert obj.get_depth() == 2 - expected = [('1', 1, 0), - ('2', 1, 5), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 1), - ('231', 3, 0), - ('new', 2, 0), - ('24', 2, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - def test_add_sibling_right_noright_root(self, model): - node = model.objects.get(desc='4') - obj = node.add_sibling('right', desc='new') - assert obj.get_depth() == 1 - expected = [('1', 1, 0), - ('2', 1, 4), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 1), - ('231', 3, 0), - ('24', 2, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0), - ('new', 1, 0)] - assert self.got(model) == expected - - def test_add_sibling_right_noright(self, model): - node = model.objects.get(desc='231') - obj = node.add_sibling('right', desc='new') - assert obj.get_depth() == 3 - expected = [('1', 1, 0), - ('2', 1, 4), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 2), - ('231', 3, 0), - ('new', 3, 0), - ('24', 2, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - -class TestDelete(TestNonEmptyTree): - - @classmethod - def setup_class(cls): - TestNonEmptyTree.setup_class() - for model, dep_model in zip(models.BASE_MODELS, models.DEP_MODELS): - for node in model.objects.all(): - dep_model(node=node).save() - - @classmethod - def teardown_class(cls): - models.empty_models_tables(models.DEP_MODELS + models.BASE_MODELS) - - def test_delete_leaf(self, model): - model.objects.get(desc='231').delete() - expected = [('1', 1, 0), - ('2', 1, 4), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 0), - ('24', 2, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - def test_delete_node(self, model): - model.objects.get(desc='23').delete() - expected = [('1', 1, 0), - ('2', 1, 3), - ('21', 2, 0), - ('22', 2, 0), - ('24', 2, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - def test_delete_root(self, model): - model.objects.get(desc='2').delete() - expected = [('1', 1, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - def test_delete_filter_root_nodes(self, model): - model.objects.filter(desc__in=('2', '3')).delete() - expected = [('1', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - def test_delete_filter_children(self, model): - model.objects.filter(desc__in=('2', '23', '231')).delete() - expected = [('1', 1, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - def test_delete_nonexistant_nodes(self, model): - model.objects.filter(desc__in=('ZZZ', 'XXX')).delete() - assert self.got(model) == UNCHANGED - - def test_delete_same_node_twice(self, model): - model.objects.filter(desc__in=('2', '2')).delete() - expected = [('1', 1, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - def test_delete_all_root_nodes(self, model): - model.get_root_nodes().delete() - count = model.objects.count() - assert count == 0 - - def test_delete_all_nodes(self, model): - model.objects.all().delete() - count = model.objects.count() - assert count == 0 - - -class TestMoveErrors(TestNonEmptyTree): - def test_move_invalid_pos(self, model): - node = model.objects.get(desc='231') - with pytest.raises(InvalidPosition): - node.move(node, 'invalid_pos') - - def test_move_to_descendant(self, model): - node = model.objects.get(desc='2') - target = model.objects.get(desc='231') - with pytest.raises(InvalidMoveToDescendant): - node.move(target, 'first-sibling') - - def test_move_missing_nodeorderby(self, model): - node = model.objects.get(desc='231') - with pytest.raises(MissingNodeOrderBy): - node.move(node, 'sorted-child') - with pytest.raises(MissingNodeOrderBy): - node.move(node, 'sorted-sibling') - - -class TestMoveSortedErrors(TestTreeBase): - - def test_nonsorted_move_in_sorted(self, sorted_model): - node = sorted_model.add_root(val1=3, val2=3, desc='zxy') - with pytest.raises(InvalidPosition): - node.move(node, 'left') - - -class TestMoveLeafRoot(TestNonEmptyTree): - def test_move_leaf_last_sibling_root(self, model): - target = model.objects.get(desc='2') - model.objects.get(desc='231').move(target, 'last-sibling') - expected = [('1', 1, 0), - ('2', 1, 4), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 0), - ('24', 2, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0), - ('231', 1, 0)] - assert self.got(model) == expected - - def test_move_leaf_first_sibling_root(self, model): - target = model.objects.get(desc='2') - model.objects.get(desc='231').move(target, 'first-sibling') - expected = [('231', 1, 0), - ('1', 1, 0), - ('2', 1, 4), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 0), - ('24', 2, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - def test_move_leaf_left_sibling_root(self, model): - target = model.objects.get(desc='2') - model.objects.get(desc='231').move(target, 'left') - expected = [('1', 1, 0), - ('231', 1, 0), - ('2', 1, 4), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 0), - ('24', 2, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - def test_move_leaf_right_sibling_root(self, model): - target = model.objects.get(desc='2') - model.objects.get(desc='231').move(target, 'right') - expected = [('1', 1, 0), - ('2', 1, 4), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 0), - ('24', 2, 0), - ('231', 1, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - def test_move_leaf_last_child_root(self, model): - target = model.objects.get(desc='2') - model.objects.get(desc='231').move(target, 'last-child') - expected = [('1', 1, 0), - ('2', 1, 5), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 0), - ('24', 2, 0), - ('231', 2, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - def test_move_leaf_first_child_root(self, model): - target = model.objects.get(desc='2') - model.objects.get(desc='231').move(target, 'first-child') - expected = [('1', 1, 0), - ('2', 1, 5), - ('231', 2, 0), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 0), - ('24', 2, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - -class TestMoveLeaf(TestNonEmptyTree): - def test_move_leaf_last_sibling(self, model): - target = model.objects.get(desc='22') - model.objects.get(desc='231').move(target, 'last-sibling') - expected = [('1', 1, 0), - ('2', 1, 5), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 0), - ('24', 2, 0), - ('231', 2, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - def test_move_leaf_first_sibling(self, model): - target = model.objects.get(desc='22') - model.objects.get(desc='231').move(target, 'first-sibling') - expected = [('1', 1, 0), - ('2', 1, 5), - ('231', 2, 0), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 0), - ('24', 2, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - def test_move_leaf_left_sibling(self, model): - target = model.objects.get(desc='22') - model.objects.get(desc='231').move(target, 'left') - expected = [('1', 1, 0), - ('2', 1, 5), - ('21', 2, 0), - ('231', 2, 0), - ('22', 2, 0), - ('23', 2, 0), - ('24', 2, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - def test_move_leaf_right_sibling(self, model): - target = model.objects.get(desc='22') - model.objects.get(desc='231').move(target, 'right') - expected = [('1', 1, 0), - ('2', 1, 5), - ('21', 2, 0), - ('22', 2, 0), - ('231', 2, 0), - ('23', 2, 0), - ('24', 2, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - def test_move_leaf_left_sibling_itself(self, model): - target = model.objects.get(desc='231') - model.objects.get(desc='231').move(target, 'left') - assert self.got(model) == UNCHANGED - - def test_move_leaf_last_child(self, model): - target = model.objects.get(desc='22') - model.objects.get(desc='231').move(target, 'last-child') - expected = [('1', 1, 0), - ('2', 1, 4), - ('21', 2, 0), - ('22', 2, 1), - ('231', 3, 0), - ('23', 2, 0), - ('24', 2, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - def test_move_leaf_first_child(self, model): - target = model.objects.get(desc='22') - model.objects.get(desc='231').move(target, 'first-child') - expected = [('1', 1, 0), - ('2', 1, 4), - ('21', 2, 0), - ('22', 2, 1), - ('231', 3, 0), - ('23', 2, 0), - ('24', 2, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - -class TestMoveBranchRoot(TestNonEmptyTree): - def test_move_branch_first_sibling_root(self, model): - target = model.objects.get(desc='2') - model.objects.get(desc='4').move(target, 'first-sibling') - expected = [('4', 1, 1), - ('41', 2, 0), - ('1', 1, 0), - ('2', 1, 4), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 1), - ('231', 3, 0), - ('24', 2, 0), - ('3', 1, 0)] - assert self.got(model) == expected - - def test_move_branch_last_sibling_root(self, model): - target = model.objects.get(desc='2') - model.objects.get(desc='4').move(target, 'last-sibling') - expected = [('1', 1, 0), - ('2', 1, 4), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 1), - ('231', 3, 0), - ('24', 2, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - def test_move_branch_left_sibling_root(self, model): - target = model.objects.get(desc='2') - model.objects.get(desc='4').move(target, 'left') - expected = [('1', 1, 0), - ('4', 1, 1), - ('41', 2, 0), - ('2', 1, 4), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 1), - ('231', 3, 0), - ('24', 2, 0), - ('3', 1, 0)] - assert self.got(model) == expected - - def test_move_branch_right_sibling_root(self, model): - target = model.objects.get(desc='2') - model.objects.get(desc='4').move(target, 'right') - expected = [('1', 1, 0), - ('2', 1, 4), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 1), - ('231', 3, 0), - ('24', 2, 0), - ('4', 1, 1), - ('41', 2, 0), - ('3', 1, 0)] - assert self.got(model) == expected - - def test_move_branch_left_noleft_sibling_root(self, model): - target = model.objects.get(desc='2').get_first_sibling() - model.objects.get(desc='4').move(target, 'left') - expected = [('4', 1, 1), - ('41', 2, 0), - ('1', 1, 0), - ('2', 1, 4), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 1), - ('231', 3, 0), - ('24', 2, 0), - ('3', 1, 0)] - assert self.got(model) == expected - - def test_move_branch_right_noright_sibling_root(self, model): - target = model.objects.get(desc='2').get_last_sibling() - model.objects.get(desc='4').move(target, 'right') - expected = [('1', 1, 0), - ('2', 1, 4), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 1), - ('231', 3, 0), - ('24', 2, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - def test_move_branch_first_child_root(self, model): - target = model.objects.get(desc='2') - model.objects.get(desc='4').move(target, 'first-child') - expected = [('1', 1, 0), - ('2', 1, 5), - ('4', 2, 1), - ('41', 3, 0), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 1), - ('231', 3, 0), - ('24', 2, 0), - ('3', 1, 0)] - assert self.got(model) == expected - - def test_move_branch_last_child_root(self, model): - target = model.objects.get(desc='2') - model.objects.get(desc='4').move(target, 'last-child') - expected = [('1', 1, 0), - ('2', 1, 5), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 1), - ('231', 3, 0), - ('24', 2, 0), - ('4', 2, 1), - ('41', 3, 0), - ('3', 1, 0)] - assert self.got(model) == expected - - -class TestMoveBranch(TestNonEmptyTree): - def test_move_branch_first_sibling(self, model): - target = model.objects.get(desc='23') - model.objects.get(desc='4').move(target, 'first-sibling') - expected = [('1', 1, 0), - ('2', 1, 5), - ('4', 2, 1), - ('41', 3, 0), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 1), - ('231', 3, 0), - ('24', 2, 0), - ('3', 1, 0)] - assert self.got(model) == expected - - def test_move_branch_last_sibling(self, model): - target = model.objects.get(desc='23') - model.objects.get(desc='4').move(target, 'last-sibling') - expected = [('1', 1, 0), - ('2', 1, 5), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 1), - ('231', 3, 0), - ('24', 2, 0), - ('4', 2, 1), - ('41', 3, 0), - ('3', 1, 0)] - assert self.got(model) == expected - - def test_move_branch_left_sibling(self, model): - target = model.objects.get(desc='23') - model.objects.get(desc='4').move(target, 'left') - expected = [('1', 1, 0), - ('2', 1, 5), - ('21', 2, 0), - ('22', 2, 0), - ('4', 2, 1), - ('41', 3, 0), - ('23', 2, 1), - ('231', 3, 0), - ('24', 2, 0), - ('3', 1, 0)] - assert self.got(model) == expected - - def test_move_branch_right_sibling(self, model): - target = model.objects.get(desc='23') - model.objects.get(desc='4').move(target, 'right') - expected = [('1', 1, 0), - ('2', 1, 5), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 1), - ('231', 3, 0), - ('4', 2, 1), - ('41', 3, 0), - ('24', 2, 0), - ('3', 1, 0)] - assert self.got(model) == expected - - def test_move_branch_left_noleft_sibling(self, model): - target = model.objects.get(desc='23').get_first_sibling() - model.objects.get(desc='4').move(target, 'left') - expected = [('1', 1, 0), - ('2', 1, 5), - ('4', 2, 1), - ('41', 3, 0), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 1), - ('231', 3, 0), - ('24', 2, 0), - ('3', 1, 0)] - assert self.got(model) == expected - - def test_move_branch_right_noright_sibling(self, model): - target = model.objects.get(desc='23').get_last_sibling() - model.objects.get(desc='4').move(target, 'right') - expected = [('1', 1, 0), - ('2', 1, 5), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 1), - ('231', 3, 0), - ('24', 2, 0), - ('4', 2, 1), - ('41', 3, 0), - ('3', 1, 0)] - assert self.got(model) == expected - - def test_move_branch_left_itself_sibling(self, model): - target = model.objects.get(desc='4') - model.objects.get(desc='4').move(target, 'left') - assert self.got(model) == UNCHANGED - - def test_move_branch_first_child(self, model): - target = model.objects.get(desc='23') - model.objects.get(desc='4').move(target, 'first-child') - expected = [('1', 1, 0), - ('2', 1, 4), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 2), - ('4', 3, 1), - ('41', 4, 0), - ('231', 3, 0), - ('24', 2, 0), - ('3', 1, 0)] - assert self.got(model) == expected - - def test_move_branch_last_child(self, model): - target = model.objects.get(desc='23') - model.objects.get(desc='4').move(target, 'last-child') - expected = [('1', 1, 0), - ('2', 1, 4), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 2), - ('231', 3, 0), - ('4', 3, 1), - ('41', 4, 0), - ('24', 2, 0), - ('3', 1, 0)] - assert self.got(model) == expected - - -class TestTreeSorted(TestTreeBase): - - def got(self, sorted_model): - return [(o.val1, o.val2, o.desc, o.get_depth(), o.get_children_count()) - for o in sorted_model.get_tree()] - - def test_add_root_sorted(self, sorted_model): - sorted_model.add_root(val1=3, val2=3, desc='zxy') - sorted_model.add_root(val1=1, val2=4, desc='bcd') - sorted_model.add_root(val1=2, val2=5, desc='zxy') - sorted_model.add_root(val1=3, val2=3, desc='abc') - sorted_model.add_root(val1=4, val2=1, desc='fgh') - sorted_model.add_root(val1=3, val2=3, desc='abc') - sorted_model.add_root(val1=2, val2=2, desc='qwe') - sorted_model.add_root(val1=3, val2=2, desc='vcx') - expected = [(1, 4, 'bcd', 1, 0), - (2, 2, 'qwe', 1, 0), - (2, 5, 'zxy', 1, 0), - (3, 2, 'vcx', 1, 0), - (3, 3, 'abc', 1, 0), - (3, 3, 'abc', 1, 0), - (3, 3, 'zxy', 1, 0), - (4, 1, 'fgh', 1, 0)] - assert self.got(sorted_model) == expected - - def test_add_child_root_sorted(self, sorted_model): - root = sorted_model.add_root(val1=0, val2=0, desc='aaa') - root.add_child(val1=3, val2=3, desc='zxy') - root.add_child(val1=1, val2=4, desc='bcd') - root.add_child(val1=2, val2=5, desc='zxy') - root.add_child(val1=3, val2=3, desc='abc') - root.add_child(val1=4, val2=1, desc='fgh') - root.add_child(val1=3, val2=3, desc='abc') - root.add_child(val1=2, val2=2, desc='qwe') - root.add_child(val1=3, val2=2, desc='vcx') - expected = [(0, 0, 'aaa', 1, 8), - (1, 4, 'bcd', 2, 0), - (2, 2, 'qwe', 2, 0), - (2, 5, 'zxy', 2, 0), - (3, 2, 'vcx', 2, 0), - (3, 3, 'abc', 2, 0), - (3, 3, 'abc', 2, 0), - (3, 3, 'zxy', 2, 0), - (4, 1, 'fgh', 2, 0)] - assert self.got(sorted_model) == expected - - def test_add_child_nonroot_sorted(self, sorted_model): - get_node = lambda node_id: sorted_model.objects.get(pk=node_id) - - root_id = sorted_model.add_root(val1=0, val2=0, desc='a').pk - node_id = get_node(root_id).add_child(val1=0, val2=0, desc='ac').pk - get_node(root_id).add_child(val1=0, val2=0, desc='aa') - get_node(root_id).add_child(val1=0, val2=0, desc='av') - get_node(node_id).add_child(val1=0, val2=0, desc='aca') - get_node(node_id).add_child(val1=0, val2=0, desc='acc') - get_node(node_id).add_child(val1=0, val2=0, desc='acb') - - expected = [(0, 0, 'a', 1, 3), - (0, 0, 'aa', 2, 0), - (0, 0, 'ac', 2, 3), - (0, 0, 'aca', 3, 0), - (0, 0, 'acb', 3, 0), - (0, 0, 'acc', 3, 0), - (0, 0, 'av', 2, 0)] - assert self.got(sorted_model) == expected - - def test_move_sorted(self, sorted_model): - sorted_model.add_root(val1=3, val2=3, desc='zxy') - sorted_model.add_root(val1=1, val2=4, desc='bcd') - sorted_model.add_root(val1=2, val2=5, desc='zxy') - sorted_model.add_root(val1=3, val2=3, desc='abc') - sorted_model.add_root(val1=4, val2=1, desc='fgh') - sorted_model.add_root(val1=3, val2=3, desc='abc') - sorted_model.add_root(val1=2, val2=2, desc='qwe') - sorted_model.add_root(val1=3, val2=2, desc='vcx') - root_nodes = sorted_model.get_root_nodes() - target = root_nodes[0] - for node in root_nodes[1:]: - # because raw queries don't update django objects - node = sorted_model.objects.get(pk=node.pk) - target = sorted_model.objects.get(pk=target.pk) - node.move(target, 'sorted-child') - expected = [(1, 4, 'bcd', 1, 7), - (2, 2, 'qwe', 2, 0), - (2, 5, 'zxy', 2, 0), - (3, 2, 'vcx', 2, 0), - (3, 3, 'abc', 2, 0), - (3, 3, 'abc', 2, 0), - (3, 3, 'zxy', 2, 0), - (4, 1, 'fgh', 2, 0)] - assert self.got(sorted_model) == expected - - def test_move_sortedsibling(self, sorted_model): - # https://bitbucket.org/tabo/django-treebeard/issue/27 - sorted_model.add_root(val1=3, val2=3, desc='zxy') - sorted_model.add_root(val1=1, val2=4, desc='bcd') - sorted_model.add_root(val1=2, val2=5, desc='zxy') - sorted_model.add_root(val1=3, val2=3, desc='abc') - sorted_model.add_root(val1=4, val2=1, desc='fgh') - sorted_model.add_root(val1=3, val2=3, desc='abc') - sorted_model.add_root(val1=2, val2=2, desc='qwe') - sorted_model.add_root(val1=3, val2=2, desc='vcx') - root_nodes = sorted_model.get_root_nodes() - target = root_nodes[0] - for node in root_nodes[1:]: - # because raw queries don't update django objects - node = sorted_model.objects.get(pk=node.pk) - target = sorted_model.objects.get(pk=target.pk) - node.val1 -= 2 - node.save() - node.move(target, 'sorted-sibling') - expected = [(0, 2, 'qwe', 1, 0), - (0, 5, 'zxy', 1, 0), - (1, 2, 'vcx', 1, 0), - (1, 3, 'abc', 1, 0), - (1, 3, 'abc', 1, 0), - (1, 3, 'zxy', 1, 0), - (1, 4, 'bcd', 1, 0), - (2, 1, 'fgh', 1, 0)] - assert self.got(sorted_model) == expected - - -class TestMP_TreeAlphabet(TestTreeBase): - def test_alphabet(self, mpalphabet_model): - if not os.getenv('TREEBEARD_TEST_ALPHABET', False): - # run this test only if the enviroment variable is set - return - basealpha = numconv.BASE85 - got_err = False - last_good = None - for alphabetlen in range(35, len(basealpha) + 1): - alphabet = basealpha[0:alphabetlen] - expected = [alphabet[0] + char for char in alphabet[1:]] - expected.extend([alphabet[1] + char for char in alphabet]) - expected.append(alphabet[2] + alphabet[0]) - - # remove all nodes - mpalphabet_model.objects.all().delete() - - # change the model's alphabet - mpalphabet_model.alphabet = alphabet - - # insert root nodes - for pos in range(len(alphabet) * 2): - try: - mpalphabet_model.add_root(numval=pos) - except: - got_err = True - break - if got_err: - break - got = [obj.path - for obj in mpalphabet_model.objects.all()] - if got != expected: - got_err = True - last_good = alphabet - sys.stdout.write( - '\nThe best BASE85 based alphabet for your setup is: %s\n' % ( - last_good, ) - ) - sys.stdout.flush() - - -class TestHelpers(TestTreeBase): - - @classmethod - def setup_class(cls): - for model in models.BASE_MODELS: - model.load_bulk(BASE_DATA) - for node in model.get_root_nodes(): - model.load_bulk(BASE_DATA, node) - model.add_root(desc='5') - - @classmethod - def teardown_class(cls): - models.empty_models_tables(models.BASE_MODELS) - - def test_descendants_group_count_root(self, model): - expected = [(o.desc, o.get_descendant_count()) - for o in model.get_root_nodes()] - got = [(o.desc, o.descendants_count) - for o in model.get_descendants_group_count()] - assert got == expected - - def test_descendants_group_count_node(self, model): - parent = model.get_root_nodes().get(desc='2') - expected = [(o.desc, o.get_descendant_count()) - for o in parent.get_children()] - got = [(o.desc, o.descendants_count) - for o in model.get_descendants_group_count(parent)] - assert got == expected - - -class TestMP_TreeSortedAutoNow(TestTreeBase): - """ - The sorting mechanism used by treebeard when adding a node can fail if the - ordering is using an "auto_now" field - """ - - def test_sorted_by_autonow_workaround(self, mpsortedautonow_model): - # workaround - for i in range(1, 5): - mpsortedautonow_model.add_root(desc='node%d' % (i, ), - created=datetime.datetime.now()) - - def test_sorted_by_autonow_FAIL(self, mpsortedautonow_model): - """ - This test asserts that we have a problem. - fix this, somehow - """ - mpsortedautonow_model.add_root(desc='node1') - with pytest.raises(ValueError): - mpsortedautonow_model.add_root(desc='node2') - - -class TestMP_TreeStepOverflow(TestTreeBase): - def test_add_root(self, mpsmallstep_model): - method = mpsmallstep_model.add_root - for i in range(1, 10): - method() - with pytest.raises(PathOverflow): - method() - - def test_add_child(self, mpsmallstep_model): - root = mpsmallstep_model.add_root() - method = root.add_child - for i in range(1, 10): - method() - with pytest.raises(PathOverflow): - method() - - def test_add_sibling(self, mpsmallstep_model): - root = mpsmallstep_model.add_root() - for i in range(1, 10): - root.add_child() - positions = ('first-sibling', 'left', 'right', 'last-sibling') - for pos in positions: - with pytest.raises(PathOverflow): - root.get_last_child().add_sibling(pos) - - def test_move(self, mpsmallstep_model): - root = mpsmallstep_model.add_root() - for i in range(1, 10): - root.add_child() - newroot = mpsmallstep_model.add_root() - targets = [(root, ['first-child', 'last-child']), - (root.get_first_child(), ['first-sibling', - 'left', - 'right', - 'last-sibling'])] - for target, positions in targets: - for pos in positions: - with pytest.raises(PathOverflow): - newroot.move(target, pos) - - -class TestMP_TreeShortPath(TestTreeBase): - """Test a tree with a very small path field (max_length=4) and a - steplen of 1 - """ - - def test_short_path(self, mpshortnotsorted_model): - obj = mpshortnotsorted_model.add_root() - obj = obj.add_child().add_child().add_child() - with pytest.raises(PathOverflow): - obj.add_child() - - -class TestMP_TreeFindProblems(TestTreeBase): - def test_find_problems(self, mpalphabet_model): - mpalphabet_model.alphabet = '01234' - mpalphabet_model(path='01', depth=1, numchild=0, numval=0).save() - mpalphabet_model(path='1', depth=1, numchild=0, numval=0).save() - mpalphabet_model(path='111', depth=1, numchild=0, numval=0).save() - mpalphabet_model(path='abcd', depth=1, numchild=0, numval=0).save() - mpalphabet_model(path='qa#$%!', depth=1, numchild=0, numval=0).save() - mpalphabet_model(path='0201', depth=2, numchild=0, numval=0).save() - mpalphabet_model(path='020201', depth=3, numchild=0, numval=0).save() - mpalphabet_model(path='03', depth=1, numchild=2, numval=0).save() - mpalphabet_model(path='0301', depth=2, numchild=0, numval=0).save() - mpalphabet_model(path='030102', depth=3, numchild=10, numval=0).save() - mpalphabet_model(path='04', depth=10, numchild=1, numval=0).save() - mpalphabet_model(path='0401', depth=20, numchild=0, numval=0).save() - - def got(ids): - return [o.path for o in - mpalphabet_model.objects.filter(id__in=ids)] - - (evil_chars, bad_steplen, orphans, wrong_depth, wrong_numchild) = ( - mpalphabet_model.find_problems()) - assert ['abcd', 'qa#$%!'] == got(evil_chars) - assert ['1', '111'] == got(bad_steplen) - assert ['0201', '020201'] == got(orphans) - assert ['03', '0301', '030102'] == got(wrong_numchild) - assert ['04', '0401'] == got(wrong_depth) - - -class TestMP_TreeFix(TestTreeBase): - - expected_no_holes = { - models.MP_TestNodeShortPath: [ - ('1', 'b', 1, 2), - ('11', 'u', 2, 1), - ('111', 'i', 3, 1), - ('1111', 'e', 4, 0), - ('12', 'o', 2, 0), - ('2', 'd', 1, 0), - ('3', 'g', 1, 0), - ('4', 'a', 1, 4), - ('41', 'a', 2, 0), - ('42', 'a', 2, 0), - ('43', 'u', 2, 1), - ('431', 'i', 3, 1), - ('4311', 'e', 4, 0), - ('44', 'o', 2, 0)], - models.MP_TestSortedNodeShortPath: [ - ('1', 'a', 1, 4), - ('11', 'a', 2, 0), - ('12', 'a', 2, 0), - ('13', 'o', 2, 0), - ('14', 'u', 2, 1), - ('141', 'i', 3, 1), - ('1411', 'e', 4, 0), - ('2', 'b', 1, 2), - ('21', 'o', 2, 0), - ('22', 'u', 2, 1), - ('221', 'i', 3, 1), - ('2211', 'e', 4, 0), - ('3', 'd', 1, 0), - ('4', 'g', 1, 0)]} - expected_with_holes = { - models.MP_TestNodeShortPath: [ - ('1', 'b', 1, 2), - ('13', 'u', 2, 1), - ('134', 'i', 3, 1), - ('1343', 'e', 4, 0), - ('14', 'o', 2, 0), - ('2', 'd', 1, 0), - ('3', 'g', 1, 0), - ('4', 'a', 1, 4), - ('41', 'a', 2, 0), - ('42', 'a', 2, 0), - ('43', 'u', 2, 1), - ('434', 'i', 3, 1), - ('4343', 'e', 4, 0), - ('44', 'o', 2, 0)], - models.MP_TestSortedNodeShortPath: [ - ('1', 'b', 1, 2), - ('13', 'u', 2, 1), - ('134', 'i', 3, 1), - ('1343', 'e', 4, 0), - ('14', 'o', 2, 0), - ('2', 'd', 1, 0), - ('3', 'g', 1, 0), - ('4', 'a', 1, 4), - ('41', 'a', 2, 0), - ('42', 'a', 2, 0), - ('43', 'u', 2, 1), - ('434', 'i', 3, 1), - ('4343', 'e', 4, 0), - ('44', 'o', 2, 0)]} - - def got(self, model): - return [(o.path, o.desc, o.get_depth(), o.get_children_count()) - for o in model.get_tree()] - - def add_broken_test_data(self, model): - model(path='4', depth=2, numchild=2, desc='a').save() - model(path='13', depth=1000, numchild=0, desc='u').save() - model(path='14', depth=4, numchild=500, desc='o').save() - model(path='134', depth=321, numchild=543, desc='i').save() - model(path='1343', depth=321, numchild=543, desc='e').save() - model(path='42', depth=1, numchild=1, desc='a').save() - model(path='43', depth=1000, numchild=0, desc='u').save() - model(path='44', depth=4, numchild=500, desc='o').save() - model(path='434', depth=321, numchild=543, desc='i').save() - model(path='4343', depth=321, numchild=543, desc='e').save() - model(path='41', depth=1, numchild=1, desc='a').save() - model(path='3', depth=221, numchild=322, desc='g').save() - model(path='1', depth=10, numchild=3, desc='b').save() - model(path='2', depth=10, numchild=3, desc='d').save() - - def test_fix_tree_non_destructive(self, mpshort_model): - self.add_broken_test_data(mpshort_model) - mpshort_model.fix_tree(destructive=False) - got = self.got(mpshort_model) - expected = self.expected_with_holes[mpshort_model] - assert got == expected - mpshort_model.find_problems() - - def test_fix_tree_destructive(self, mpshort_model): - self.add_broken_test_data(mpshort_model) - mpshort_model.fix_tree(destructive=True) - got = self.got(mpshort_model) - expected = self.expected_no_holes[mpshort_model] - assert got == expected - mpshort_model.find_problems() - - -class TestIssues(TestTreeBase): - # test for http://code.google.com/p/django-treebeard/issues/detail?id=14 - - def test_many_to_many_django_user_anonymous(self, mpm2muser_model): - # Using AnonymousUser() in the querysets will expose non-treebeard - # related problems in Django 1.0 - # - # Postgres: - # ProgrammingError: can't adapt - # SQLite: - # InterfaceError: Error binding parameter 4 - probably unsupported - # type. - # MySQL compared a string to an integer field: - # `treebeard_mp_testissue14_users`.`user_id` = 'AnonymousUser' - # - # Using a None field instead works (will be translated to IS NULL). - # - # anonuserobj = AnonymousUser() - anonuserobj = None - - def qs_check(qs, expected): - assert [o.name for o in qs] == expected - - def qs_check_first_or_user(expected, root, user): - qs_check( - root.get_children().filter(Q(name="first") | Q(users=user)), - expected) - - user = User.objects.create_user('test_user', 'test@example.com', - 'testpasswd') - user.save() - root = mpm2muser_model.add_root(name="the root node") - - root.add_child(name="first") - second = root.add_child(name="second") - - qs_check(root.get_children(), ['first', 'second']) - qs_check(root.get_children().filter(Q(name="first")), ['first']) - qs_check(root.get_children().filter(Q(users=user)), []) - - qs_check_first_or_user(['first'], root, user) - - qs_check_first_or_user(['first', 'second'], root, anonuserobj) - - user = User.objects.get(username="test_user") - second.users.add(user) - qs_check_first_or_user(['first', 'second'], root, user) - - qs_check_first_or_user(['first'], root, anonuserobj) - - -class TestMoveNodeForm(TestNonEmptyTree): - def _get_nodes_list(self, nodes): - return [(pk, '%sNode %d' % (' ' * 4 * (depth - 1), pk)) - for pk, depth in nodes] - - def _assert_nodes_in_choices(self, form, nodes): - choices = form.fields['_ref_node_id'].choices - assert 0 == choices.pop(0)[0] - assert nodes == [(choice[0], choice[1]) for choice in choices] - - def _move_node_helper(self, node, safe_parent_nodes): - form_class = movenodeform_factory(type(node)) - form = form_class(instance=node) - assert ['desc', '_position', '_ref_node_id'] == list( - form.base_fields.keys()) - got = [choice[0] for choice in form.fields['_position'].choices] - assert ['first-child', 'left', 'right'] == got - nodes = self._get_nodes_list(safe_parent_nodes) - self._assert_nodes_in_choices(form, nodes) - - def _get_node_ids_and_depths(self, nodes): - return [(node.id, node.get_depth()) for node in nodes] - - def test_form_root_node(self, model): - nodes = list(model.get_tree()) - node = nodes.pop(0) - safe_parent_nodes = self._get_node_ids_and_depths(nodes) - self._move_node_helper(node, safe_parent_nodes) - - def test_form_leaf_node(self, model): - nodes = list(model.get_tree()) - node = nodes.pop() - safe_parent_nodes = self._get_node_ids_and_depths(nodes) - self._move_node_helper(node, safe_parent_nodes) - - def test_form_admin(self, model): - request = None - nodes = list(model.get_tree()) - safe_parent_nodes = self._get_node_ids_and_depths(nodes) - for node in model.objects.all(): - site = AdminSite() - form_class = movenodeform_factory(model) - admin_class = admin_factory(form_class) - ma = admin_class(model, site) - got = list(ma.get_form(request).base_fields.keys()) - desc_pos_refnodeid = ['desc', '_position', '_ref_node_id'] - assert desc_pos_refnodeid == got - got = ma.get_fieldsets(request) - expected = [(None, {'fields': desc_pos_refnodeid})] - assert got == expected - got = ma.get_fieldsets(request, node) - assert got == expected - form = ma.get_form(request)() - nodes = self._get_nodes_list(safe_parent_nodes) - self._assert_nodes_in_choices(form, nodes) - - -class TestModelAdmin(TestNonEmptyTree): - def test_default_fields(self, model): - site = AdminSite() - form_class = movenodeform_factory(model) - admin_class = admin_factory(form_class) - ma = admin_class(model, site) - assert list(ma.get_form(None).base_fields.keys()) == [ - 'desc', '_position', '_ref_node_id'] - - -class TestSortedForm(TestTreeSorted): - def test_sorted_form(self, sorted_model): - sorted_model.add_root(val1=3, val2=3, desc='zxy') - sorted_model.add_root(val1=1, val2=4, desc='bcd') - sorted_model.add_root(val1=2, val2=5, desc='zxy') - sorted_model.add_root(val1=3, val2=3, desc='abc') - sorted_model.add_root(val1=4, val2=1, desc='fgh') - sorted_model.add_root(val1=3, val2=3, desc='abc') - sorted_model.add_root(val1=2, val2=2, desc='qwe') - sorted_model.add_root(val1=3, val2=2, desc='vcx') - - form_class = movenodeform_factory(sorted_model) - form = form_class() - assert list(form.fields.keys()) == ['val1', 'val2', 'desc', - '_position', '_ref_node_id'] - - form = form_class(instance=sorted_model.objects.get(desc='bcd')) - assert list(form.fields.keys()) == ['val1', 'val2', 'desc', - '_position', '_ref_node_id'] - assert 'id__position' in str(form) - assert 'id__ref_node_id' in str(form) - - -class TestForm(TestNonEmptyTree): - def test_form(self, model): - form_class = movenodeform_factory(model) - form = form_class() - assert list(form.fields.keys()) == ['desc', '_position', - '_ref_node_id'] - - form = form_class(instance=model.objects.get(desc='1')) - assert list(form.fields.keys()) == ['desc', '_position', - '_ref_node_id'] - assert 'id__position' in str(form) - assert 'id__ref_node_id' in str(form) - - def test_get_position_ref_node(self, model): - form_class = movenodeform_factory(model) - - instance_parent = model.objects.get(desc='1') - form = form_class(instance=instance_parent) - assert form._get_position_ref_node(instance_parent) == { - '_position': 'first-child', - '_ref_node_id': '' - } - - instance_child = model.objects.get(desc='21') - form = form_class(instance=instance_child) - assert form._get_position_ref_node(instance_child) == { - '_position': 'first-child', - '_ref_node_id': model.objects.get(desc='2').pk - } - - instance_grandchild = model.objects.get(desc='22') - form = form_class(instance=instance_grandchild) - assert form._get_position_ref_node(instance_grandchild) == { - '_position': 'right', - '_ref_node_id': model.objects.get(desc='21').pk - } - - instance_grandchild = model.objects.get(desc='231') - form = form_class(instance=instance_grandchild) - assert form._get_position_ref_node(instance_grandchild) == { - '_position': 'first-child', - '_ref_node_id': model.objects.get(desc='23').pk - } - - def test_clean_cleaned_data(self, model): - instance_parent = model.objects.get(desc='1') - _position = 'first-child' - _ref_node_id = '' - form_class = movenodeform_factory(model) - form = form_class( - instance=instance_parent, - data={ - '_position': _position, - '_ref_node_id': _ref_node_id, - 'desc': instance_parent.desc - } - ) - assert form.is_valid() - assert form._clean_cleaned_data() == (_position, _ref_node_id) - - def test_save_edit(self, model): - instance_parent = model.objects.get(desc='1') - original_count = len(model.objects.all()) - form_class = movenodeform_factory(model) - form = form_class( - instance=instance_parent, - data={ - '_position': 'first-child', - '_ref_node_id': model.objects.get(desc='2').pk, - 'desc': instance_parent.desc - } - ) - assert form.is_valid() - saved_instance = form.save() - assert original_count == model.objects.all().count() - assert saved_instance.get_children_count() == 0 - assert saved_instance.get_depth() == 2 - assert not saved_instance.is_root() - assert saved_instance.is_leaf() - - # Return to original state - form_class = movenodeform_factory(model) - form = form_class( - instance=saved_instance, - data={ - '_position': 'first-child', - '_ref_node_id': '', - 'desc': saved_instance.desc - } - ) - assert form.is_valid() - restored_instance = form.save() - assert original_count == model.objects.all().count() - assert restored_instance.get_children_count() == 0 - assert restored_instance.get_depth() == 1 - assert restored_instance.is_root() - assert restored_instance.is_leaf() - - def test_save_new(self, model): - original_count = model.objects.all().count() - assert original_count == 10 - _position = 'first-child' - form_class = movenodeform_factory(model) - form = form_class( - data={'_position': _position, 'desc': 'New Form Test'}) - assert form.is_valid() - assert form.save() is not None - assert original_count < model.objects.all().count() - - -class TestAdminTreeTemplateTags(TestCase): - def test_treebeard_css(self): - template = Template("{% load admin_tree %}{% treebeard_css %}") - context = Context() - rendered = template.render(context) - expected = ('') - assert expected == rendered - - def test_treebeard_js(self): - template = Template("{% load admin_tree %}{% treebeard_js %}") - context = Context() - rendered = template.render(context) - expected = ('' - '' - '' - '') - assert expected == rendered - - def test_get_static_url(self): - with self.settings(STATIC_URL=None, MEDIA_URL=None): - assert get_static_url() == '/' - with self.settings(STATIC_URL='/static/', MEDIA_URL=None): - assert get_static_url() == '/static/' - with self.settings(STATIC_URL=None, MEDIA_URL='/media/'): - assert get_static_url() == '/media/' - with self.settings(STATIC_URL='/static/', MEDIA_URL='/media/'): - assert get_static_url() == '/static/' - - -class TestAdminTree(TestNonEmptyTree): - template = Template('{% load admin_tree %}{% spaceless %}' - '{% result_tree cl request %}{% endspaceless %}') - - def test_result_tree(self, model_without_proxy): - """ - Verifies that inclusion tag result_list generates a table when with - default ModelAdmin settings. - """ - model = model_without_proxy - request = RequestFactory().get('/admin/tree/') - site = AdminSite() - form_class = movenodeform_factory(model) - admin_class = admin_factory(form_class) - m = admin_class(model, site) - list_display = m.get_list_display(request) - list_display_links = m.get_list_display_links(request, list_display) - cl = ChangeList(request, model, list_display, list_display_links, - m.list_filter, m.date_hierarchy, m.search_fields, - m.list_select_related, m.list_per_page, - m.list_max_show_all, m.list_editable, m) - cl.formset = None - context = Context({'cl': cl, - 'request': request}) - table_output = self.template.render(context) - # We have the same amount of drag handlers as objects - drag_handler = ' ' - assert table_output.count(drag_handler) == model.objects.count() - # All nodes are in the result tree - for object in model.objects.all(): - url = cl.url_for_result(object) - node = 'Node %i' % (url, object.pk) - assert node in table_output - # Unfiltered - assert '' in \ - table_output - - def test_unicode_result_tree(self, model_with_unicode): - """ - Verifies that inclusion tag result_list generates a table when with - default ModelAdmin settings. - """ - model = model_with_unicode - # Add a unicode description - model.add_root(desc='áéîøü') - request = RequestFactory().get('/admin/tree/') - site = AdminSite() - form_class = movenodeform_factory(model) - admin_class = admin_factory(form_class) - m = admin_class(model, site) - list_display = m.get_list_display(request) - list_display_links = m.get_list_display_links(request, list_display) - cl = ChangeList(request, model, list_display, list_display_links, - m.list_filter, m.date_hierarchy, m.search_fields, - m.list_select_related, m.list_per_page, - m.list_max_show_all, m.list_editable, m) - cl.formset = None - context = Context({'cl': cl, - 'request': request}) - table_output = self.template.render(context) - # We have the same amount of drag handlers as objects - drag_handler = ' ' - assert table_output.count(drag_handler) == model.objects.count() - # All nodes are in the result tree - for object in model.objects.all(): - url = cl.url_for_result(object) - node = '%s' % (url, object.desc) - assert node in table_output - # Unfiltered - assert '' in \ - table_output - - def test_result_filtered(self, model_without_proxy): - """ Test template changes with filters or pagination. - """ - model = model_without_proxy - # Filtered GET - request = RequestFactory().get('/admin/tree/?desc=1') - site = AdminSite() - form_class = movenodeform_factory(model) - admin_class = admin_factory(form_class) - m = admin_class(model, site) - list_display = m.get_list_display(request) - list_display_links = m.get_list_display_links(request, list_display) - cl = ChangeList(request, model, list_display, list_display_links, - m.list_filter, m.date_hierarchy, m.search_fields, - m.list_select_related, m.list_per_page, - m.list_max_show_all, m.list_editable, m) - cl.formset = None - context = Context({'cl': cl, - 'request': request}) - table_output = self.template.render(context) - # Filtered - assert '' in \ - table_output - - # Not Filtered GET, it should ignore pagination - request = RequestFactory().get('/admin/tree/?p=1') - list_display = m.get_list_display(request) - list_display_links = m.get_list_display_links(request, list_display) - cl = ChangeList(request, model, list_display, list_display_links, - m.list_filter, m.date_hierarchy, m.search_fields, - m.list_select_related, m.list_per_page, - m.list_max_show_all, m.list_editable, m) - cl.formset = None - context = Context({'cl': cl, - 'request': request}) - table_output = self.template.render(context) - # Not Filtered - assert '' in \ - table_output - - # Not Filtered GET, it should ignore all - request = RequestFactory().get('/admin/tree/?all=1') - list_display = m.get_list_display(request) - list_display_links = m.get_list_display_links(request, list_display) - cl = ChangeList(request, model, list_display, list_display_links, - m.list_filter, m.date_hierarchy, m.search_fields, - m.list_select_related, m.list_per_page, - m.list_max_show_all, m.list_editable, m) - cl.formset = None - context = Context({'cl': cl, - 'request': request}) - table_output = self.template.render(context) - # Not Filtered - assert '' in \ - table_output - - -class TestAdminTreeList(TestNonEmptyTree): - template = Template('{% load admin_tree_list %}{% spaceless %}' - '{% result_tree cl request %}{% endspaceless %}') - - def test_result_tree_list(self, model_without_proxy): - """ - Verifies that inclusion tag result_list generates a table when with - default ModelAdmin settings. - """ - model = model_without_proxy - request = RequestFactory().get('/admin/tree/') - site = AdminSite() - form_class = movenodeform_factory(model) - admin_class = admin_factory(form_class) - m = admin_class(model, site) - list_display = m.get_list_display(request) - list_display_links = m.get_list_display_links(request, list_display) - cl = ChangeList(request, model, list_display, list_display_links, - m.list_filter, m.date_hierarchy, m.search_fields, - m.list_select_related, m.list_per_page, - m.list_max_show_all, m.list_editable, m) - cl.formset = None - context = Context({'cl': cl, - 'request': request}) - table_output = self.template.render(context) - - output_template = '
  • Node %i' - for object in model.objects.all(): - expected_output = output_template % (object.pk, object.pk) - assert expected_output in table_output - - def test_result_tree_list_with_action(self, model_without_proxy): - model = model_without_proxy - request = RequestFactory().get('/admin/tree/') - site = AdminSite() - form_class = movenodeform_factory(model) - admin_class = admin_factory(form_class) - m = admin_class(model, site) - list_display = m.get_list_display(request) - list_display_links = m.get_list_display_links(request, list_display) - cl = ChangeList(request, model, list_display, list_display_links, - m.list_filter, m.date_hierarchy, m.search_fields, - m.list_select_related, m.list_per_page, - m.list_max_show_all, m.list_editable, m) - cl.formset = None - context = Context({'cl': cl, - 'request': request, - 'action_form': True}) - table_output = self.template.render(context) - output_template = ('' - 'Node %i') - - for object in model.objects.all(): - expected_output = output_template % (object.pk, object.pk, - object.pk) - assert expected_output in table_output - - - def test_result_tree_list_with_get(self, model_without_proxy): - model = model_without_proxy - # Test t GET parameter with value id - request = RequestFactory().get('/admin/tree/?t=id') - site = AdminSite() - form_class = movenodeform_factory(model) - admin_class = admin_factory(form_class) - m = admin_class(model, site) - list_display = m.get_list_display(request) - list_display_links = m.get_list_display_links(request, list_display) - cl = ChangeList(request, model, list_display, list_display_links, - m.list_filter, m.date_hierarchy, m.search_fields, - m.list_select_related, m.list_per_page, - m.list_max_show_all, m.list_editable, m) - cl.formset = None - context = Context({'cl': cl, - 'request': request}) - table_output = self.template.render(context) - output_template = "opener.dismissRelatedLookupPopup(window, '%i');" - for object in model.objects.all(): - expected_output = output_template % object.pk - assert expected_output in table_output - - -class TestTreeAdmin(TestNonEmptyTree): - site = AdminSite() - - def _create_superuser(self, username): - return User.objects.create(username=username, is_superuser=True) - - def _mocked_authenticated_request(self, url, user): - request_factory = RequestFactory() - request = request_factory.get(url) - request.user = user - return request - - def _mocked_request(self, data): - request_factory = RequestFactory() - request = request_factory.post('/', data=data) - setattr(request, 'session', 'session') - messages = FallbackStorage(request) - setattr(request, '_messages', messages) - return request - - def _get_admin_obj(self, model_class): - form_class = movenodeform_factory(model_class) - admin_class = admin_factory(form_class) - return admin_class(model_class, self.site) - - def test_changelist_view(self): - tmp_user = self._create_superuser('changelist_tmp') - request = self._mocked_authenticated_request('/', tmp_user) - admin_obj = self._get_admin_obj(models.AL_TestNode) - admin_obj.changelist_view(request) - assert admin_obj.change_list_template == 'admin/tree_list.html' - - admin_obj = self._get_admin_obj(models.MP_TestNode) - admin_obj.changelist_view(request) - assert admin_obj.change_list_template != 'admin/tree_list.html' - - def test_get_node(self, model): - admin_obj = self._get_admin_obj(model) - target = model.objects.get(desc='2') - assert admin_obj.get_node(target.pk) == target - - def test_move_node_validate_keyerror(self, model): - admin_obj = self._get_admin_obj(model) - request = self._mocked_request(data={}) - response = admin_obj.move_node(request) - assert response.status_code == 400 - request = self._mocked_request(data={'node_id': 1}) - response = admin_obj.move_node(request) - assert response.status_code == 400 - - def test_move_node_validate_valueerror(self, model): - admin_obj = self._get_admin_obj(model) - request = self._mocked_request(data={'node_id': 1, - 'sibling_id': 2, - 'as_child': 'invalid'}) - response = admin_obj.move_node(request) - assert response.status_code == 400 - - def test_move_validate_missing_nodeorderby(self, model): - node = model.objects.get(desc='231') - admin_obj = self._get_admin_obj(model) - request = self._mocked_request(data={}) - response = admin_obj.try_to_move_node(True, node, 'sorted-child', - request, target=node) - assert response.status_code == 400 - - response = admin_obj.try_to_move_node(True, node, 'sorted-sibling', - request, target=node) - assert response.status_code == 400 - - def test_move_validate_invalid_pos(self, model): - node = model.objects.get(desc='231') - admin_obj = self._get_admin_obj(model) - request = self._mocked_request(data={}) - response = admin_obj.try_to_move_node(True, node, 'invalid_pos', - request, target=node) - assert response.status_code == 400 - - def test_move_validate_to_descendant(self, model): - node = model.objects.get(desc='2') - target = model.objects.get(desc='231') - admin_obj = self._get_admin_obj(model) - request = self._mocked_request(data={}) - response = admin_obj.try_to_move_node(True, node, 'first-sibling', - request, target) - assert response.status_code == 400 - - def test_move_left(self, model): - node = model.objects.get(desc='231') - target = model.objects.get(desc='2') - - admin_obj = self._get_admin_obj(model) - request = self._mocked_request(data={'node_id': node.pk, - 'sibling_id': target.pk, - 'as_child': 0}) - response = admin_obj.move_node(request) - assert response.status_code == 200 - expected = [('1', 1, 0), - ('231', 1, 0), - ('2', 1, 4), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 0), - ('24', 2, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - - def test_move_last_child(self, model): - node = model.objects.get(desc='231') - target = model.objects.get(desc='2') - - admin_obj = self._get_admin_obj(model) - request = self._mocked_request(data={'node_id': node.pk, - 'sibling_id': target.pk, - 'as_child': 1}) - response = admin_obj.move_node(request) - assert response.status_code == 200 - expected = [('1', 1, 0), - ('2', 1, 5), - ('21', 2, 0), - ('22', 2, 0), - ('23', 2, 0), - ('24', 2, 0), - ('231', 2, 0), - ('3', 1, 0), - ('4', 1, 1), - ('41', 2, 0)] - assert self.got(model) == expected - diff --git a/wagtail/vendor/django-treebeard/treebeard/tests/urls.py b/wagtail/vendor/django-treebeard/treebeard/tests/urls.py deleted file mode 100644 index ccc4d83a9..000000000 --- a/wagtail/vendor/django-treebeard/treebeard/tests/urls.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.conf.urls import patterns, include, url -from django.contrib import admin - -admin.autodiscover() - -urlpatterns = patterns('', - # Uncomment the next line to enable the admin: - url(r'^admin/', include(admin.site.urls)), -) \ No newline at end of file diff --git a/wagtail/wagtailadmin/forms.py b/wagtail/wagtailadmin/forms.py index 6e30381c1..e55a831f4 100644 --- a/wagtail/wagtailadmin/forms.py +++ b/wagtail/wagtailadmin/forms.py @@ -12,7 +12,7 @@ class SearchForm(forms.Form): if _placeholder is not None: placeholder = _placeholder else: - placeholder = 'Search {}'.format(placeholder_suffix) + placeholder = 'Search {0}'.format(placeholder_suffix) self.fields['q'].widget.attrs = {'placeholder': placeholder} q = forms.CharField(label=_("Search term"), widget=forms.TextInput()) diff --git a/wagtail/wagtailadmin/hooks.py b/wagtail/wagtailadmin/hooks.py index ff167d70d..d299c3346 100644 --- a/wagtail/wagtailadmin/hooks.py +++ b/wagtail/wagtailadmin/hooks.py @@ -1,5 +1,9 @@ from django.conf import settings -from django.utils.importlib import import_module +try: + from importlib import import_module +except ImportError: + # for Python 2.6, fall back on django.utils.importlib (deprecated as of Django 1.7) + from django.utils.importlib import import_module _hooks = {} diff --git a/wagtail/wagtailadmin/locale/zh/LC_MESSAGES/django.po b/wagtail/wagtailadmin/locale/zh/LC_MESSAGES/django.po index 80a75d43f..c43755acc 100644 --- a/wagtail/wagtailadmin/locale/zh/LC_MESSAGES/django.po +++ b/wagtail/wagtailadmin/locale/zh/LC_MESSAGES/django.po @@ -31,11 +31,11 @@ msgstr "搜索词" #: .\forms.py:42 msgid "Enter your username" -msgstr "" +msgstr "请输入用户名" #: .\forms.py:45 msgid "Enter password" -msgstr "" +msgstr "请输入密码" #: .\forms.py:50 msgid "Enter your email address to reset your password" @@ -89,7 +89,7 @@ msgstr "登录Wagtail" #: .\templates\wagtailadmin\login.html:42 msgid "Forgotten it?" -msgstr "忘记了?" +msgstr "忘记密码?" #: .\templates\wagtailadmin\account\account.html:4 msgid "Account" @@ -104,7 +104,7 @@ msgid "" "Your avatar image is provided by Gravatar and is connected to your email " "address. With a Gravatar account you can set an avatar for any number of " "other email addresses you use." -msgstr "您的头像图片是由Gravatar提供的,并且关联了您的电子邮件地址。一个Gravatar账号可以设置多个电子邮件地址的头像图片。" +msgstr "您的头像图片是由Gravatar提供的,并且关联了您的电子邮箱。一个Gravatar账号可以设置多个电子邮箱的头像图片。" #: .\templates\wagtailadmin\account\account.html:23 #: .\templates\wagtailadmin\account\change_password.html:4 @@ -113,7 +113,7 @@ msgstr "修改密码" #: .\templates\wagtailadmin\account\account.html:27 msgid "Change the password you use to log in." -msgstr "修改您用于登录的密码。" +msgstr "修改登录密码。" #: .\templates\wagtailadmin\account\change_password.html:16 msgid "Change Password" diff --git a/wagtail/wagtailadmin/locale/zh_TW/LC_MESSAGES/django.mo b/wagtail/wagtailadmin/locale/zh_TW/LC_MESSAGES/django.mo new file mode 100644 index 000000000..a00aa643b Binary files /dev/null and b/wagtail/wagtailadmin/locale/zh_TW/LC_MESSAGES/django.mo differ diff --git a/wagtail/wagtailadmin/locale/zh_TW/LC_MESSAGES/django.po b/wagtail/wagtailadmin/locale/zh_TW/LC_MESSAGES/django.po new file mode 100644 index 000000000..f4d70b60e --- /dev/null +++ b/wagtail/wagtailadmin/locale/zh_TW/LC_MESSAGES/django.po @@ -0,0 +1,843 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Wagtail\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2014-03-14 23:02+0200\n" +"PO-Revision-Date: 2014-05-01 12:09+0000\n" +"Last-Translator: wdv4758h \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: zh_TW\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: .\edit_handlers.py:81 .\edit_handlers.py:130 .\edit_handlers.py:134 +msgid "Please type a valid time" +msgstr "請輸入一個有效的時間" + +#: .\edit_handlers.py:724 +msgid "Common page configuration" +msgstr "一般頁面設定" + +#: .\forms.py:18 +msgid "Search term" +msgstr "搜尋關鍵字" + +#: .\forms.py:42 +msgid "Enter your username" +msgstr "請輸入您的帳號" + +#: .\forms.py:45 +msgid "Enter password" +msgstr "請輸入密碼" + +#: .\forms.py:50 +msgid "Enter your email address to reset your password" +msgstr "請輸入您的電子信箱來重新設定密碼" + +#: .\forms.py:59 +msgid "Please fill your email address." +msgstr "請輸入您的電子信箱" + +#: .\forms.py:72 +msgid "" +"Sorry, you cannot reset your password here as your user account is managed " +"by another server." +msgstr "對不起,您不能在此重新設定您的密碼,因為您的帳號是由其他伺服器所管理。" + +#: .\forms.py:75 +msgid "This email address is not recognised." +msgstr "找不到這個電子信箱。" + +#: .\templates\wagtailadmin\base.html:7 .\templates\wagtailadmin\home.html:4 +msgid "Dashboard" +msgstr "Dashboard" + +#: .\templates\wagtailadmin\base.html:31 +msgid "Menu" +msgstr "選單" + +#: .\templates\wagtailadmin\home.html:22 +#, python-format +msgid "Welcome to the %(site_name)s Wagtail CMS" +msgstr "歡迎進入 %(site_name)s 的 Wagtail 內容管理系統" + +#: .\templates\wagtailadmin\home.html:33 +msgid "" +"This is your dashboard on which helpful information about content you've " +"created will be displayed." +msgstr "這是您的 Dashboard,會顯示對於已建立的內容有幫助的訊息。" + +#: .\templates\wagtailadmin\login.html:4 +#: .\templates\wagtailadmin\login.html:55 +msgid "Sign in" +msgstr "登入" + +#: .\templates\wagtailadmin\login.html:18 +msgid "Your username and password didn't match. Please try again." +msgstr "您的帳號和密碼輸入錯誤,請再試一次。" + +#: .\templates\wagtailadmin\login.html:26 +msgid "Sign in to Wagtail" +msgstr "登入 Wagtail" + +#: .\templates\wagtailadmin\login.html:42 +msgid "Forgotten it?" +msgstr "忘記了嗎?" + +#: .\templates\wagtailadmin\account\account.html:4 +msgid "Account" +msgstr "帳號" + +#: .\templates\wagtailadmin\account\account.html:11 +msgid "Set gravatar" +msgstr "設定 gravatar" + +#: .\templates\wagtailadmin\account\account.html:15 +msgid "" +"Your avatar image is provided by Gravatar and is connected to your email " +"address. With a Gravatar account you can set an avatar for any number of " +"other email addresses you use." +msgstr "您的頭像是由 Gravatar 所提供,並且已經聯結你的電子信箱。一個 Gravatar 帳號可以設定多個電子信箱的頭像圖片。" + +#: .\templates\wagtailadmin\account\account.html:23 +#: .\templates\wagtailadmin\account\change_password.html:4 +msgid "Change password" +msgstr "修改密碼" + +#: .\templates\wagtailadmin\account\account.html:27 +msgid "Change the password you use to log in." +msgstr "修改登入用的密碼。" + +#: .\templates\wagtailadmin\account\change_password.html:16 +msgid "Change Password" +msgstr "修改密碼" + +#: .\templates\wagtailadmin\account\change_password.html:19 +msgid "" +"Your password can't be changed here. Please contact a site administrator." +msgstr "您的密碼不能在這更改。請聯絡網站管理員。" + +#: .\templates\wagtailadmin\account\password_reset\complete.html:4 +#: .\templates\wagtailadmin\account\password_reset\confirm.html:42 +#: .\templates\wagtailadmin\account\password_reset\done.html:4 +#: .\templates\wagtailadmin\account\password_reset\form.html:37 +msgid "Reset password" +msgstr "重新設定密碼" + +#: .\templates\wagtailadmin\account\password_reset\complete.html:15 +msgid "Password change successful" +msgstr "密碼修改成功" + +#: .\templates\wagtailadmin\account\password_reset\complete.html:16 +msgid "Login" +msgstr "登入" + +#: .\templates\wagtailadmin\account\password_reset\confirm.html:4 +#: .\templates\wagtailadmin\account\password_reset\confirm.html:26 +msgid "Set your new password" +msgstr "設定您的新密碼" + +#: .\templates\wagtailadmin\account\password_reset\confirm.html:19 +msgid "The passwords do not match. Please try again." +msgstr "密碼不一致,請再試一次。" + +#: .\templates\wagtailadmin\account\password_reset\done.html:15 +msgid "Check your email" +msgstr "請檢查您的電子信箱" + +#: .\templates\wagtailadmin\account\password_reset\done.html:16 +msgid "A link to reset your password has been emailed to you." +msgstr "一個重新設定密碼連結已經寄到您的電子信箱了" + +#: .\templates\wagtailadmin\account\password_reset\email.txt:2 +msgid "Please follow the link below to reset your password" +msgstr "請點擊下面的連結來重新設定您的密碼" + +#: .\templates\wagtailadmin\account\password_reset\email_subject.txt:2 +msgid "Password reset" +msgstr "密碼已經重新設定" + +#: .\templates\wagtailadmin\account\password_reset\form.html:27 +msgid "Reset your password" +msgstr "重新設定您的密碼" + +#: .\templates\wagtailadmin\chooser\_link_types.html:5 +#: .\templates\wagtailadmin\chooser\_link_types.html:7 +msgid "Internal link" +msgstr "內部連結" + +#: .\templates\wagtailadmin\chooser\_link_types.html:11 +#: .\templates\wagtailadmin\chooser\_link_types.html:13 +msgid "External link" +msgstr "外部連結" + +#: .\templates\wagtailadmin\chooser\_link_types.html:17 +#: .\templates\wagtailadmin\chooser\_link_types.html:19 +msgid "Email link" +msgstr "電子信箱連結" + +#: .\templates\wagtailadmin\chooser\_search_form.html:7 +#: .\templates\wagtailadmin\pages\search.html:3 +#: .\templates\wagtailadmin\pages\search.html:16 +#: .\templatetags\wagtailadmin_nav.py:44 +msgid "Search" +msgstr "搜尋" + +#: .\templates\wagtailadmin\chooser\_search_results.html:3 +#: .\templatetags\wagtailadmin_nav.py:43 +msgid "Explorer" +msgstr "瀏覽" + +#: .\templates\wagtailadmin\chooser\_search_results.html:5 +#: .\templates\wagtailadmin\pages\index.html:15 +#: .\templates\wagtailadmin\pages\move_choose_destination.html:10 +msgid "Home" +msgstr "首頁" + +#: .\templates\wagtailadmin\chooser\_search_results.html:13 +#, python-format +msgid "" +"\n" +" There is one match\n" +" " +msgid_plural "" +"\n" +" There are %(counter)s matches\n" +" " +msgstr[0] "\n 有一個符合" +msgstr[1] "\n 有 $(counter)s 個符合" + +#: .\templates\wagtailadmin\chooser\browse.html:2 +#: .\templates\wagtailadmin\chooser\search.html:2 +#: .\templates\wagtailadmin\edit_handlers\page_chooser_panel.html:13 +msgid "Choose a page" +msgstr "選擇一個頁面" + +#: .\templates\wagtailadmin\chooser\email_link.html:2 +msgid "Add an email link" +msgstr "新增一個電子信箱" + +#: .\templates\wagtailadmin\chooser\email_link.html:14 +#: .\templates\wagtailadmin\chooser\external_link.html:14 +msgid "Insert link" +msgstr "插入一個連結" + +#: .\templates\wagtailadmin\chooser\external_link.html:2 +msgid "Add an external link" +msgstr "新增一個外部連結" + +#: .\templates\wagtailadmin\edit_handlers\chooser_panel.html:20 +msgid "Clear choice" +msgstr "清除選擇" + +#: .\templates\wagtailadmin\edit_handlers\chooser_panel.html:22 +msgid "Choose another item" +msgstr "選擇其他選項" + +#: .\templates\wagtailadmin\edit_handlers\chooser_panel.html:27 +msgid "Choose an item" +msgstr "選擇一個選項" + +#: .\templates\wagtailadmin\edit_handlers\inline_panel_child.html:5 +msgid "Move up" +msgstr "往上移動" + +#: .\templates\wagtailadmin\edit_handlers\inline_panel_child.html:6 +msgid "Move down" +msgstr "往下移動" + +#: .\templates\wagtailadmin\edit_handlers\inline_panel_child.html:8 +#: .\templates\wagtailadmin\pages\confirm_delete.html:7 +#: .\templates\wagtailadmin\pages\edit.html:36 +#: .\templates\wagtailadmin\pages\list.html:68 +#: .\templates\wagtailadmin\pages\list.html:188 +msgid "Delete" +msgstr "刪除" + +#: .\templates\wagtailadmin\edit_handlers\page_chooser_panel.html:12 +msgid "Choose another page" +msgstr "選擇另外一個頁面" + +#: .\templates\wagtailadmin\home\pages_for_moderation.html:5 +msgid "Pages awaiting moderation" +msgstr "這些頁面正等待審核" + +#: .\templates\wagtailadmin\home\pages_for_moderation.html:13 +#: .\templates\wagtailadmin\home\recent_edits.html:12 +#: .\templates\wagtailadmin\pages\list.html:101 +msgid "Title" +msgstr "標題" + +#: .\templates\wagtailadmin\home\pages_for_moderation.html:14 +#: .\templates\wagtailadmin\pages\list.html:22 +msgid "Parent" +msgstr "上一層" + +#: .\templates\wagtailadmin\home\pages_for_moderation.html:15 +#: .\templates\wagtailadmin\pages\list.html:24 +#: .\templates\wagtailadmin\pages\list.html:116 +msgid "Type" +msgstr "類型" + +#: .\templates\wagtailadmin\home\pages_for_moderation.html:16 +msgid "Edited" +msgstr "編輯" + +#: .\templates\wagtailadmin\home\pages_for_moderation.html:23 +#: .\templates\wagtailadmin\home\recent_edits.html:21 +#: .\templates\wagtailadmin\pages\list.html:167 +#: .\templates\wagtailadmin\pages\list.html:176 +msgid "Edit this page" +msgstr "編輯這個頁面" + +#: .\templates\wagtailadmin\home\pages_for_moderation.html:28 +#: .\templates\wagtailadmin\pages\_moderator_userbar.html:12 +msgid "Approve" +msgstr "通過" + +#: .\templates\wagtailadmin\home\pages_for_moderation.html:34 +#: .\templates\wagtailadmin\pages\_moderator_userbar.html:17 +msgid "Reject" +msgstr "拒絕" + +#: .\templates\wagtailadmin\home\pages_for_moderation.html:37 +#: .\templates\wagtailadmin\home\recent_edits.html:23 +#: .\templates\wagtailadmin\pages\_moderator_userbar.html:9 +#: .\templates\wagtailadmin\pages\list.html:56 +#: .\templates\wagtailadmin\pages\list.html:176 +msgid "Edit" +msgstr "編輯" + +#: .\templates\wagtailadmin\home\pages_for_moderation.html:38 +#: .\templates\wagtailadmin\pages\create.html:24 +#: .\templates\wagtailadmin\pages\edit.html:42 +msgid "Preview" +msgstr "預覽" + +#: .\templates\wagtailadmin\home\recent_edits.html:5 +msgid "Your most recent edits" +msgstr "你最近的編輯" + +#: .\templates\wagtailadmin\home\recent_edits.html:13 +msgid "Date" +msgstr "日期" + +#: .\templates\wagtailadmin\home\recent_edits.html:14 +#: .\templates\wagtailadmin\pages\list.html:25 +#: .\templates\wagtailadmin\pages\list.html:128 +msgid "Status" +msgstr "狀態" + +#: .\templates\wagtailadmin\home\recent_edits.html:25 +#: .\templates\wagtailadmin\pages\list.html:59 +#: .\templates\wagtailadmin\pages\list.html:179 +msgid "View draft" +msgstr "觀看草稿" + +#: .\templates\wagtailadmin\home\recent_edits.html:28 +#: .\templates\wagtailadmin\pages\list.html:62 +#: .\templates\wagtailadmin\pages\list.html:182 +msgid "View live" +msgstr "觀看線上版" + +#: .\templates\wagtailadmin\home\site_summary.html:3 +msgid "Site summary" +msgstr "網站摘要" + +#: .\templates\wagtailadmin\home\site_summary.html:6 +#, python-format +msgid "" +"\n" +" %(total_pages)s Page\n" +" " +msgid_plural "" +"\n" +" %(total_pages)s Pages\n" +" " +msgstr[0] "\n %(total_pages)s 頁面\n " +msgstr[1] "\n %(total_pages)s 頁面\n " + +#: .\templates\wagtailadmin\home\site_summary.html:13 +#, python-format +msgid "" +"\n" +" %(total_images)s Image\n" +" " +msgid_plural "" +"\n" +" %(total_images)s Images\n" +" " +msgstr[0] "\n %(total_images)s 圖片\n " +msgstr[1] "\n %(total_images)s 圖片\n " + +#: .\templates\wagtailadmin\home\site_summary.html:20 +#, python-format +msgid "" +"\n" +" %(total_docs)s Document\n" +" " +msgid_plural "" +"\n" +" %(total_docs)s Documents\n" +" " +msgstr[0] "\n %(total_docs)s 文件\n " +msgstr[1] "\n %(total_docs)s 文件\n " + +#: .\templates\wagtailadmin\notifications\approved.html:1 +#, python-format +msgid "The page \"%(title)s\" has been approved" +msgstr "這個頁面 \"%(title)s\" 已經通過" + +#: .\templates\wagtailadmin\notifications\approved.html:2 +#, python-format +msgid "The page \"%(title)s\" has been approved." +msgstr "這個頁面 \"%(title)s\" 已經通過" + +#: .\templates\wagtailadmin\notifications\approved.html:4 +msgid "You can view the page here:" +msgstr "你可以在此觀看這個頁面" + +#: .\templates\wagtailadmin\notifications\rejected.html:1 +#, python-format +msgid "The page \"%(title)s\" has been rejected" +msgstr "這個頁面 \"%(title)s\" 已經被拒絕" + +#: .\templates\wagtailadmin\notifications\rejected.html:2 +#, python-format +msgid "The page \"%(title)s\" has been rejected." +msgstr "這個頁面 \"%(title)s\" 已經被拒絕" + +#: .\templates\wagtailadmin\notifications\rejected.html:4 +#: .\templates\wagtailadmin\notifications\submitted.html:5 +msgid "You can edit the page here:" +msgstr "你可以在此編輯這個頁面:" + +#: .\templates\wagtailadmin\notifications\submitted.html:1 +#, python-format +msgid "The page \"%(page)s\" has been submitted for moderation" +msgstr "這個頁面 \"%(page)s\" 已經送審" + +#: .\templates\wagtailadmin\notifications\submitted.html:2 +#, python-format +msgid "The page \"%(page)s\" has been submitted for moderation." +msgstr "這個頁面 \"%(page)s\" 已經送審。" + +#: .\templates\wagtailadmin\notifications\submitted.html:4 +msgid "You can preview the page here:" +msgstr "你可以在此預覽這個頁面:" + +#: .\templates\wagtailadmin\pages\_moderator_userbar.html:4 +#, python-format +msgid "" +"\n" +" Previewing '%(title)s', submitted by %(submitted_by)s on %(submitted_on)s.\n" +" " +msgstr "\n 預覽 '%(title)s', %(submitted_by)s 在 %(submitted_on)s 送審。\n " + +#: .\templates\wagtailadmin\pages\add_subpage.html:6 +#, python-format +msgid "Create a page in %(title)s" +msgstr "以 %(title)s 為題建立一個頁面" + +#: .\templates\wagtailadmin\pages\add_subpage.html:9 +msgid "Create a page in" +msgstr "在這建立一個頁面" + +#: .\templates\wagtailadmin\pages\add_subpage.html:13 +msgid "Choose which type of page you'd like to create." +msgstr "選擇希望建立的頁面類型。" + +#: .\templates\wagtailadmin\pages\add_subpage.html:26 +#, python-format +msgid "Pages using %(page_type)s" +msgstr "%(page_type)s 類的頁面" + +#: .\templates\wagtailadmin\pages\confirm_delete.html:3 +#, python-format +msgid "Delete %(title)s" +msgstr "刪除 %(title)s" + +#: .\templates\wagtailadmin\pages\confirm_delete.html:12 +msgid "Are you sure you want to delete this page?" +msgstr "你確定要刪除這頁嗎?" + +#: .\templates\wagtailadmin\pages\confirm_delete.html:14 +#, python-format +msgid "" +"\n" +" This will also delete one more subpage.\n" +" " +msgid_plural "" +"\n" +" This will also delete %(descendant_count)s more subpages.\n" +" " +msgstr[0] "\n 這也會刪除一個子頁面。 " +msgstr[1] "\n 這也會刪除 %(descendant_count)s 個子頁面。 " + +#: .\templates\wagtailadmin\pages\confirm_delete.html:22 +msgid "" +"Alternatively you can unpublish the page. This removes the page from public " +"view and you can edit or publish it again later." +msgstr "你可以選擇取消發佈此頁面。此頁面將將無法從外部觀看,你可以編輯後再次發。" + +#: .\templates\wagtailadmin\pages\confirm_delete.html:26 +msgid "Delete it" +msgstr "刪除" + +#: .\templates\wagtailadmin\pages\confirm_delete.html:26 +msgid "Unpublish it" +msgstr "取消發佈" + +#: .\templates\wagtailadmin\pages\confirm_move.html:3 +#, python-format +msgid "Move %(title)s" +msgstr "移動 %(title)s" + +#: .\templates\wagtailadmin\pages\confirm_move.html:6 +#: .\templates\wagtailadmin\pages\list.html:65 +#: .\templates\wagtailadmin\pages\list.html:185 +msgid "Move" +msgstr "移動" + +#: .\templates\wagtailadmin\pages\confirm_move.html:11 +#, python-format +msgid "Are you sure you want to move this page into '%(title)s'?" +msgstr "你確定想要移動此頁面至 '%(title)s' 嗎?" + +#: .\templates\wagtailadmin\pages\confirm_move.html:13 +#, python-format +msgid "" +"Are you sure you want to move this page and all of its children into " +"'%(title)s'?" +msgstr "你確定要移動此頁面和其所有子頁面至 '%(title)s' 嗎?" + +#: .\templates\wagtailadmin\pages\confirm_move.html:18 +msgid "Yes, move this page" +msgstr "是的,移動此頁面" + +#: .\templates\wagtailadmin\pages\confirm_unpublish.html:3 +#, python-format +msgid "Unpublish %(title)s" +msgstr "取消發佈 %(title)s" + +#: .\templates\wagtailadmin\pages\confirm_unpublish.html:6 +#: .\templates\wagtailadmin\pages\edit.html:33 +#: .\templates\wagtailadmin\pages\list.html:71 +#: .\templates\wagtailadmin\pages\list.html:191 +msgid "Unpublish" +msgstr "取消發佈" + +#: .\templates\wagtailadmin\pages\confirm_unpublish.html:10 +msgid "Are you sure you want to unpublish this page?" +msgstr "你確定想取消發佈此頁面嗎?" + +#: .\templates\wagtailadmin\pages\confirm_unpublish.html:13 +msgid "Yes, unpublish it" +msgstr "是的,取消發佈" + +#: .\templates\wagtailadmin\pages\content_type_use.html:7 +msgid "Pages using" +msgstr "頁面正在使用" + +#: .\templates\wagtailadmin\pages\create.html:5 +#, python-format +msgid "New %(page_type)s" +msgstr "新 %(page_type)s 分類" + +#: .\templates\wagtailadmin\pages\create.html:9 +msgid "New" +msgstr "新" + +#: .\templates\wagtailadmin\pages\create.html:21 +msgid "Save as draft" +msgstr "儲存為草稿" + +#: .\templates\wagtailadmin\pages\create.html:26 +#: .\templates\wagtailadmin\pages\edit.html:39 +msgid "Publish" +msgstr "發佈" + +#: .\templates\wagtailadmin\pages\create.html:28 +#: .\templates\wagtailadmin\pages\edit.html:41 +msgid "Submit for moderation" +msgstr "送審" + +#: .\templates\wagtailadmin\pages\edit.html:5 +#, python-format +msgid "Editing %(title)s" +msgstr "編輯 %(title)s" + +#: .\templates\wagtailadmin\pages\edit.html:12 +#, python-format +msgid "Editing %(title)s" +msgstr "編輯 %(title)s" + +#: .\templates\wagtailadmin\pages\edit.html:15 +msgid "Status:" +msgstr "狀態:" + +#: .\templates\wagtailadmin\pages\edit.html:29 +msgid "Save draft" +msgstr "儲存草稿" + +#: .\templates\wagtailadmin\pages\edit.html:52 +#, python-format +msgid "Last modified: %(last_mod)s" +msgstr "上一次編輯:%(last_mod)s" + +#: .\templates\wagtailadmin\pages\edit.html:54 +#, python-format +msgid "by %(modified_by)s" +msgstr "作者 %(modified_by)s" + +#: .\templates\wagtailadmin\pages\index.html:4 +#, python-format +msgid "Exploring %(title)s" +msgstr "瀏覽%(title)s" + +#: .\templates\wagtailadmin\pages\list.html:53 +#: .\templates\wagtailadmin\pages\list.html:194 +msgid "Add child page" +msgstr "新增子頁面" + +#: .\templates\wagtailadmin\pages\list.html:94 +msgid "Disable ordering of child pages" +msgstr "禁止子頁面的排序" + +#: .\templates\wagtailadmin\pages\list.html:94 +#: .\templates\wagtailadmin\pages\list.html:96 +msgid "Order" +msgstr "排序" + +#: .\templates\wagtailadmin\pages\list.html:96 +msgid "Enable ordering of child pages" +msgstr "開啟子頁面排序" + +#: .\templates\wagtailadmin\pages\list.html:149 +msgid "Drag" +msgstr "拖曳" + +#: .\templates\wagtailadmin\pages\list.html:220 +#: .\templates\wagtailadmin\pages\list.html:224 +#, python-format +msgid "Explorer subpages of '%(title)s'" +msgstr "瀏覽 '%(title)s' 的子頁面" + +#: .\templates\wagtailadmin\pages\list.html:220 +#: .\templates\wagtailadmin\pages\list.html:224 +#: .\templates\wagtailadmin\pages\list.html:228 +msgid "Explore" +msgstr "瀏覽" + +#: .\templates\wagtailadmin\pages\list.html:228 +#, python-format +msgid "Explorer child pages of '%(title)s'" +msgstr "瀏覽 '%(title)s' 的子頁面" + +#: .\templates\wagtailadmin\pages\list.html:230 +#, python-format +msgid "Add a child page to '%(title)s'" +msgstr "新增子頁面至 '%(title)s'" + +#: .\templates\wagtailadmin\pages\list.html:230 +msgid "Add subpage" +msgstr "新增子頁面" + +#: .\templates\wagtailadmin\pages\list.html:239 +msgid "No pages have been created." +msgstr "沒有已儲存的頁面" + +#: .\templates\wagtailadmin\pages\list.html:239 +#, python-format +msgid "Why not add one?" +msgstr "為什麼不 新增一個頁面呢?" + +#: .\templates\wagtailadmin\pages\move_choose_destination.html:3 +#, python-format +msgid "Select a new parent page for %(title)s" +msgstr "為 %(title)s 選擇一個新的母頁面" + +#: .\templates\wagtailadmin\pages\move_choose_destination.html:7 +#, python-format +msgid "Select a new parent page for %(title)s" +msgstr "為 %(title)s 選擇一個新的母頁面" + +#: .\templates\wagtailadmin\pages\search_results.html:6 +#, python-format +msgid "" +"\n" +" There is one match\n" +" " +msgid_plural "" +"\n" +" There are %(counter)s matches\n" +" " +msgstr[0] "\n 有一個符合" +msgstr[1] "\n 有 $(counter)s 個符合" + +#: .\templates\wagtailadmin\pages\search_results.html:17 +#, python-format +msgid "" +"\n" +" Page %(page_number)s of %(num_pages)s.\n" +" " +msgstr "\n 第 %(page_number)s / %(num_pages)s頁。\n " + +#: .\templates\wagtailadmin\pages\search_results.html:24 +#: .\templates\wagtailadmin\pages\search_results.html:26 +#: .\templates\wagtailadmin\shared\pagination_nav.html:8 +#: .\templates\wagtailadmin\shared\pagination_nav.html:10 +#: .\templates\wagtailadmin\shared\pagination_nav.html:14 +msgid "Previous" +msgstr "往前" + +#: .\templates\wagtailadmin\pages\search_results.html:33 +#: .\templates\wagtailadmin\pages\search_results.html:35 +#: .\templates\wagtailadmin\shared\pagination_nav.html:21 +#: .\templates\wagtailadmin\shared\pagination_nav.html:23 +#: .\templates\wagtailadmin\shared\pagination_nav.html:25 +msgid "Next" +msgstr "往後" + +#: .\templates\wagtailadmin\pages\search_results.html:43 +#, python-format +msgid "Sorry, no pages match \"%(query_string)s\"" +msgstr "對不起,沒有任何頁面符合 \"%(query_string)s\"" + +#: .\templates\wagtailadmin\pages\search_results.html:45 +msgid "Enter a search term above" +msgstr "請輸入關鍵字" + +#: .\templates\wagtailadmin\pages\select_location.html:3 +#, python-format +msgid "Where do you want to create a %(page_type)s" +msgstr "你想在哪建立 %(page_type)s" + +#: .\templates\wagtailadmin\pages\select_location.html:5 +msgid "Where do you want to create this" +msgstr "你想在哪建立這個" + +#: .\templates\wagtailadmin\pages\select_type.html:3 +#: .\templates\wagtailadmin\pages\select_type.html:6 +msgid "Create a new page" +msgstr "建立一個新頁面" + +#: .\templates\wagtailadmin\pages\select_type.html:10 +msgid "" +"Your new page will be saved in the top level of your website. You " +"can move it after saving." +msgstr "你的新頁面將會儲存到網站的 最上層 你可以在儲存後移動它。" + +#: .\templates\wagtailadmin\shared\main_nav.html:15 +msgid "Account settings" +msgstr "帳號設定" + +#: .\templates\wagtailadmin\shared\main_nav.html:16 +msgid "Log out" +msgstr "登出" + +#: .\templates\wagtailadmin\shared\main_nav.html:20 +msgid "More" +msgstr "更多" + +#: .\templates\wagtailadmin\shared\main_nav.html:22 +msgid "Redirects" +msgstr "重導向" + +#: .\templates\wagtailadmin\shared\main_nav.html:23 +msgid "Editors Picks" +msgstr "編者精選" + +#: .\templates\wagtailadmin\shared\pagination_nav.html:3 +#, python-format +msgid "Page %(page_num)s of %(total_pages)s." +msgstr "第 %(page_num)s 頁 共 %(total_pages)s 頁" + +#: .\templatetags\wagtailadmin_nav.py:52 +msgid "Images" +msgstr "圖片" + +#: .\templatetags\wagtailadmin_nav.py:56 +msgid "Documents" +msgstr "文件" + +#: .\templatetags\wagtailadmin_nav.py:61 +msgid "Snippets" +msgstr "片段" + +#: .\templatetags\wagtailadmin_nav.py:66 +msgid "Users" +msgstr "使用者" + +#: .\views\account.py:26 +msgid "Your password has been changed successfully!" +msgstr "您的密碼已經更改成功。" + +#: .\views\pages.py:99 +msgid "Sorry, you do not have access to create a page of type '{0}'." +msgstr "對不起,你沒有建立 '{0}' 類型頁面的權限。" + +#: .\views\pages.py:103 +msgid "" +"Pages of this type can only be created as children of '{0}'. This " +"new page will be saved there." +msgstr "這一類的頁面只能建立為 '{0}' 的子頁面。此頁面將會儲存在那邊。" + +#: .\views\pages.py:166 +msgid "This slug is already in use" +msgstr "這個地址已被使用" + +#: .\views\pages.py:187 .\views\pages.py:254 .\views\pages.py:589 +msgid "Page '{0}' published." +msgstr "第 '{0}' 頁已發佈。" + +#: .\views\pages.py:189 .\views\pages.py:256 +msgid "Page '{0}' submitted for moderation." +msgstr "第 '{0}' 頁已送審。" + +#: .\views\pages.py:192 +msgid "Page '{0}' created." +msgstr "第 '{0}' 頁已建立。" + +#: .\views\pages.py:201 +msgid "The page could not be created due to errors." +msgstr "這頁面因有錯誤而無法建立。" + +#: .\views\pages.py:259 +msgid "Page '{0}' updated." +msgstr "第 '{0}' 頁已更新" + +#: .\views\pages.py:268 +msgid "The page could not be saved due to validation errors" +msgstr "這頁面因有驗證錯誤而無法儲存。" + +#: .\views\pages.py:280 +msgid "This page is currently awaiting moderation" +msgstr "這頁正等待審核" + +#: .\views\pages.py:298 +msgid "Page '{0}' deleted." +msgstr "第 '{0}' 頁已刪除" + +#: .\views\pages.py:428 +msgid "Page '{0}' unpublished." +msgstr "第 '{0}' 頁已取消發佈" + +#: .\views\pages.py:479 +msgid "Page '{0}' moved." +msgstr "第 '{0}' 頁已移動。" + +#: .\views\pages.py:584 .\views\pages.py:602 .\views\pages.py:621 +msgid "The page '{0}' is not currently awaiting moderation." +msgstr "第 '{0}' 頁目前不需要等待審核。" + +#: .\views\pages.py:608 +msgid "Page '{0}' rejected for publication." +msgstr "第 '{0}' 頁已被拒絕發佈。" diff --git a/wagtail/wagtailadmin/modal_workflow.py b/wagtail/wagtailadmin/modal_workflow.py index a4d86f501..998dfdec3 100644 --- a/wagtail/wagtailadmin/modal_workflow.py +++ b/wagtail/wagtailadmin/modal_workflow.py @@ -23,4 +23,4 @@ def render_modal_workflow(request, html_template, js_template, template_vars={}) response_text = "{%s}" % ','.join(response_keyvars) - return HttpResponse(response_text, mimetype="text/javascript") + return HttpResponse(response_text, content_type="text/javascript") diff --git a/wagtail/wagtailadmin/static/wagtailadmin/js/core.js b/wagtail/wagtailadmin/static/wagtailadmin/js/core.js index e5895e34f..380c3d179 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/js/core.js +++ b/wagtail/wagtailadmin/static/wagtailadmin/js/core.js @@ -128,7 +128,7 @@ $(function(){ $(window.headerSearch.termInput).trigger('focus'); function search () { - var workingClasses = "working icon icon-spinner"; + var workingClasses = "icon-spinner"; $(window.headerSearch.termInput).parent().addClass(workingClasses); search_next_index++; diff --git a/wagtail/wagtailadmin/static/wagtailadmin/js/page-editor.js b/wagtail/wagtailadmin/static/wagtailadmin/js/page-editor.js index 63f80a26a..cbff2c8af 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/js/page-editor.js +++ b/wagtail/wagtailadmin/static/wagtailadmin/js/page-editor.js @@ -1,3 +1,15 @@ +var halloPlugins = { + 'halloformat': {}, + 'halloheadings': {formatBlocks: ["p", "h2", "h3", "h4", "h5"]}, + 'hallolists': {}, + 'hallohr': {}, + 'halloreundo': {}, + 'hallowagtaillink': {}, +}; +function registerHalloPlugin(name, opts) { + halloPlugins[name] = (opts || {}); +} + function makeRichTextEditable(id) { var input = $('#' + id); var richText = $('
    ').html(input.val()); @@ -19,17 +31,7 @@ function makeRichTextEditable(id) { richText.hallo({ toolbar: 'halloToolbarFixed', toolbarcssClass: 'testy', - plugins: { - 'halloformat': {}, - 'halloheadings': {formatBlocks: ["p", "h2", "h3", "h4", "h5"]}, - 'hallolists': {}, - 'hallohr': {}, - 'halloreundo': {}, - 'hallowagtailimage': {}, - 'hallowagtailembeds': {}, - 'hallowagtaillink': {}, - 'hallowagtaildoclink': {} - } + plugins: halloPlugins }).bind('hallomodified', function(event, data) { input.val(data.content); if (!removeStylingPending) { @@ -319,7 +321,7 @@ $(function() { }); /* Set up behaviour of preview button */ - $('#action-preview').click(function() { + $('.action-preview').click(function() { var previewWindow = window.open($(this).data('placeholder'), $(this).data('windowname')); $.ajax({ @@ -349,5 +351,6 @@ $(function() { previewWindow.document.close(); } }); + return false; }); }); diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/dropdowns.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/dropdowns.scss index 4469a49a0..ee618f377 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/dropdowns.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/dropdowns.scss @@ -6,6 +6,15 @@ cursor:pointer; } + input[type=button], input[type=submit], button, .button{ + padding:1em 0; + display:block; + width:100%; + text-align:left; + padding-left:1em; + } + + ul{ @include unlist(); background-color:$color-teal; @@ -25,7 +34,6 @@ } a{ - box-sizing:border-box; white-space: nowrap; position:relative; @@ -55,7 +63,7 @@ a, input[type=submit], input[type=reset], input[type=button], .button, button{ font-size:0.95em; -webkit-font-smoothing: auto; - text-shadow:-1px -1px 1px rgba(0,0,0,0.2); + @include border-radius(0); } label{ @@ -95,16 +103,7 @@ bottom:100%; } - input[type=button], input[type=submit], button{ - padding:1em 0; - @include border-radius(0); - display:block; - width:100%; - text-align:left; - padding-left:1em; - text-shadow:-1px -1px 1px rgba(0,0,0,0.2); - } - + .button{ float:left; @@ -123,6 +122,7 @@ &:before{ width:1em; + font-size:1.2rem; } &:hover{ @@ -132,6 +132,21 @@ &.open > .button + .dropdown-toggle{ background-color:$color-teal-darker; } + + /* Styles for dropdowns which are also buttons e.g page editor */ + &.dropdown-button{ + .dropdown-toggle{ + @include border-radius(0 3px 3px 0); + } + &.open{ + > input[type=button], > input[type=submit], > button, > .button{ + @include border-radius(0 0 3px 3px); + } + .dropdown-toggle{ + @include border-radius(0 0 3px 0); + } + } + } } .dropdown.white{ @@ -211,7 +226,6 @@ footer .actions .dropdown-toggle{ &:before, &:after{ margin:0; - right:1em !important; } } diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/explorer.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/explorer.scss index 31be22efe..8ca6ed98f 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/explorer.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/explorer.scss @@ -54,7 +54,7 @@ $explorer-z-index:500; opacity:0.5; left:1em; margin-right:0.5em; - + font-size:1.5em; } &:hover{ @@ -86,6 +86,7 @@ $explorer-z-index:500; display: block; width: 100%; text-align: center; + font-size:1.7em; } &:hover{ diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/formatters.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/formatters.scss index 569821e10..bc2b02a8c 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/formatters.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/formatters.scss @@ -177,4 +177,9 @@ a.tag:hover{ /* make a block-level element inline */ .inline{ display:inline; +} + +/* utility class to allow things to be scrollable if their contents can't wrap more nicely */ +.overflow{ + overflow:auto; } \ No newline at end of file diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/forms.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/forms.scss index d4842874e..29c44b4f3 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/forms.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/forms.scss @@ -21,22 +21,13 @@ legend{ @include visuallyhidden(); } -.fields li{ - padding-top:0.5em; - padding-bottom:0.5em; -} - -.field{ - padding:0 0 0.6em 0; -} - label{ font-weight:bold; color:$color-grey-1; font-size:1.1em; display:block; padding:0 0 0.8em 0; - line-height:1em; + line-height:1.3em; .checkbox &, .radio &{ @@ -47,10 +38,10 @@ label{ input, textarea, select, .richtext, .tagit{ @include border-radius(6px); @include border-box(); - font-family:Open Sans,Arial,sans-serif; width:100%; - border:1px dashed $color-input-border; - padding:1.2em; + font-family:Open Sans,Arial,sans-serif; + border:1px solid $color-input-border; + padding:0.9em 1.2em; background-color:$color-fieldset-hover; -webkit-appearance: none; color:$color-text-input; @@ -76,10 +67,44 @@ input, textarea, select, .richtext, .tagit{ } } -input[type=radio],input[type=checkbox]{ +/* select boxes */ +.typed_choice_field .input{ + position:relative; + + select{ + outline:none; + } + + &:after{ + @include border-radius(0 6px 6px 0); + z-index:0; + position:absolute; + right:1px; + top:1px; + height:95%; + width:1.5em; + font-family:wagtail; + content:"q"; + border:1px solid $color-input-border; + border-width:0 0 0 1px; + text-align:center; + line-height:1.4em; + font-size:3em; + pointer-events:none; + color:$color-grey-3; + background-color:$color-fieldset-hover; + margin:0px 1px 0 0; + } + + .ie &:after{ + display:none; + } +} + +/* radio and check boxes */ +input[type=radio], input[type=checkbox]{ @include border-radius(0); cursor:pointer; - float:left; border:0; } @@ -142,6 +167,7 @@ input[type=checkbox]:checked:before{ /* Core button style */ input[type=submit], input[type=reset], input[type=button], .button, button{ + font-family:Open Sans,Arial,sans-serif; @include border-radius(3px); width:auto; padding:0.7em 1em; @@ -160,8 +186,9 @@ input[type=submit], input[type=reset], input[type=button], .button, button{ overflow:hidden; position:relative; font-weight:normal; + outline:none; -moz-appearance: none; - -moz-box-sizing:content-box; + -moz-box-sizing:border-box; &.button-small{ padding:0.55em 0.8em; @@ -169,15 +196,10 @@ input[type=submit], input[type=reset], input[type=button], .button, button{ } &.button-secondary{ - border:1px solid $color-button; color:$color-button; background-color:white; } - &.icon:before{ - font-size:1.5em; - } - &.icon.text-replace:before{ font-size:auto; } @@ -257,6 +279,13 @@ input[type=submit], input[type=reset], input[type=button], .button, button{ } } +button.icon{ + &:before, + &:after{ + line-height:0; + } +} + .multiple{ @include transition(max-height 10s ease); padding:0; @@ -346,18 +375,15 @@ input[type=submit], input[type=reset], input[type=button], .button, button{ .help, .error-message{ font-size:0.85em; font-weight:normal; - margin:0 0 0.5em 0; + margin:0.5em 0 0 0; +} +.error-message{ + color:$color-red; } .help{ color:$color-grey-2; } -/* permanently show checkbox/radio help as they have no focus state */ -.boolean_field .help, .radio .help{ - opacity:1; -} - - fieldset:hover > .help, .field.focused + .help, .field:focus + .help, @@ -376,18 +402,77 @@ li.focused > .help{ font-size:13px; } -.error-message{ - margin:0; - color:$color-red; - clear:both; -} - .error input, .error textarea, .error select, .error .tagit{ border-color:$color-red; background-color:$color-input-error-bg; } +/* Layouts for particular kinds of of fields */ + +/* permanently show checkbox/radio help as they have no focus state */ +.boolean_field .help, .radio .help{ + opacity:1; +} +.iconfield { + position:relative; + + input:not([type=radio]), input:not([type=checkbox]), input:not([type=submit]), input:not([type=button]){ + padding-left:2.5em; + } + + &:before, &:after{ + font-family:wagtail; + position:absolute; + top:0.4em; + font-size:1.4em; + color:$color-grey-3; + } + &:before{ + left:0.5em; + } + &:after{ + right:0.5em; + } + + /* special case for search spinners */ + &.icon-spinner:after{ + color:$color-teal; + opacity:0.8; + font-size:20px; + width:20px; + height:20px; + line-height:23px; + text-align:center; + top:0.3em; + + } +} + +.fields li{ + padding-top:0.5em; + padding-bottom:1.2em; +} + +.field-content .input li{ + label{ + width:auto; + float:none; + } +} + +.input{ + clear:both; +} + /* field sizing */ + +.field-small{ + input, textarea, select, .richtext, .tagit{ + @include border-radius(3px); + padding:0.4em 1em; + } +} + .field{ &.col1, &.col2, @@ -449,7 +534,7 @@ ul.inline li:first-child, li.inline:first-child{ display:block; float:left; color:$color-grey-3; - line-height:0.85em; + line-height:1em; font-size:2.5em; margin-right:0.3em; } @@ -491,7 +576,6 @@ ul.inline li:first-child, li.inline:first-child{ .unchosen, .chosen{ &:before{ content:"b"; - margin-left:-0.1em; /* this glyphs appear to have left padding, counteracted here */ } } } @@ -564,112 +648,6 @@ ul.tagit li.tagit-choice-editable{ } } - -/* search bars (search integrated into header area) */ -.search-bar{ - margin-top:-2em; - padding-top:1em; - padding-bottom:1em; - margin-bottom:2em; - - &.full-width{ - @include nice-padding(); - background-color:$color-header-bg; - border-bottom:1px solid $color-grey-4; - } - - label{ - display:none; - } - - .fields{ - position:relative; - clear:both; - - .field input{ - padding-left:3em; - - &:focus{ - background-color:white; - } - } - .field:before, .field:after{ - font-family:wagtail; - position:absolute; - top:1em; - font-size:25px; - - } - .field:before{ - left:0.5em; - content:"f"; - color:$color-grey-3; - } - .field:after{ - color:$color-teal; - opacity:0.8; - font-size:20px; - width:20px; - height:20px; - line-height:23px; - text-align:center; - top:0.3em; - right:0.5em; - } - } - .submit{ - display:none; - position:absolute; - right:0; - top:0; - input{ - padding:1.55em 2em; - } - } - .taglist{ - font-size:0.9em; - line-height:2.4em; - h3{ - display:inline; - } - a{ - white-space: nowrap - } - } - - &.small{ - margin:0; - padding:0; - .fields{ - li{ - padding:0; - } - .field{ - padding:0; - } - .field input{ - padding:0.4em 1.4em 0.4em 2em; - - &:focus{ - background-color:white; - } - } - .field:before{ - font-size:1.1rem; - top:0.45em; - } - - } - } -} - -/* mozilla specific hack */ -@-moz-document url-prefix() { - .search-bar .fields .field:after{ - line-height:20px; - } -} - /* Transitions */ fieldset, input, textarea, select{ @include transition(background-color 0.2s ease); @@ -682,11 +660,22 @@ input[type=submit], input[type=reset], input[type=button], .button, button{ } @media screen and (min-width: $breakpoint-mobile){ - .help{ - opacity:1; - } - .fields{ - max-width:800px; + label{ + @include column(2); + padding-top:1.2em; + padding-left:0; + + .model_multiple_choice_field &, + .boolean_field &, + .model_choice_field &, + .image_field &, + .file_field &{ + padding-top:0; + } + + .boolean_field &{ + padding-bottom:0; + } } input[type=submit], input[type=reset], input[type=button], .button, button{ @@ -702,4 +691,20 @@ input[type=submit], input[type=reset], input[type=button], .button, button{ } } } + + .help{ + opacity:1; + } + .fields{ + max-width:800px; + } + + .field{ + @include row(); + } + + .field-content{ + @include column(10); + padding-right:0; + } } \ No newline at end of file diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/header.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/header.scss new file mode 100644 index 000000000..849420ae0 --- /dev/null +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/header.scss @@ -0,0 +1,156 @@ +header{ + padding-top:1em; + padding-bottom:1em; + background-color: $color-header-bg; + margin-bottom:2em; + color:white; + + h1, h2{ + margin:0; + color:white; + } + + h1{ + padding:0.2em 0; + + &.icon:before{ + width:1em; + display:none; + margin-right:0.4em; + font-size:1.5em; + } + } + + .col{ + float:left; + margin-right:2em; + } + .left{ + float:left; + + .hasform &:first-child{ + padding-bottom:0.5em; + float:none; + } + } + .right{ + text-align:right; + float:right; + } + + /* For case where content below header should merge with it */ + &.merged{ + margin-bottom:0; + } + &.tab-merged, &.no-border{ + border:0; + } + &.merged.no-border{ + padding-bottom:0; + } + &.no-v-padding{ + padding-top:0; + padding-bottom:0; + } + /* + &.hasform h1{ + margin-top:0.2em; + } + */ + .button{ + background-color:$color-teal-darker; + &:hover{ + background-color:$color-teal-dark; + } + } + + /* necessary on mobile only to make way for hamburger menu */ + &.nice-padding{ + padding-left:4em; + } + + label{ + @include visuallyhidden(); + } + + input[type=text], select{ + border-width:0; + + &:focus{ + background-color:white; + } + } + + .fields{ + margin-top:-0.5em; + li{ + padding-bottom:0; + } + .field{ + padding:0; + } + } + + .field-content{ + width:auto; + padding:0; + } +} + +/* mozilla specific hack */ +@-moz-document url-prefix() { + .iconfield.icon-spinner:after{ + line-height:20px; + } +} + +.page-explorer header{ + margin-bottom:0; + padding-bottom:0em; +} + + +@media screen and (min-width: $breakpoint-mobile){ + header{ + padding-top:1.5em; + padding-bottom:1.5em; + + .left{ + float:left; + margin-right:0; + + &:first-child{ + padding-bottom:0; + float:left; + } + } + .second{ + clear:none; + + .right, .left{ + float:right; + } + } + + h1.icon:before{ + display:inline-block; + } + + .col3{ + @include column(3); + } + .col3.addbutton{ + width:auto; + } + .col6{ + @include column(6); + } + .col9{ + @include column(9); + } + .breadcrumb{ + margin-left:-($desktop-nice-padding); + margin-right:-($desktop-nice-padding); + } + } +} \ No newline at end of file diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/icons.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/icons.scss index b2d5ddd43..4de3aa131 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/icons.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/icons.scss @@ -21,7 +21,6 @@ line-height: 1em; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - font-size:1.8em; text-align:left; vertical-align:middle; margin-right:0.2em; @@ -36,9 +35,7 @@ .hallotoolbar [class*=" icon-"]:before, .hallotoolbar [class*=" icon-"]:before, .hallotoolbar [class^="icon-"]:before{ - font-size:1.1em; vertical-align:-10%; - font-size:1.05em; margin-right:0; } @@ -191,7 +188,7 @@ content:"Q"; } .icon-download:before{ - content:"S"; + content:"S"; /* Credit: Icon made by Freepik from Flaticon.com */ } .icon-order:before{ content:"T"; @@ -225,7 +222,8 @@ content:"3"; } .icon-view:before{ - content:"4"; + content:"4"; /* Credit: Icon made by Zurb from Flaticon.com */ + font-size:1.5rem; } .icon-collapse-down:before{ content:"5"; @@ -233,14 +231,20 @@ .icon-collapse-up:before{ content:"6"; } +.icon-date:before{ + content:"7"; +} +.icon-success:before{ + content:"9"; +} .icon-help:before{ content:"?"; } .icon-warning:before{ content:"!"; } -.icon-success:before{ - content:"9"; +.icon-form:before{ + content:"$"; } .icon.text-replace{ @@ -259,6 +263,10 @@ } } +.icon.icon-larger:before{ + font-size:1.5em; +} + @keyframes spin { from { transform: rotate(0deg); diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/core.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/core.scss index 812fb00ee..e622680bd 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/core.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/core.scss @@ -11,6 +11,7 @@ @import "components/listing.scss"; @import "components/messages.scss"; @import "components/formatters.scss"; +@import "components/header.scss"; @import "fonts.scss"; @@ -115,7 +116,7 @@ img{ } .nav-wrapper{ - @include box-shadow(inset -2px 0px 10px 0px rgba(0, 0, 0, 0.5)); + @include box-shadow(inset -5px 0px 5px -3px rgba(0, 0, 0, 0.3)); position:relative; background: $color-grey-1; margin-left: -100%; @@ -185,6 +186,10 @@ img{ background-color:$color-grey-1; } + a:before{ + font-size:1.2rem; + } + .menu-snippets &.menu-snippets, .menu-users &.menu-users, .menu-snippets &.menu-snippets, @@ -197,20 +202,6 @@ img{ a{ color:white; } - - /* - &:after{ - content: " "; - width: 0px; - height: 0px; - display: block; - position: absolute; - right: 0; - top: 0.9em; - border: 0.7em solid #fff; - border-color: transparent #fff transparent transparent; - } - */ } } @@ -395,88 +386,6 @@ body.explorer-open { } } -header{ - padding-top:1em; - padding-bottom:1em; - background-color: $color-header-bg; - margin-bottom:2em; - color:white; - - h1, h2{ - margin:0; - color:white; - } - - h1{ - padding:0.2em 0; - - &.icon:before{ - width:1em; - display:none; - margin-right:0.4em; - font-size:1.5em; - } - } - - .col{ - float:left; - margin-right:2em; - } - .left{ - float:left; - - .hasform &:first-child{ - padding-bottom:0.5em; - float:none; - } - } - .search-bar input{ - @include border-radius(3px); - width:auto; - border-width:0; - } - .right{ - text-align:right; - float:right; - } - - /* For case where content below header should merge with it */ - &.merged{ - margin-bottom:0; - } - &.tab-merged, &.no-border{ - border:0; - } - &.merged.no-border{ - padding-bottom:0; - } - &.no-v-padding{ - padding-top:0; - padding-bottom:0; - } - /* - &.hasform h1{ - margin-top:0.2em; - } - */ - .button{ - background-color:$color-teal-darker; - &:hover{ - background-color:$color-teal-dark; - } - } - /* necessary on mobile only to make way for hamburger menu */ - &.nice-padding{ - padding-left:4em; - } -} - -.page-explorer header{ - margin-bottom:0; - padding-bottom:0em; -} - - footer{ @include row(); @include border-radius(3px 3px 0 0); @@ -842,52 +751,6 @@ footer, .logo{ } } - header{ - padding-top:1.5em; - padding-bottom:1.5em; - - &.nice-padding{ - @include nice-padding(); - } - - .left{ - float:left; - margin-right:0; - - &:first-child{ - padding-bottom:0; - float:left; - } - } - .second{ - clear:none; - - .right, .left{ - float:right; - } - } - - h1.icon:before{ - display:inline-block; - } - - .col3{ - @include column(3); - } - .col3.addbutton{ - width:auto; - } - .col6{ - @include column(6); - } - .col9{ - @include column(9); - } - .breadcrumb{ - margin-left:-($desktop-nice-padding); - margin-right:-($desktop-nice-padding); - } - } footer{ width:80%; margin-left:50px; @@ -914,13 +777,8 @@ footer, .logo{ .wrapper{ max-width:$breakpoint-desktop-larger; } - .nav-wrapper{ - @include box-shadow(inset -6px 0px 4px 0px rgba(0, 0, 0, 0.2)); - - .inner{ - background:$color-grey-1; - @include box-shadow(inset -6px 0px 4px 0px rgba(0, 0, 0, 0.2)); - } + .nav-wrapper .inner{ + background:$color-grey-1; } footer{ diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/fonts/wagtail.eot b/wagtail/wagtailadmin/static/wagtailadmin/scss/fonts/wagtail.eot index cdce4afab..6ac63514a 100644 Binary files a/wagtail/wagtailadmin/static/wagtailadmin/scss/fonts/wagtail.eot and b/wagtail/wagtailadmin/static/wagtailadmin/scss/fonts/wagtail.eot differ diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/fonts/wagtail.svg b/wagtail/wagtailadmin/static/wagtailadmin/scss/fonts/wagtail.svg index 82af03769..9bded54bf 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/fonts/wagtail.svg +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/fonts/wagtail.svg @@ -51,7 +51,6 @@ - @@ -62,11 +61,14 @@ - + + + + diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/fonts/wagtail.ttf b/wagtail/wagtailadmin/static/wagtailadmin/scss/fonts/wagtail.ttf index 865f2f8b2..2006feefb 100644 Binary files a/wagtail/wagtailadmin/static/wagtailadmin/scss/fonts/wagtail.ttf and b/wagtail/wagtailadmin/static/wagtailadmin/scss/fonts/wagtail.ttf differ diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/fonts/wagtail.woff b/wagtail/wagtailadmin/static/wagtailadmin/scss/fonts/wagtail.woff index cc36da3d4..072c20a16 100644 Binary files a/wagtail/wagtailadmin/static/wagtailadmin/scss/fonts/wagtail.woff and b/wagtail/wagtailadmin/static/wagtailadmin/scss/fonts/wagtail.woff differ diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/login.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/login.scss index a8164272d..3e158e51f 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/login.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/login.scss @@ -82,7 +82,7 @@ form{ .field{ padding:0; } - .field.icon:before{ + .iconfield:before{ display:none; } @@ -168,24 +168,29 @@ form{ font-size:4em; } - .field.icon:before{ - display:inline-block; - position: absolute; - color:$color-grey-4; - border: 2px solid $color-grey-4; - border-radius: 100%; - width: 1em; - padding: 0.3em; - left: $desktop-nice-padding; - margin-top: -1em; - top: 50%; - } - .full{ margin:0px (-$desktop-nice-padding); - input{ - padding-left:($desktop-nice-padding + 50px); + .iconfield{ + &:before{ + display:inline-block; + position: absolute; + color:$color-grey-4; + border: 2px solid $color-grey-4; + border-radius: 100%; + width: 1em; + padding: 0.3em; + left: $desktop-nice-padding; + margin-top: -1.1rem; + top: 50%; + font-size:1.3rem; + } + + input{ + padding-left:($desktop-nice-padding + 50px); + } } + + } } \ No newline at end of file diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/page-editor.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/page-editor.scss index 165ce5a1e..c9a22b3ac 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/page-editor.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/page-editor.scss @@ -148,7 +148,6 @@ display:block; float:none; - .help{ display:none; } @@ -269,8 +268,8 @@ } &.collapsible { - // li.collapsed gets its height from the fieldset only, which is now hidden - // and h2 has position:absolute which doesn't add to it either, so it would be 0 without this + /* li.collapsed gets its height from the fieldset only, which is now hidden + and h2 has position:absolute which doesn't add to it either, so it would be 0 without this */ min-height: 41px; h2{ @@ -306,11 +305,34 @@ } } +footer .preview{ + button, .button{ + background-color:lighten($color-grey-2,10%); + border-color:lighten($color-grey-2,10%); -/* styles applied to an element as it is being reordered */ -.preview{ - padding-top:1em; - padding-bottom:0.5em; + &:hover{ + background-color:$color-grey-2; + border-color:$color-grey-2; + } + } + .dropdown{ + input[type=button], input[type=submit], button, .button{ + background-color:lighten($color-grey-2,10%); + border-color:lighten($color-grey-2,10%); + + &:hover{ + background-color:$color-grey-2; + border-color:$color-grey-2; + } + } + ul, .dropdown-toggle{ + background-color:lighten($color-grey-2,10%); + } + .dropdown-toggle:hover, + &.open > .button + .dropdown-toggle{ + background-color:$color-grey-2; + } + } } @media screen and (min-width: $breakpoint-mobile){ @@ -320,6 +342,10 @@ @include column(10); padding-left:0; padding-right:0; + + fieldset{ + width:100%; + } } .object-help{ @@ -348,6 +374,12 @@ .field{ padding:0; } + .field-content{ + display: block; + float: none; + width: auto; + padding: inherit; + } } .multiple{ @include column(10); @@ -358,5 +390,12 @@ &.empty .add{ margin:0 0 0 -50px; } + + &.single-field label{ + display: block; + float: none; + width: auto; + padding:auto; + } } } diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/userbar.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/userbar.scss index 84b99b537..af9e20195 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/userbar.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/userbar.scss @@ -87,11 +87,13 @@ li, .home{ .action{ @include transition(background-color 0.2s ease, color 0.2s ease); background-color:$color-teal; - color:white; + color:$color-teal; &:before{ margin-right:0.4em; vertical-align:middle; + font-size:1.7em; + color:white; } &:hover{ diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/variables.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/variables.scss index 3533bd876..4c06efec8 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/variables.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/variables.scss @@ -42,7 +42,7 @@ $color-grey-5: #fafafa; $color-thead-bg: $color-grey-5; $color-header-bg: $color-teal; // #ff6a58; $color-fieldset-hover: $color-grey-5; -$color-input-border: $color-grey-3; +$color-input-border: $color-grey-4; $color-input-focus: #f4fcfc; $color-input-error-bg: #feedee; $color-button: $color-teal; diff --git a/wagtail/wagtailadmin/tasks.py b/wagtail/wagtailadmin/tasks.py index 3d6f3e8f1..59779d36b 100644 --- a/wagtail/wagtailadmin/tasks.py +++ b/wagtail/wagtailadmin/tasks.py @@ -85,3 +85,16 @@ def send_notification(page_revision_id, notification, excluded_user_id): # Send email send_mail(email_subject, email_content, from_email, email_addresses) + + +@task +def send_email_task(email_subject, email_content, email_addresses, from_email=None): + if not from_email: + if hasattr(settings, 'WAGTAILADMIN_NOTIFICATION_FROM_EMAIL'): + from_email = settings.WAGTAILADMIN_NOTIFICATION_FROM_EMAIL + elif hasattr(settings, 'DEFAULT_FROM_EMAIL'): + from_email = settings.DEFAULT_FROM_EMAIL + else: + from_email = 'webmaster@localhost' + + send_mail(email_subject, email_content, from_email, email_addresses) diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/account/password_reset/complete.html b/wagtail/wagtailadmin/templates/wagtailadmin/account/password_reset/complete.html index 8a71eda3a..c0cf872e2 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/account/password_reset/complete.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/account/password_reset/complete.html @@ -1,4 +1,4 @@ -{% extends "wagtailadmin/skeleton.html" %} +{% extends "wagtailadmin/admin_base.html" %} {% load compress %} {% load i18n %} {% block titletag %}{% trans "Reset password" %}{% endblock %} diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/account/password_reset/confirm.html b/wagtail/wagtailadmin/templates/wagtailadmin/account/password_reset/confirm.html index 598761587..5aeae5a71 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/account/password_reset/confirm.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/account/password_reset/confirm.html @@ -1,4 +1,4 @@ -{% extends "wagtailadmin/skeleton.html" %} +{% extends "wagtailadmin/admin_base.html" %} {% load compress %} {% load i18n %} {% block titletag %}{% trans "Set your new password" %}{% endblock %} diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/account/password_reset/done.html b/wagtail/wagtailadmin/templates/wagtailadmin/account/password_reset/done.html index 207d88835..cf1088530 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/account/password_reset/done.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/account/password_reset/done.html @@ -1,4 +1,4 @@ -{% extends "wagtailadmin/skeleton.html" %} +{% extends "wagtailadmin/admin_base.html" %} {% load i18n %} {% load compress %} {% block titletag %}{% trans "Reset password" %}{% endblock %} diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/account/password_reset/form.html b/wagtail/wagtailadmin/templates/wagtailadmin/account/password_reset/form.html index f61035aa2..b9a58edb5 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/account/password_reset/form.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/account/password_reset/form.html @@ -1,7 +1,7 @@ -{% extends "wagtailadmin/skeleton.html" %} +{% extends "wagtailadmin/admin_base.html" %} {% load compress %} {% load i18n %} -{% block titletag %}{ trans "Reset password" %}{% endblock %} +{% block titletag %}{% trans "Reset password" %}{% endblock %} {% block bodyclass %}login{% endblock %} {% block extra_css %} diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/chooser/_search_behaviour.js b/wagtail/wagtailadmin/templates/wagtailadmin/chooser/_search_behaviour.js index 9c3f77154..a3fee7722 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/chooser/_search_behaviour.js +++ b/wagtail/wagtailadmin/templates/wagtailadmin/chooser/_search_behaviour.js @@ -1,6 +1,6 @@ -modal.ajaxifyForm($('form.search-bar', modal.body)); +modal.ajaxifyForm($('form.search-form', modal.body)); -var searchUrl = $('form.search-bar', modal.body).attr('action'); +var searchUrl = $('form.search-form', modal.body).attr('action'); function search() { $.ajax({ diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/chooser/_search_results.html b/wagtail/wagtailadmin/templates/wagtailadmin/chooser/_search_results.html index b9a2f0e49..c08c6e216 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/chooser/_search_results.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/chooser/_search_results.html @@ -1,12 +1,7 @@ {% load i18n %} {% if not is_searching %}

    {% trans "Explorer" %}

    - + {% include "wagtailadmin/shared/breadcrumb.html" with page=parent_page %} {% else %}

    diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/edit_handlers/field_panel_field.html b/wagtail/wagtailadmin/templates/wagtailadmin/edit_handlers/field_panel_field.html index 602efa583..472cae0b2 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/edit_handlers/field_panel_field.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/edit_handlers/field_panel_field.html @@ -1,18 +1,23 @@
    - {% if field_type != "boolean_field" %}{{ field.label_tag }}{% endif %} - {% block form_field %} - {{ field }} - {% endblock %} - {% if field_type = "boolean_field" %}{{ field.label_tag }}{% endif %} + {{ field.label_tag }} +
    +
    + {% block form_field %} + {{ field }} + {% endblock %} + +
    + {% if field.help_text %} +

    {{ field.help_text }}

    + {% endif %} + + {% if field.errors %} +

    + {% for error in field.errors %} + {{ error }} + {% endfor %} +

    + {% endif %} +
    -{% if field.errors %} -

    - {% for error in field.errors %} - {{ error }} - {% endfor %} -

    -{% endif %} -{% if field.help_text %} -

    {{ field.help_text }}

    -{% endif %} diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/login.html b/wagtail/wagtailadmin/templates/wagtailadmin/login.html index 9cb28ed86..012dd32f8 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/login.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/login.html @@ -28,15 +28,19 @@
    • -
      +
      {{ form.username.label_tag }} - {{ form.username }} +
      + {{ form.username }} +
    • -
      +
      {{ form.password.label_tag }} - {{ form.password }} +
      + {{ form.password }} +
      {% if show_password_reset %}

      {% trans "Forgotten it?" %}

      diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/pages/_editor_css.html b/wagtail/wagtailadmin/templates/wagtailadmin/pages/_editor_css.html index 9c6e832e8..2e140c89c 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/pages/_editor_css.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/pages/_editor_css.html @@ -4,11 +4,6 @@ CSS declarations to be included on the 'create page' and 'edit page' views {% endcomment %} -{% comment %} - TODO: have a mechanism for sub-apps to specify their own declarations - - ideally wagtailadmin shouldn't have to know anything at all about wagtailimages and friends -{% endcomment %} - {% compress css %} diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/pages/_editor_js.html b/wagtail/wagtailadmin/templates/wagtailadmin/pages/_editor_js.html index 6efaecf12..37d600f07 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/pages/_editor_js.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/pages/_editor_js.html @@ -4,6 +4,11 @@ Javascript declarations to be included on the 'create page' and 'edit page' views {% endcomment %} + {% compress js %} @@ -14,21 +19,9 @@ - - - - {% comment %} - TODO: use the insert_editor_js hook to inject things like image-chooser.js and hallo-wagtailimage.js - from their respective apps such as wagtailimages - - ideally wagtailadmin shouldn't have to know anything at all about wagtailimages. - TODO: a method of injecting these sorts of things on demand when the modal is spawned. - {% endcomment %} - - - {% hook_output 'insert_editor_js' %} @@ -41,19 +34,10 @@ {% comment %} Additional js from widgets media. Allows for custom widgets in admin panel. - Can be used for TODO above (including image-choser.js at wagtailimages) {% endcomment %} {{ edit_handler.form.media.js }} ', + ((settings.STATIC_URL, filename) for filename in js_files) + ) + return js_includes + format_html( + """ + + """, + urlresolvers.reverse('wagtaildocs_chooser') + ) +hooks.register('insert_editor_js', editor_js) diff --git a/wagtail/wagtailembeds/embeds.py b/wagtail/wagtailembeds/embeds.py index c144b39b9..f8ab35f16 100644 --- a/wagtail/wagtailembeds/embeds.py +++ b/wagtail/wagtailembeds/embeds.py @@ -1,5 +1,11 @@ import sys -from importlib import import_module + +try: + from importlib import import_module +except ImportError: + # for Python 2.6, fall back on django.utils.importlib (deprecated as of Django 1.7) + from django.utils.importlib import import_module + from django.conf import settings from datetime import datetime from django.utils import six @@ -71,7 +77,9 @@ def embedly(url, max_width=None, key=None): # Return embed as a dict return { - 'title': oembed['title'], + 'title': oembed['title'] if 'title' in oembed else '', + 'author_name': oembed['author_name'] if 'author_name' in oembed else '', + 'provider_name': oembed['provider_name'] if 'provider_name' in oembed else '', 'type': oembed['type'], 'thumbnail_url': oembed.get('thumbnail_url'), 'width': oembed.get('width'), @@ -108,7 +116,9 @@ def oembed(url, max_width=None): # Return embed as a dict return { - 'title': oembed['title'], + 'title': oembed['title'] if 'title' in oembed else '', + 'author_name': oembed['author_name'] if 'author_name' in oembed else '', + 'provider_name': oembed['provider_name'] if 'provider_name' in oembed else '', 'type': oembed['type'], 'thumbnail_url': oembed.get('thumbnail_url'), 'width': oembed.get('width'), @@ -142,6 +152,17 @@ def get_embed(url, max_width=None, finder=None): finder = get_default_finder() embed_dict = finder(url, max_width) + # Make sure width and height are valid integers before inserting into database + try: + embed_dict['width'] = int(embed_dict['width']) + except (TypeError, ValueError): + embed_dict['width'] = None + + try: + embed_dict['height'] = int(embed_dict['height']) + except (TypeError, ValueError): + embed_dict['height'] = None + # Create database record embed, created = Embed.objects.get_or_create( url=url, diff --git a/wagtail/wagtailembeds/format.py b/wagtail/wagtailembeds/format.py index b08955c2d..453c73641 100644 --- a/wagtail/wagtailembeds/format.py +++ b/wagtail/wagtailembeds/format.py @@ -1,6 +1,7 @@ from __future__ import division # Use true division from django.utils.html import escape +from django.template.loader import render_to_string from wagtail.wagtailembeds import get_embed @@ -15,8 +16,11 @@ def embed_to_frontend_html(url): else: ratio = "0" - # Build html - return '
      %s
      ' % (ratio, embed.html) + # Render template + render_to_string('wagtailembeds/embed_frontend.html', { + 'embed': embed, + 'ratio': ratio, + }) else: return '' except: @@ -28,4 +32,7 @@ def embed_to_editor_html(url): if embed is None: return - return '

      %s

      %s

      ' % (url, escape(embed.title), url, embed.thumbnail_url) + # Render template + return render_to_string('wagtailembeds/embed_editor.html', { + 'embed': embed, + }) diff --git a/wagtail/wagtailembeds/locale/zh_TW/LC_MESSAGES/django.mo b/wagtail/wagtailembeds/locale/zh_TW/LC_MESSAGES/django.mo new file mode 100644 index 000000000..7a51d6e23 Binary files /dev/null and b/wagtail/wagtailembeds/locale/zh_TW/LC_MESSAGES/django.mo differ diff --git a/wagtail/wagtailembeds/locale/zh_TW/LC_MESSAGES/django.po b/wagtail/wagtailembeds/locale/zh_TW/LC_MESSAGES/django.po new file mode 100644 index 000000000..43227b1dc --- /dev/null +++ b/wagtail/wagtailembeds/locale/zh_TW/LC_MESSAGES/django.po @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: Lihan Li , 2014 +msgid "" +msgstr "" +"Project-Id-Version: Wagtail\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2014-02-22 19:56+0200\n" +"PO-Revision-Date: 2014-02-24 17:34+0000\n" +"Last-Translator: wdv4758h \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: zh_TW\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: .\forms.py:11 +msgid "Please enter a valid URL" +msgstr "請輸入有效的 URL" + +#: .\forms.py:15 +msgid "URL" +msgstr "URL" + +#: .\templates\wagtailembeds\chooser\chooser.html:3 +msgid "Insert embed" +msgstr "插入 embed" + +#: .\templates\wagtailembeds\chooser\chooser.html:14 +msgid "Insert" +msgstr "插入" + +#: .\views\chooser.py:34 +msgid "" +"There seems to be a problem with your embedly API key. Please check your " +"settings." +msgstr "embedly API key 有問題。請檢查設定。" + +#: .\views\chooser.py:36 +msgid "Cannot find an embed for this URL." +msgstr "在這個 URL 中無法找到 embed" + +#: .\views\chooser.py:38 +msgid "" +"There seems to be an error with Embedly while trying to embed this URL. " +"Please try again later." +msgstr "" +"在嵌入這個 URL 的時候,Embedly 似乎有錯。" +"請稍候再試." diff --git a/wagtail/wagtailembeds/migrations/0002_auto__add_field_embed_author_name__add_field_embed_provider_name.py b/wagtail/wagtailembeds/migrations/0002_auto__add_field_embed_author_name__add_field_embed_provider_name.py new file mode 100644 index 000000000..7da581d21 --- /dev/null +++ b/wagtail/wagtailembeds/migrations/0002_auto__add_field_embed_author_name__add_field_embed_provider_name.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Embed.author_name' + db.add_column(u'wagtailembeds_embed', 'author_name', + self.gf('django.db.models.fields.TextField')(default='', blank=True), + keep_default=False) + + # Adding field 'Embed.provider_name' + db.add_column(u'wagtailembeds_embed', 'provider_name', + self.gf('django.db.models.fields.TextField')(default='', blank=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Embed.author_name' + db.delete_column(u'wagtailembeds_embed', 'author_name') + + # Deleting field 'Embed.provider_name' + db.delete_column(u'wagtailembeds_embed', 'provider_name') + + + models = { + u'wagtailembeds.embed': { + 'Meta': {'unique_together': "(('url', 'max_width'),)", 'object_name': 'Embed'}, + 'author_name': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'height': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'html': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'max_width': ('django.db.models.fields.SmallIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'provider_name': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'thumbnail_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '10'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'width': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}) + } + } + + complete_apps = ['wagtailembeds'] \ No newline at end of file diff --git a/wagtail/wagtailembeds/models.py b/wagtail/wagtailembeds/models.py index 154ee8676..c147860f0 100644 --- a/wagtail/wagtailembeds/models.py +++ b/wagtail/wagtailembeds/models.py @@ -15,6 +15,8 @@ class Embed(models.Model): type = models.CharField(max_length=10, choices=EMBED_TYPES) html = models.TextField(blank=True) title = models.TextField(blank=True) + author_name = models.TextField(blank=True) + provider_name = models.TextField(blank=True) thumbnail_url = models.URLField(null=True, blank=True) width = models.IntegerField(null=True, blank=True) height = models.IntegerField(null=True, blank=True) diff --git a/wagtail/wagtailembeds/templates/wagtailembeds/chooser/embed_chosen.js b/wagtail/wagtailembeds/templates/wagtailembeds/chooser/embed_chosen.js index d32fce563..af6e0913e 100644 --- a/wagtail/wagtailembeds/templates/wagtailembeds/chooser/embed_chosen.js +++ b/wagtail/wagtailembeds/templates/wagtailembeds/chooser/embed_chosen.js @@ -1,4 +1,4 @@ function(modal) { - modal.respond('embedChosen', '{{ embed_html|safe }}'); + modal.respond('embedChosen', '{{ embed_html|escapejs }}'); modal.close(); } \ No newline at end of file diff --git a/wagtail/wagtailembeds/templates/wagtailembeds/embed_editor.html b/wagtail/wagtailembeds/templates/wagtailembeds/embed_editor.html new file mode 100644 index 000000000..0cbfa2d43 --- /dev/null +++ b/wagtail/wagtailembeds/templates/wagtailembeds/embed_editor.html @@ -0,0 +1,13 @@ +
      +

      {{ embed.title }}

      +

      URL: {{ embed.url }}

      + {% if embed.provider_name %} +

      Provider: {{ embed.provider_name }}

      + {% endif %} + {% if embed.author_name %} +

      Author: {{ embed.author_name }}

      + {% endif %} + {% if embed.thumbnail_url %} + {{ embed.title }} + {% endif %} +
      diff --git a/wagtail/wagtailembeds/templates/wagtailembeds/embed_frontend.html b/wagtail/wagtailembeds/templates/wagtailembeds/embed_frontend.html new file mode 100644 index 000000000..feb209311 --- /dev/null +++ b/wagtail/wagtailembeds/templates/wagtailembeds/embed_frontend.html @@ -0,0 +1,3 @@ +
      + {{ embed.html }} +
      diff --git a/wagtail/wagtailembeds/tests.py b/wagtail/wagtailembeds/tests.py index e3ac82d2b..b1eefdb17 100644 --- a/wagtail/wagtailembeds/tests.py +++ b/wagtail/wagtailembeds/tests.py @@ -8,6 +8,20 @@ class TestEmbeds(TestCase): def setUp(self): self.hit_count = 0 + def dummy_finder(self, url, max_width=None): + # Up hit count + self.hit_count += 1 + + # Return a pretend record + return { + 'title': "Test: " + url, + 'type': 'video', + 'thumbnail_url': '', + 'width': max_width if max_width else 640, + 'height': 480, + 'html': "

      Blah blah blah

      ", + } + def test_get_embed(self): embed = get_embed('www.test.com/1234', max_width=400, finder=self.dummy_finder) @@ -31,20 +45,23 @@ class TestEmbeds(TestCase): embed = get_embed('www.test.com/4321', finder=self.dummy_finder) self.assertEqual(self.hit_count, 3) - def dummy_finder(self, url, max_width=None): - # Up hit count - self.hit_count += 1 - - # Return a pretend record + def dummy_finder_invalid_width(self, url, max_width=None): + # Return a record with an invalid width return { 'title': "Test: " + url, 'type': 'video', 'thumbnail_url': '', - 'width': max_width if max_width else 640, + 'width': '100%', 'height': 480, 'html': "

      Blah blah blah

      ", } + def test_invalid_width(self): + embed = get_embed('www.test.com/1234', max_width=400, finder=self.dummy_finder_invalid_width) + + # Width must be set to None + self.assertEqual(embed.width, None) + class TestChooser(TestCase): def setUp(self): diff --git a/wagtail/wagtailembeds/urls.py b/wagtail/wagtailembeds/urls.py index 157e6b122..40a97bf35 100644 --- a/wagtail/wagtailembeds/urls.py +++ b/wagtail/wagtailembeds/urls.py @@ -1,8 +1,8 @@ -from django.conf.urls import patterns, url +from django.conf.urls import url +from wagtail.wagtailembeds.views import chooser -urlpatterns = patterns( - 'wagtail.wagtailembeds.views', - url(r'^chooser/$', 'chooser.chooser', name='wagtailembeds_chooser'), - url(r'^chooser/upload/$', 'chooser.chooser_upload', name='wagtailembeds_chooser_upload'), -) +urlpatterns = [ + url(r'^chooser/$', chooser.chooser, name='wagtailembeds_chooser'), + url(r'^chooser/upload/$', chooser.chooser_upload, name='wagtailembeds_chooser_upload'), +] diff --git a/wagtail/wagtailembeds/wagtail_hooks.py b/wagtail/wagtailembeds/wagtail_hooks.py new file mode 100644 index 000000000..89c5e66a5 --- /dev/null +++ b/wagtail/wagtailembeds/wagtail_hooks.py @@ -0,0 +1,29 @@ +from django.conf import settings +from django.conf.urls import include, url +from django.core import urlresolvers +from django.utils.html import format_html + +from wagtail.wagtailadmin import hooks +from wagtail.wagtailembeds import urls + + +def register_admin_urls(): + return [ + url(r'^embeds/', include(urls)), + ] +hooks.register('register_admin_urls', register_admin_urls) + + +def editor_js(): + return format_html(""" + + + """, + settings.STATIC_URL, + 'wagtailembeds/js/hallo-plugins/hallo-wagtailembeds.js', + urlresolvers.reverse('wagtailembeds_chooser') + ) +hooks.register('insert_editor_js', editor_js) diff --git a/wagtail/wagtailforms/__init__.py b/wagtail/wagtailforms/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/wagtail/wagtailforms/forms.py b/wagtail/wagtailforms/forms.py new file mode 100644 index 000000000..f17023a77 --- /dev/null +++ b/wagtail/wagtailforms/forms.py @@ -0,0 +1,87 @@ +import django.forms +from django.utils.datastructures import SortedDict + + +class BaseForm(django.forms.Form): + def __init__(self, *args, **kwargs): + kwargs.setdefault('label_suffix', '') + return super(BaseForm, self).__init__(*args, **kwargs) + + +class FormBuilder(): + formfields = SortedDict() + + def __init__(self, fields): + for field in fields: + options = self.get_options(field) + f = getattr(self, "create_"+field.field_type+"_field")(field, options) + self.formfields[field.clean_name] = f + + def get_options(self, field): + options = {} + options['label'] = field.label + options['help_text'] = field.help_text + options['required'] = field.required + options['initial'] = field.default_value + return options + + def create_singleline_field(self, field, options): + # TODO: This is a default value - it may need to be changed + options['max_length'] = 255 + return django.forms.CharField(**options) + + def create_multiline_field(self, field, options): + return django.forms.CharField(widget=django.forms.Textarea, **options) + + def create_date_field(self, field, options): + return django.forms.DateField(**options) + + def create_datetime_field(self, field, options): + return django.forms.DateTimeField(**options) + + def create_email_field(self, field, options): + return django.forms.EmailField(**options) + + def create_url_field(self, field, options): + return django.forms.URLField(**options) + + def create_number_field(self, field, options): + return django.forms.DecimalField(**options) + + def create_dropdown_field(self, field, options): + options['choices'] = map( + lambda x: (x.strip(), x.strip()), + field.choices.split(',') + ) + return django.forms.ChoiceField(**options) + + def create_radio_field(self, field, options): + options['choices'] = map( + lambda x: (x.strip(), x.strip()), + field.choices.split(',') + ) + return django.forms.ChoiceField(widget=django.forms.RadioSelect, **options) + + def create_checkboxes_field(self, field, options): + options['choices'] = [(x.strip(), x.strip()) for x in field.choices.split(',')] + options['initial'] = [x.strip() for x in field.default_value.split(',')] + return django.forms.MultipleChoiceField( + widget=django.forms.CheckboxSelectMultiple, **options + ) + + def create_checkbox_field(self, field, options): + return django.forms.BooleanField(**options) + + def get_form_class(self): + return type('WagtailForm', (BaseForm,), self.formfields) + + +class SelectDateForm(django.forms.Form): + date_from = django.forms.DateField( + required=False, + widget=django.forms.DateInput(attrs={'placeholder': 'Date from'}) + ) + date_to = django.forms.DateField( + required=False, + widget=django.forms.DateInput(attrs={'placeholder': 'Date to'}) + ) diff --git a/wagtail/wagtailforms/migrations/0001_initial.py b/wagtail/wagtailforms/migrations/0001_initial.py new file mode 100644 index 000000000..80cbdb2b6 --- /dev/null +++ b/wagtail/wagtailforms/migrations/0001_initial.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +from wagtail.wagtailcore.compat import AUTH_USER_MODEL, AUTH_USER_MODEL_NAME + + +class Migration(SchemaMigration): + + depends_on = ( + ("wagtailcore", "0002_initial_data"), + ) + + def forwards(self, orm): + # Adding model 'FormSubmission' + db.create_table(u'wagtailforms_formsubmission', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('form_data', self.gf('django.db.models.fields.TextField')()), + ('page', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['wagtailcore.Page'])), + ('submit_time', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + )) + db.send_create_signal(u'wagtailforms', ['FormSubmission']) + + + def backwards(self, orm): + # Deleting model 'FormSubmission' + db.delete_table(u'wagtailforms_formsubmission') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + AUTH_USER_MODEL: { + 'Meta': {'object_name': AUTH_USER_MODEL_NAME}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'wagtailcore.page': { + 'Meta': {'object_name': 'Page'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': u"orm['contenttypes.ContentType']"}), + 'depth': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'has_unpublished_changes': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'live': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'owned_pages'", 'null': 'True', 'to': u"orm['%s']" % AUTH_USER_MODEL}), + 'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'search_description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'seo_title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'show_in_menus': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'url_path': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}) + }, + u'wagtailforms.formsubmission': { + 'Meta': {'object_name': 'FormSubmission'}, + 'form_data': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'page': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['wagtailcore.Page']"}), + 'submit_time': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) + } + } + + complete_apps = ['wagtailforms'] \ No newline at end of file diff --git a/wagtail/wagtailforms/migrations/__init__.py b/wagtail/wagtailforms/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/wagtail/wagtailforms/models.py b/wagtail/wagtailforms/models.py new file mode 100644 index 000000000..75f0eceee --- /dev/null +++ b/wagtail/wagtailforms/models.py @@ -0,0 +1,200 @@ +from django.db import models +from django.shortcuts import render +from django.utils.translation import ugettext_lazy as _ +from django.utils.text import slugify + +from unidecode import unidecode +import json +import re + +from wagtail.wagtailcore.models import Page, Orderable, UserPagePermissionsProxy, get_page_types +from wagtail.wagtailadmin.edit_handlers import FieldPanel +from wagtail.wagtailadmin import tasks + +from .forms import FormBuilder + + +FORM_FIELD_CHOICES = ( + ('singleline', _('Single line text')), + ('multiline', _('Multi-line text')), + ('email', _('Email')), + ('number', _('Number')), + ('url', _('URL')), + ('checkbox', _('Checkbox')), + ('checkboxes', _('Checkboxes')), + ('dropdown', _('Drop down')), + ('radio', _('Radio buttons')), + ('date', _('Date')), + ('datetime', _('Date/time')), +) + + +HTML_EXTENSION_RE = re.compile(r"(.*)\.html") + + +class FormSubmission(models.Model): + """Data for a Form submission.""" + + form_data = models.TextField() + page = models.ForeignKey(Page) + + submit_time = models.DateTimeField(auto_now_add=True) + + def get_data(self): + return json.loads(self.form_data) + + def __unicode__(self): + return self.form_data + + +class AbstractFormField(Orderable): + """Database Fields required for building a Django Form field.""" + + label = models.CharField( + max_length=255, + help_text=_('The label of the form field') + ) + field_type = models.CharField(max_length=16, choices=FORM_FIELD_CHOICES) + required = models.BooleanField(default=True) + choices = models.CharField( + max_length=512, + blank=True, + help_text=_('Comma seperated list of choices. Only applicable in checkboxes, radio and dropdown.') + ) + default_value = models.CharField( + max_length=255, + blank=True, + help_text=_('Default value. Comma seperated values supported for checkboxes.') + ) + help_text = models.CharField(max_length=255, blank=True) + + @property + def clean_name(self): + # unidecode will return an ascii string while slugify wants a + # unicode string on the other hand, slugify returns a safe-string + # which will be converted to a normal str + return str(slugify(unicode(unidecode(self.label)))) + + panels = [ + FieldPanel('label'), + FieldPanel('help_text'), + FieldPanel('required'), + FieldPanel('field_type', classname="formbuilder-type"), + FieldPanel('choices', classname="formbuilder-choices"), + FieldPanel('default_value', classname="formbuilder-default"), + ] + + class Meta: + abstract = True + ordering = ['sort_order'] + + +_FORM_CONTENT_TYPES = None + +def get_form_types(): + global _FORM_CONTENT_TYPES + if _FORM_CONTENT_TYPES is None: + _FORM_CONTENT_TYPES = [ + ct for ct in get_page_types() + if issubclass(ct.model_class(), AbstractForm) + ] + return _FORM_CONTENT_TYPES + + +def get_forms_for_user(user): + """Return a queryset of form pages that this user is allowed to access the submissions for""" + editable_pages = UserPagePermissionsProxy(user).editable_pages() + return editable_pages.filter(content_type__in=get_form_types()) + + +class AbstractForm(Page): + """A Form Page. Pages implementing a form should inhert from it""" + + form_builder = FormBuilder + is_abstract = True # Don't display me in "Add" + + def __init__(self, *args, **kwargs): + super(AbstractForm, self).__init__(*args, **kwargs) + if not hasattr(self, 'landing_page_template'): + template_wo_ext = re.match(HTML_EXTENSION_RE, self.template).group(1) + self.landing_page_template = template_wo_ext + '_landing.html' + + class Meta: + abstract = True + + def get_form_parameters(self): + return {} + + def process_form_submission(self, form): + # remove csrf_token from form.data + form_data = dict( + i for i in form.data.items() + if i[0] != 'csrfmiddlewaretoken' + ) + + FormSubmission.objects.create( + form_data=json.dumps(form_data), + page=self, + ) + + def serve(self, request): + fb = self.form_builder(self.form_fields.all()) + form_class = fb.get_form_class() + form_params = self.get_form_parameters() + + if request.method == 'POST': + form = form_class(request.POST, **form_params) + + if form.is_valid(): + self.process_form_submission(form) + # If we have a form_processing_backend call its process method + if hasattr(self, 'form_processing_backend'): + form_processor = self.form_processing_backend() + form_processor.process(self, form) + + # render the landing_page + # TODO: It is much better to redirect to it + return render(request, self.landing_page_template, { + 'self': self, + }) + else: + form = form_class(**form_params) + + return render(request, self.template, { + 'self': self, + 'form': form, + }) + + def get_page_modes(self): + return [ + ('form', 'Form'), + ('landing', 'Landing page'), + ] + + def show_as_mode(self, mode): + if mode == 'landing': + return render(self.dummy_request(), self.landing_page_template, { + 'self': self, + }) + else: + return super(AbstractForm, self).show_as_mode(mode) + + +class AbstractEmailForm(AbstractForm): + """A Form Page that sends email. Pages implementing a form to be send to an email should inherit from it""" + is_abstract = True # Don't display me in "Add" + + to_address = models.CharField(max_length=255, blank=True, help_text=_("Optional - form submissions will be emailed to this address")) + from_address = models.CharField(max_length=255, blank=True) + subject = models.CharField(max_length=255, blank=True) + + def process_form_submission(self, form): + super(AbstractEmailForm, self).process_form_submission(form) + + if self.to_address: + content = '\n'.join([x[1].label + ': ' + form.data.get(x[0]) for x in form.fields.items()]) + tasks.send_email_task.delay(self.subject, content, [self.to_address], self.from_address,) + + + class Meta: + abstract = True diff --git a/wagtail/wagtailforms/static/wagtailforms/js/page-editor.js b/wagtail/wagtailforms/static/wagtailforms/js/page-editor.js new file mode 100644 index 000000000..8e7398133 --- /dev/null +++ b/wagtail/wagtailforms/static/wagtailforms/js/page-editor.js @@ -0,0 +1,3 @@ +$(function(){ + +}); \ No newline at end of file diff --git a/wagtail/wagtailforms/templates/wagtailforms/index.html b/wagtail/wagtailforms/templates/wagtailforms/index.html new file mode 100644 index 000000000..c5ccc1aa4 --- /dev/null +++ b/wagtail/wagtailforms/templates/wagtailforms/index.html @@ -0,0 +1,15 @@ +{% extends "wagtailadmin/base.html" %} +{% load i18n %} +{% block titletag %}{% trans "Forms" %}{% endblock %} +{% block bodyclass %}menu-forms{% endblock %} +{% block content %} + {% trans "Forms" as forms_str %} + {% trans "Pages" as select_form_str %} + {% include "wagtailadmin/shared/header.html" with title=forms_str subtitle=select_form_str icon="form" %} + +
      +
      + {% include "wagtailforms/results_forms.html" %} +
      +
      +{% endblock %} diff --git a/wagtail/wagtailforms/templates/wagtailforms/index_submissions.html b/wagtail/wagtailforms/templates/wagtailforms/index_submissions.html new file mode 100644 index 000000000..a1b45f6d0 --- /dev/null +++ b/wagtail/wagtailforms/templates/wagtailforms/index_submissions.html @@ -0,0 +1,68 @@ +{% extends "wagtailadmin/base.html" %} +{% load i18n %} +{% load localize %} +{% block titletag %}{% blocktrans with form_title=form_page.title|capfirst %}Submissions of {{ form_title }}{% endblocktrans %}{% endblock %} +{% block bodyclass %}menu-snippets{% endblock %} +{% block extra_js %} + {% get_localized_datepicker_js %} + {% get_date_format_override as format_override %} + + +{% endblock %} +{% block content %} +
      +
      +
      +
      +
      +

      + {% blocktrans with form_title=form_page.title|capfirst %}Form data {{ form_title }}{% endblocktrans %} +

      +
      + +
      +
      + +
      +
      +
      +
      +
      + {% if submissions %} + {% include "wagtailforms/list_submissions.html" %} + + {% include "wagtailadmin/shared/pagination_nav.html" with items=submissions is_searching=False linkurl='-' %} + {# Here we pass an invalid non-empty URL name as linkurl to generate pagination links with the URL path omitted #} + {% else %} +

      {% blocktrans with title=form_page.title %}There have been no submissions of the '{{ title }}' form.{% endblocktrans %}

      + {% endif %} +
      +{% endblock %} diff --git a/wagtail/wagtailforms/templates/wagtailforms/list_forms.html b/wagtail/wagtailforms/templates/wagtailforms/list_forms.html new file mode 100644 index 000000000..b1958f5b2 --- /dev/null +++ b/wagtail/wagtailforms/templates/wagtailforms/list_forms.html @@ -0,0 +1,23 @@ +{% load i18n %} + + + + + + + + + + + {% for fp in form_pages %} + + + + + {% endfor %} + +
      {% trans "Title" %}{% trans "Origin" %}
      +

      {{ fp|capfirst }}

      +
      + {{ fp.content_type.name |capfirst }} ({{ fp.content_type.app_label }}.{{ fp.content_type.model }}) +
      \ No newline at end of file diff --git a/wagtail/wagtailforms/templates/wagtailforms/list_submissions.html b/wagtail/wagtailforms/templates/wagtailforms/list_submissions.html new file mode 100644 index 000000000..38b4185c7 --- /dev/null +++ b/wagtail/wagtailforms/templates/wagtailforms/list_submissions.html @@ -0,0 +1,27 @@ +{% load i18n %} +
      + + + + + + + + {% for heading in data_headings %} + + {% endfor %} + + + + {% for row in data_rows %} + + {% for cell in row %} + + {% endfor %} + + {% endfor %} + +
      {% trans "Submission Date" %}{{ heading }}
      + {{ cell }} +
      +
      diff --git a/wagtail/wagtailforms/templates/wagtailforms/results_forms.html b/wagtail/wagtailforms/templates/wagtailforms/results_forms.html new file mode 100644 index 000000000..6cf198553 --- /dev/null +++ b/wagtail/wagtailforms/templates/wagtailforms/results_forms.html @@ -0,0 +1,8 @@ +{% load i18n %} +{% if form_pages %} + {% include "wagtailforms/list_forms.html" %} + + {% include "wagtailadmin/shared/pagination_nav.html" with items=form_pages linkurl="wagtailforms_index" %} +{% else %} +

      {% trans "No form pages have been created." %}

      +{% endif %} diff --git a/wagtail/wagtailforms/tests.py b/wagtail/wagtailforms/tests.py new file mode 100644 index 000000000..8ec2ee5c3 --- /dev/null +++ b/wagtail/wagtailforms/tests.py @@ -0,0 +1,63 @@ +from django.test import TestCase + +from wagtail.wagtailcore.models import Page +from wagtail.wagtailforms.models import FormSubmission + +class TestFormSubmission(TestCase): + fixtures = ['test.json'] + + def test_get_form(self): + response = self.client.get('/contact-us/') + self.assertContains(response, """""") + self.assertNotContains(response, "Thank you for your feedback") + + def test_post_invalid_form(self): + response = self.client.post('/contact-us/', { + 'your-email': 'bob', 'your-message': 'hello world' + }) + self.assertNotContains(response, "Thank you for your feedback") + self.assertContains(response, "Enter a valid email address.") + + def test_post_valid_form(self): + response = self.client.post('/contact-us/', { + 'your-email': 'bob@example.com', 'your-message': 'hello world' + }) + self.assertNotContains(response, "Your email") + self.assertContains(response, "Thank you for your feedback") + + form_page = Page.objects.get(url_path='/home/contact-us/') + + self.assertTrue(FormSubmission.objects.filter(page=form_page, form_data__contains='hello world').exists()) + + +class TestFormsBackend(TestCase): + fixtures = ['test.json'] + + def test_cannot_see_forms_without_permission(self): + form_page = Page.objects.get(url_path='/home/contact-us/') + + self.client.login(username='eventeditor', password='password') + response = self.client.get('/admin/forms/') + self.assertFalse(form_page in response.context['form_pages']) + + def test_can_see_forms_with_permission(self): + form_page = Page.objects.get(url_path='/home/contact-us/') + + self.client.login(username='siteeditor', password='password') + response = self.client.get('/admin/forms/') + self.assertTrue(form_page in response.context['form_pages']) + + def test_can_get_submissions(self): + form_page = Page.objects.get(url_path='/home/contact-us/') + + self.client.login(username='siteeditor', password='password') + + response = self.client.get('/admin/forms/submissions/%d/' % form_page.id) + self.assertEqual(len(response.context['data_rows']), 2) + + response = self.client.get('/admin/forms/submissions/%d/?date_from=01%%2F01%%2F2014' % form_page.id) + self.assertEqual(len(response.context['data_rows']), 1) + + response = self.client.get('/admin/forms/submissions/%d/?date_from=01%%2F01%%2F2014&action=CSV' % form_page.id) + data_line = response.content.split("\n")[1] + self.assertTrue('new@example.com' in data_line) diff --git a/wagtail/wagtailforms/urls.py b/wagtail/wagtailforms/urls.py new file mode 100644 index 000000000..e3f6bf6a8 --- /dev/null +++ b/wagtail/wagtailforms/urls.py @@ -0,0 +1,9 @@ +from django.conf.urls import patterns, url + + +urlpatterns = patterns( + 'wagtail.wagtailforms.views', + url(r'^$', 'index', name='wagtailforms_index'), + url(r'^submissions/(\d+)/$', 'list_submissions', name='wagtailforms_list_submissions'), + +) diff --git a/wagtail/wagtailforms/views.py b/wagtail/wagtailforms/views.py new file mode 100644 index 000000000..2174642c6 --- /dev/null +++ b/wagtail/wagtailforms/views.py @@ -0,0 +1,104 @@ +import datetime +import unicodecsv + +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger +from django.core.exceptions import PermissionDenied +from django.http import HttpResponse +from django.shortcuts import get_object_or_404, render +from django.contrib.auth.decorators import permission_required + +from wagtail.wagtailcore.models import Page +from wagtail.wagtailforms.models import FormSubmission, get_forms_for_user +from wagtail.wagtailforms.forms import SelectDateForm + + +@permission_required('wagtailadmin.access_admin') +def index(request): + p = request.GET.get("p", 1) + + form_pages = get_forms_for_user(request.user) + + paginator = Paginator(form_pages, 20) + + try: + form_pages = paginator.page(p) + except PageNotAnInteger: + form_pages = paginator.page(1) + except EmptyPage: + form_pages = paginator.page(paginator.num_pages) + + return render(request, 'wagtailforms/index.html', { + 'form_pages': form_pages, + }) + + +@permission_required('wagtailadmin.access_admin') +def list_submissions(request, page_id): + form_page = get_object_or_404(Page, id=page_id).specific + + if not get_forms_for_user(request.user).filter(id=page_id).exists(): + raise PermissionDenied + + data_fields = [ + (field.clean_name, field.label) + for field in form_page.form_fields.all() + ] + + submissions = FormSubmission.objects.filter(page=form_page) + + select_date_form = SelectDateForm(request.GET) + if select_date_form.is_valid(): + date_from = select_date_form.cleaned_data.get('date_from') + date_to = select_date_form.cleaned_data.get('date_to') + # careful: date_to should be increased by 1 day since the submit_time + # is a time so it will always be greater + if date_to: + date_to += datetime.timedelta(days=1) + if date_from and date_to: + submissions = submissions.filter(submit_time__range=[date_from, date_to]) + elif date_from and not date_to: + submissions = submissions.filter(submit_time__gte=date_from) + elif not date_from and date_to: + submissions = submissions.filter(submit_time__lte=date_to) + + if request.GET.get('action') == 'CSV': + # return a CSV instead + response = HttpResponse(content_type='text/csv; charset=utf-8') + response['Content-Disposition'] = 'attachment;filename=export.csv' + writer = unicodecsv.writer(response, encoding='utf-8') + + header_row = ['Submission date'] + [label for name, label in data_fields] + + writer.writerow(header_row) + for s in submissions: + data_row = [s.submit_time] + form_data = s.get_data() + for name, label in data_fields: + data_row.append(form_data.get(name)) + writer.writerow(data_row) + return response + + p = request.GET.get('p', 1) + paginator = Paginator(submissions, 20) + + try: + submissions = paginator.page(p) + except PageNotAnInteger: + submissions = paginator.page(1) + except EmptyPage: + submissions = paginator.page(paginator.num_pages) + + data_headings = [label for name, label in data_fields] + data_rows = [] + for s in submissions: + form_data = s.get_data() + data_row = [s.submit_time] + [form_data.get(name) for name, label in data_fields] + data_rows.append(data_row) + + return render(request, 'wagtailforms/index_submissions.html', { + 'form_page': form_page, + 'select_date_form': select_date_form, + 'submissions': submissions, + 'data_headings': data_headings, + 'data_rows': data_rows + }) diff --git a/wagtail/wagtailforms/wagtail_hooks.py b/wagtail/wagtailforms/wagtail_hooks.py new file mode 100644 index 000000000..277fc6e73 --- /dev/null +++ b/wagtail/wagtailforms/wagtail_hooks.py @@ -0,0 +1,28 @@ +from django.core import urlresolvers +from django.conf import settings +from django.conf.urls import include, url +from django.utils.translation import ugettext_lazy as _ + +from wagtail.wagtailadmin import hooks +from wagtail.wagtailadmin.menu import MenuItem + +from wagtail.wagtailforms import urls +from wagtail.wagtailforms.models import get_forms_for_user + +def register_admin_urls(): + return [ + url(r'^forms/', include(urls)), + ] +hooks.register('register_admin_urls', register_admin_urls) + +def construct_main_menu(request, menu_items): + # show this only if the user has permission to retrieve submissions for at least one form + if get_forms_for_user(request.user).exists(): + menu_items.append( + MenuItem(_('Forms'), urlresolvers.reverse('wagtailforms_index'), classnames='icon icon-form', order=700) + ) +hooks.register('construct_main_menu', construct_main_menu) + +def editor_js(): + return """""" % settings.STATIC_URL +hooks.register('insert_editor_js', editor_js) diff --git a/wagtail/wagtailimages/backends/__init__.py b/wagtail/wagtailimages/backends/__init__.py index 9d6b9590c..62d0c1c78 100644 --- a/wagtail/wagtailimages/backends/__init__.py +++ b/wagtail/wagtailimages/backends/__init__.py @@ -2,7 +2,12 @@ # Based on the Django cache framework and wagtailsearch # https://github.com/django/django/blob/5d263dee304fdaf95e18d2f0619d6925984a7f02/django/core/cache/__init__.py -from importlib import import_module +try: + from importlib import import_module +except ImportError: + # for Python 2.6, fall back on django.utils.importlib (deprecated as of Django 1.7) + from django.utils.importlib import import_module + from django.utils import six import sys from django.conf import settings diff --git a/wagtail/wagtailimages/formats.py b/wagtail/wagtailimages/formats.py index b45797148..d00b65b44 100644 --- a/wagtail/wagtailimages/formats.py +++ b/wagtail/wagtailimages/formats.py @@ -1,7 +1,12 @@ from django.conf import settings -from django.utils.importlib import import_module from django.utils.html import escape +try: + from importlib import import_module +except ImportError: + # for Python 2.6, fall back on django.utils.importlib (deprecated as of Django 1.7) + from django.utils.importlib import import_module + class Format(object): def __init__(self, name, label, classnames, filter_spec): diff --git a/wagtail/wagtailimages/locale/zh_TW/LC_MESSAGES/django.mo b/wagtail/wagtailimages/locale/zh_TW/LC_MESSAGES/django.mo new file mode 100644 index 000000000..284cefcb2 Binary files /dev/null and b/wagtail/wagtailimages/locale/zh_TW/LC_MESSAGES/django.mo differ diff --git a/wagtail/wagtailimages/locale/zh_TW/LC_MESSAGES/django.po b/wagtail/wagtailimages/locale/zh_TW/LC_MESSAGES/django.po new file mode 100644 index 000000000..5db580a36 --- /dev/null +++ b/wagtail/wagtailimages/locale/zh_TW/LC_MESSAGES/django.po @@ -0,0 +1,170 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Wagtail\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2014-03-14 23:02+0200\n" +"PO-Revision-Date: 2014-03-14 21:12+0000\n" +"Last-Translator: wdv4758h \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: zh_TW\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: .\models.py:22 +msgid "Title" +msgstr "標題" + +#: .\models.py:39 +msgid "Not a valid image format. Please use a gif, jpeg or png file instead." +msgstr "不是有效的圖片格式。請用 gif、jpeg 或者 png 格式的圖片" + +#: .\models.py:41 +msgid "File" +msgstr "文件" + +#: .\models.py:47 +msgid "Tags" +msgstr "標籤" + +#: .\templates\wagtailimages\chooser\chooser.html:3 +#: .\templates\wagtailimages\edit_handlers\image_chooser_panel.html:19 +msgid "Choose an image" +msgstr "選擇一個圖片" + +#: .\templates\wagtailimages\chooser\chooser.html:8 +#: .\templates\wagtailimages\chooser\chooser.html:20 +msgid "Search" +msgstr "搜尋" + +#: .\templates\wagtailimages\chooser\chooser.html:9 +#: .\templates\wagtailimages\chooser\chooser.html:43 +msgid "Upload" +msgstr "上傳" + +#: .\templates\wagtailimages\chooser\chooser.html:23 +msgid "Popular tags" +msgstr "熱門的標籤" + +#: .\templates\wagtailimages\chooser\results.html:6 +#: .\templates\wagtailimages\images\results.html:6 +#, python-format +msgid "" +"\n" +" There is one match\n" +" " +msgid_plural "" +"\n" +" There are %(counter)s matches\n" +" " +msgstr[0] "\n 有一個符合" +msgstr[1] "\n 有 $(counter)s 個符合" + +#: .\templates\wagtailimages\chooser\results.html:13 +#: .\templates\wagtailimages\images\results.html:13 +msgid "Latest images" +msgstr "最新圖片" + +#: .\templates\wagtailimages\chooser\select_format.html:3 +msgid "Choose a format" +msgstr "選擇一個格式" + +#: .\templates\wagtailimages\chooser\select_format.html:17 +msgid "Insert image" +msgstr "插入圖片" + +#: .\templates\wagtailimages\edit_handlers\image_chooser_panel.html:17 +msgid "Clear image" +msgstr "清除圖片" + +#: .\templates\wagtailimages\edit_handlers\image_chooser_panel.html:18 +msgid "Choose another image" +msgstr "選擇另外一個圖片" + +#: .\templates\wagtailimages\images\_file_field.html:6 +msgid "Change image:" +msgstr "更改圖片:" + +#: .\templates\wagtailimages\images\add.html:4 +#: .\templates\wagtailimages\images\index.html:19 +msgid "Add an image" +msgstr "新增一個圖片" + +#: .\templates\wagtailimages\images\add.html:15 +msgid "Add image" +msgstr "新增圖片" + +#: .\templates\wagtailimages\images\add.html:25 +#: .\templates\wagtailimages\images\edit.html:33 +msgid "Save" +msgstr "儲存" + +#: .\templates\wagtailimages\images\confirm_delete.html:4 +#: .\templates\wagtailimages\images\confirm_delete.html:8 +#: .\templates\wagtailimages\images\edit.html:33 +msgid "Delete image" +msgstr "刪除圖片" + +#: .\templates\wagtailimages\images\confirm_delete.html:16 +msgid "Are you sure you want to delete this image?" +msgstr "你確定要刪除這個圖片嗎?" + +#: .\templates\wagtailimages\images\confirm_delete.html:19 +msgid "Yes, delete" +msgstr "是的,刪除" + +#: .\templates\wagtailimages\images\edit.html:4 +#, python-format +msgid "Editing image %(title)s" +msgstr "編輯圖片 %(title)s" + +#: .\templates\wagtailimages\images\edit.html:15 +msgid "Editing" +msgstr "編輯" + +#: .\templates\wagtailimages\images\index.html:5 +#: .\templates\wagtailimages\images\index.html:18 +msgid "Images" +msgstr "圖片" + +#: .\templates\wagtailimages\images\results.html:31 +#, python-format +msgid "Sorry, no images match \"%(query_string)s\"" +msgstr "對不起,沒有任何圖片符合 \"%(query_string)s\"" + +#: .\templates\wagtailimages\images\results.html:34 +#, python-format +msgid "" +"You've not uploaded any images. Why not add one now?" +msgstr "沒有任何上傳的圖片。為什麼不 新增一個呢?" + +#: .\views\images.py:29 .\views\images.py:40 +msgid "Search images" +msgstr "搜尋圖片" + +#: .\views\images.py:92 +msgid "Image '{0}' updated." +msgstr "圖片 '{0}' 已更新" + +#: .\views\images.py:95 +msgid "The image could not be saved due to errors." +msgstr "圖片因有錯誤而無法儲存。" + +#: .\views\images.py:114 +msgid "Image '{0}' deleted." +msgstr "圖片 '{0}' 已刪除." + +#: .\views\images.py:132 +msgid "Image '{0}' added." +msgstr "圖片 '{0}' 已加入." + +#: .\views\images.py:135 +msgid "The image could not be created due to errors." +msgstr "圖片因有錯而不能被建立。" diff --git a/wagtail/wagtailimages/models.py b/wagtail/wagtailimages/models.py index f0be0fe4d..24fd1f0bf 100644 --- a/wagtail/wagtailimages/models.py +++ b/wagtail/wagtailimages/models.py @@ -4,7 +4,7 @@ import os.path from taggit.managers import TaggableManager from django.core.files import File -from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist, ValidationError +from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist from django.db import models from django.db.models.signals import pre_delete from django.dispatch.dispatcher import receiver @@ -17,6 +17,8 @@ from unidecode import unidecode from wagtail.wagtailadmin.taggable import TagSearchable from wagtail.wagtailimages.backends import get_image_backend +from .utils import validate_image_format + class AbstractImage(models.Model, TagSearchable): title = models.CharField(max_length=255, verbose_name=_('Title') ) @@ -34,12 +36,7 @@ class AbstractImage(models.Model, TagSearchable): filename = prefix[:-1] + dot + extension return os.path.join(folder_name, filename) - def file_extension_validator(ffile): - extension = ffile.name.split(".")[-1].lower() - if extension not in ["gif", "jpg", "jpeg", "png"]: - raise ValidationError(_("Not a valid image format. Please use a gif, jpeg or png file instead.")) - - file = models.ImageField(verbose_name=_('File'), upload_to=get_upload_to, width_field='width', height_field='height', validators=[file_extension_validator]) + file = models.ImageField(verbose_name=_('File'), upload_to=get_upload_to, width_field='width', height_field='height', validators=[validate_image_format]) width = models.IntegerField(editable=False) height = models.IntegerField(editable=False) created_at = models.DateTimeField(auto_now_add=True) diff --git a/wagtail/wagtailimages/templates/wagtailimages/chooser/chooser.html b/wagtail/wagtailimages/templates/wagtailimages/chooser/chooser.html index 03b4554c8..1fcadb3f9 100644 --- a/wagtail/wagtailimages/templates/wagtailimages/chooser/chooser.html +++ b/wagtail/wagtailimages/templates/wagtailimages/chooser/chooser.html @@ -5,13 +5,13 @@ {% if uploadform %} {% endif %}
      -
      {% if uploadform %} -
      +
      {% csrf_token %}
        diff --git a/wagtail/wagtailimages/tests.py b/wagtail/wagtailimages/tests.py index b668cc608..00d1245e6 100644 --- a/wagtail/wagtailimages/tests.py +++ b/wagtail/wagtailimages/tests.py @@ -4,9 +4,7 @@ from django.contrib.auth.models import User, Group, Permission from django.core.urlresolvers import reverse from django.core.files.uploadedfile import SimpleUploadedFile -import unittest - -from wagtail.tests.utils import login +from wagtail.tests.utils import login, unittest from wagtail.wagtailimages.models import get_image_model from wagtail.wagtailimages.templatetags import image_tags diff --git a/wagtail/wagtailimages/urls.py b/wagtail/wagtailimages/urls.py index 10adaadd2..4fa4efaf9 100644 --- a/wagtail/wagtailimages/urls.py +++ b/wagtail/wagtailimages/urls.py @@ -1,15 +1,14 @@ -from django.conf.urls import patterns, url +from django.conf.urls import url +from wagtail.wagtailimages.views import images, chooser +urlpatterns = [ + url(r'^$', images.index, name='wagtailimages_index'), + url(r'^(\d+)/$', images.edit, name='wagtailimages_edit_image'), + url(r'^(\d+)/delete/$', images.delete, name='wagtailimages_delete_image'), + url(r'^add/$', images.add, name='wagtailimages_add_image'), -urlpatterns = patterns( - 'wagtail.wagtailimages.views', - url(r'^$', 'images.index', name='wagtailimages_index'), - url(r'^(\d+)/$', 'images.edit', name='wagtailimages_edit_image'), - url(r'^(\d+)/delete/$', 'images.delete', name='wagtailimages_delete_image'), - url(r'^add/$', 'images.add', name='wagtailimages_add_image'), - - url(r'^chooser/$', 'chooser.chooser', name='wagtailimages_chooser'), - url(r'^chooser/(\d+)/$', 'chooser.image_chosen', name='wagtailimages_image_chosen'), - url(r'^chooser/upload/$', 'chooser.chooser_upload', name='wagtailimages_chooser_upload'), - url(r'^chooser/(\d+)/select_format/$', 'chooser.chooser_select_format', name='wagtailimages_chooser_select_format'), -) + url(r'^chooser/$', chooser.chooser, name='wagtailimages_chooser'), + url(r'^chooser/(\d+)/$', chooser.image_chosen, name='wagtailimages_image_chosen'), + url(r'^chooser/upload/$', chooser.chooser_upload, name='wagtailimages_chooser_upload'), + url(r'^chooser/(\d+)/select_format/$', chooser.chooser_select_format, name='wagtailimages_chooser_select_format'), +] diff --git a/wagtail/wagtailimages/utils.py b/wagtail/wagtailimages/utils.py new file mode 100644 index 000000000..2f0c0e676 --- /dev/null +++ b/wagtail/wagtailimages/utils.py @@ -0,0 +1,28 @@ +import os + +from PIL import Image + +from django.core.exceptions import ValidationError +from django.utils.translation import ugettext_lazy as _ + + +def validate_image_format(f): + # Check file extension + extension = os.path.splitext(f.name)[1].lower()[1:] + + if extension == 'jpg': + extension = 'jpeg' + + if extension not in ['gif', 'jpeg', 'png']: + raise ValidationError(_("Not a valid image. Please use a gif, jpeg or png file with the correct file extension.")) + + if not f.closed: + # Open image file + file_position = f.tell() + f.seek(0) + image = Image.open(f) + f.seek(file_position) + + # Check that the internal format matches the extension + if image.format.upper() != extension.upper(): + raise ValidationError(_("Not a valid %s image. Please use a gif, jpeg or png file with the correct file extension.") % (extension.upper())) diff --git a/wagtail/wagtailimages/views/chooser.py b/wagtail/wagtailimages/views/chooser.py index 510174c4c..221a75581 100644 --- a/wagtail/wagtailimages/views/chooser.py +++ b/wagtail/wagtailimages/views/chooser.py @@ -113,6 +113,8 @@ def chooser_upload(request): Image = get_image_model() ImageForm = get_image_form() + searchform = SearchForm() + if request.POST: image = Image(uploaded_by_user=request.user) form = ImageForm(request.POST, request.FILES, instance=image) @@ -138,7 +140,7 @@ def chooser_upload(request): return render_modal_workflow( request, 'wagtailimages/chooser/chooser.html', 'wagtailimages/chooser/chooser.js', - {'images': images, 'uploadform': form} + {'images': images, 'uploadform': form, 'searchform': searchform} ) diff --git a/wagtail/wagtailimages/wagtail_hooks.py b/wagtail/wagtailimages/wagtail_hooks.py new file mode 100644 index 000000000..51492d404 --- /dev/null +++ b/wagtail/wagtailimages/wagtail_hooks.py @@ -0,0 +1,45 @@ +from django.conf import settings +from django.conf.urls import include, url +from django.core import urlresolvers +from django.utils.html import format_html, format_html_join +from django.utils.translation import ugettext_lazy as _ + +from wagtail.wagtailadmin import hooks +from wagtail.wagtailadmin.menu import MenuItem + +from wagtail.wagtailimages import urls + + +def register_admin_urls(): + return [ + url(r'^images/', include(urls)), + ] +hooks.register('register_admin_urls', register_admin_urls) + + +def construct_main_menu(request, menu_items): + if request.user.has_perm('wagtailimages.add_image'): + menu_items.append( + MenuItem(_('Images'), urlresolvers.reverse('wagtailimages_index'), classnames='icon icon-image', order=300) + ) +hooks.register('construct_main_menu', construct_main_menu) + + +def editor_js(): + js_files = [ + 'wagtailimages/js/hallo-plugins/hallo-wagtailimage.js', + 'wagtailimages/js/image-chooser.js', + ] + js_includes = format_html_join('\n', '', + ((settings.STATIC_URL, filename) for filename in js_files) + ) + return js_includes + format_html( + """ + + """, + urlresolvers.reverse('wagtailimages_chooser') + ) +hooks.register('insert_editor_js', editor_js) diff --git a/wagtail/wagtailredirects/locale/zh_TW/LC_MESSAGES/django.mo b/wagtail/wagtailredirects/locale/zh_TW/LC_MESSAGES/django.mo new file mode 100644 index 000000000..b5f8e50b3 Binary files /dev/null and b/wagtail/wagtailredirects/locale/zh_TW/LC_MESSAGES/django.mo differ diff --git a/wagtail/wagtailredirects/locale/zh_TW/LC_MESSAGES/django.po b/wagtail/wagtailredirects/locale/zh_TW/LC_MESSAGES/django.po new file mode 100644 index 000000000..0d5079b5c --- /dev/null +++ b/wagtail/wagtailredirects/locale/zh_TW/LC_MESSAGES/django.po @@ -0,0 +1,151 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Wagtail\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2014-03-14 23:02+0200\n" +"PO-Revision-Date: 2014-03-14 21:12+0000\n" +"Last-Translator: wdv4758h \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: zh_TW\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: .\models.py:10 +msgid "Redirect from" +msgstr "重導向起點" + +#: .\models.py:12 +msgid "Permanent" +msgstr "永久" + +#: .\models.py:12 +msgid "" +"Recommended. Permanent redirects ensure search engines forget the old page " +"(the 'Redirect from') and index the new page instead." +msgstr "(推薦) 永久頁面重導向確保搜尋引擎忘記舊頁面(重導向起點),並且改成對新頁面做索引。" + +#: .\models.py:13 +msgid "Redirect to a page" +msgstr "重導向到一個頁面" + +#: .\models.py:14 +msgid "Redirect to any URL" +msgstr "重導向到任何一個 URL" + +#: .\views.py:56 +msgid "Search redirects" +msgstr "搜尋重導向" + +#: .\views.py:69 +msgid "Redirect '{0}' updated." +msgstr "重導向 '{0}' 已更新" + +#: .\views.py:72 +msgid "The redirect could not be saved due to errors." +msgstr "重導向因有錯誤而無法儲存。" + +#: .\views.py:90 +msgid "Redirect '{0}' deleted." +msgstr "重導向 '{0}' 已刪除" + +#: .\views.py:110 +msgid "Redirect '{0} added." +msgstr "重導向 '{0}' 已加入" + +#: .\views.py:113 +msgid "The redirect could not be created due to errors." +msgstr "重導向因有錯誤而無法建立。" + +#: .\templates\wagtailredirects\add.html:3 +#: .\templates\wagtailredirects\add.html:6 +#: .\templates\wagtailredirects\index.html:18 +msgid "Add redirect" +msgstr "新增重導向" + +#: .\templates\wagtailredirects\add.html:14 +#: .\templates\wagtailredirects\edit.html:14 +msgid "Save" +msgstr "儲存" + +#: .\templates\wagtailredirects\confirm_delete.html:4 +#, python-format +msgid "Delete redirect %(title)s" +msgstr "刪除重導向 %(title)s" + +#: .\templates\wagtailredirects\confirm_delete.html:6 +msgid "Delete" +msgstr "刪除" + +#: .\templates\wagtailredirects\confirm_delete.html:10 +msgid "Are you sure you want to delete this redirect?" +msgstr "你確定想刪除這個重導向?" + +#: .\templates\wagtailredirects\confirm_delete.html:13 +msgid "Yes, delete" +msgstr "是的, 刪除" + +#: .\templates\wagtailredirects\edit.html:4 +#, python-format +msgid "Editing %(title)s" +msgstr "編輯 %(title)s" + +#: .\templates\wagtailredirects\edit.html:6 +msgid "Editing" +msgstr "編輯" + +#: .\templates\wagtailredirects\edit.html:15 +msgid "Delete redirect" +msgstr "刪除重導向" + +#: .\templates\wagtailredirects\index.html:3 +#: .\templates\wagtailredirects\index.html:17 +msgid "Redirects" +msgstr "重導向" + +#: .\templates\wagtailredirects\list.html:18 +msgid "To" +msgstr "到" + +#: .\templates\wagtailredirects\list.html:19 +msgid "Type" +msgstr "類型" + +#: .\templates\wagtailredirects\list.html:26 +msgid "Edit this redirect" +msgstr "編輯這個重導向" + +#: .\templates\wagtailredirects\list.html:35 +msgid "primary" +msgstr "主要" + +#: .\templates\wagtailredirects\results.html:5 +#, python-format +msgid "" +"\n" +" There is one match\n" +" " +msgid_plural "" +"\n" +" There are %(counter)s matches\n" +" " +msgstr[0] "\n 有一個符合" +msgstr[1] "\n 有 $(counter)s 個符合" + +#: .\templates\wagtailredirects\results.html:18 +#, python-format +msgid "Sorry, no redirects match \"%(query_string)s\"" +msgstr "對不起,沒有任何重導向符合 \"%(query_string)s\"" + +#: .\templates\wagtailredirects\results.html:21 +#, python-format +msgid "" +"No redirects have been created. Why not add one?" +msgstr "沒有任何已儲存的重導向。為什麼不 新增一個?" diff --git a/wagtail/wagtailredirects/urls.py b/wagtail/wagtailredirects/urls.py index 298065225..1f9d36096 100644 --- a/wagtail/wagtailredirects/urls.py +++ b/wagtail/wagtailredirects/urls.py @@ -1,10 +1,10 @@ -from django.conf.urls import patterns, url +from django.conf.urls import url +from wagtail.wagtailredirects import views -urlpatterns = patterns( - 'wagtail.wagtailredirects.views', - url(r'^$', 'index', name='wagtailredirects_index'), - url(r'^(\d+)/$', 'edit', name='wagtailredirects_edit_redirect'), - url(r'^(\d+)/delete/$', 'delete', name='wagtailredirects_delete_redirect'), - url(r'^add/$', 'add', name='wagtailredirects_add_redirect'), -) +urlpatterns = [ + url(r'^$', views.index, name='wagtailredirects_index'), + url(r'^(\d+)/$', views.edit, name='wagtailredirects_edit_redirect'), + url(r'^(\d+)/delete/$', views.delete, name='wagtailredirects_delete_redirect'), + url(r'^add/$', views.add, name='wagtailredirects_add_redirect'), +] diff --git a/wagtail/wagtailredirects/views.py b/wagtail/wagtailredirects/views.py index 375071c4e..8dfd687f1 100644 --- a/wagtail/wagtailredirects/views.py +++ b/wagtail/wagtailredirects/views.py @@ -109,7 +109,7 @@ def add(request): theredirect.site = request.site theredirect.save() - messages.success(request, _("Redirect '{0} added.").format(theredirect.title)) + messages.success(request, _("Redirect '{0}' added.").format(theredirect.title)) return redirect('wagtailredirects_index') else: messages.error(request, _("The redirect could not be created due to errors.")) diff --git a/wagtail/wagtailredirects/wagtail_hooks.py b/wagtail/wagtailredirects/wagtail_hooks.py new file mode 100644 index 000000000..22ae97320 --- /dev/null +++ b/wagtail/wagtailredirects/wagtail_hooks.py @@ -0,0 +1,11 @@ +from django.conf.urls import include, url + +from wagtail.wagtailadmin import hooks +from wagtail.wagtailredirects import urls + + +def register_admin_urls(): + return [ + url(r'^redirects/', include(urls)), + ] +hooks.register('register_admin_urls', register_admin_urls) diff --git a/wagtail/wagtailsearch/backends/__init__.py b/wagtail/wagtailsearch/backends/__init__.py index adc492ada..5e1b4fc5d 100644 --- a/wagtail/wagtailsearch/backends/__init__.py +++ b/wagtail/wagtailsearch/backends/__init__.py @@ -2,7 +2,12 @@ # Based on the Django cache framework # https://github.com/django/django/blob/5d263dee304fdaf95e18d2f0619d6925984a7f02/django/core/cache/__init__.py -from importlib import import_module +try: + from importlib import import_module +except ImportError: + # for Python 2.6, fall back on django.utils.importlib (deprecated as of Django 1.7) + from django.utils.importlib import import_module + from django.utils import six import sys from django.conf import settings diff --git a/wagtail/wagtailsearch/backends/elasticsearch.py b/wagtail/wagtailsearch/backends/elasticsearch.py index 87926a388..d2b58d564 100644 --- a/wagtail/wagtailsearch/backends/elasticsearch.py +++ b/wagtail/wagtailsearch/backends/elasticsearch.py @@ -1,5 +1,4 @@ from django.db import models -from django.conf import settings from elasticutils import get_es, S @@ -37,7 +36,7 @@ class ElasticSearchResults(object): results = results.prefetch_related(prefetch) # Put results into a dictionary (using primary key as the key) - results_dict = {str(result.pk): result for result in results} + results_dict = dict((str(result.pk), result) for result in results) # Build new list with items in the correct order results_sorted = [results_dict[str(pk)] for pk in pk_list if str(pk) in results_dict] @@ -58,12 +57,23 @@ class ElasticSearch(BaseSearch): super(ElasticSearch, self).__init__(params) # Get settings - self.es_urls = params.get('URLS', ['http://localhost:9200']) - self.es_index = params.get('INDEX', 'wagtail') + self.es_urls = params.pop('URLS', ['http://localhost:9200']) + self.es_index = params.pop('INDEX', 'wagtail') + self.es_timeout = params.pop('TIMEOUT', 5) + self.es_force_new = params.pop('FORCE_NEW', False) # Get ElasticSearch interface - self.es = get_es(urls=self.es_urls) - self.s = S().es(urls=self.es_urls).indexes(self.es_index) + # Any remaining params are passed into the ElasticSearch constructor + self.es = get_es( + urls=self.es_urls, + timeout=self.es_timeout, + force_new=self.es_force_new, + **params) + self.s = S().es( + urls=self.es_urls, + timeout=self.es_timeout, + force_new=self.es_force_new, + **params).indexes(self.es_index) def reset_index(self): # Delete old index diff --git a/wagtail/wagtailsearch/indexed.py b/wagtail/wagtailsearch/indexed.py index 04d8b0a9e..aa750d7b9 100644 --- a/wagtail/wagtailsearch/indexed.py +++ b/wagtail/wagtailsearch/indexed.py @@ -40,7 +40,7 @@ class Indexed(object): if isinstance(indexed_fields, basestring): indexed_fields = [indexed_fields] if isinstance(indexed_fields, list): - indexed_fields = {field: dict(type="string") for field in indexed_fields} + indexed_fields = dict((field, dict(type="string")) for field in indexed_fields) if not isinstance(indexed_fields, dict): raise ValueError() diff --git a/wagtail/wagtailsearch/locale/zh_TW/LC_MESSAGES/django.mo b/wagtail/wagtailsearch/locale/zh_TW/LC_MESSAGES/django.mo new file mode 100644 index 000000000..038c04394 Binary files /dev/null and b/wagtail/wagtailsearch/locale/zh_TW/LC_MESSAGES/django.mo differ diff --git a/wagtail/wagtailsearch/locale/zh_TW/LC_MESSAGES/django.po b/wagtail/wagtailsearch/locale/zh_TW/LC_MESSAGES/django.po new file mode 100644 index 000000000..f322e51b3 --- /dev/null +++ b/wagtail/wagtailsearch/locale/zh_TW/LC_MESSAGES/django.po @@ -0,0 +1,191 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Wagtail\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2014-03-14 23:02+0200\n" +"PO-Revision-Date: 2014-03-14 21:12+0000\n" +"Last-Translator: wdv4758h \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: zh_TW\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: .\forms.py:8 +msgid "Search term(s)/phrase" +msgstr "搜尋關鍵字" + +#: .\forms.py:9 +msgid "" +"Enter the full search string to match. An \n" +" exact match is required for your Editors Picks to be \n" +" displayed, wildcards are NOT allowed." +msgstr "" +"請輸入完整的字串來選擇。\n" +" 輸入必須和你的 '編者精選' 完全一樣 \n" +" (不能使用萬用字元)" + +#: .\templates\wagtailsearch\editorspicks\add.html:3 +#: .\templates\wagtailsearch\editorspicks\add.html:5 +msgid "Add editor's pick" +msgstr "新增編者精選" + +#: .\templates\wagtailsearch\editorspicks\add.html:9 +msgid "" +"\n" +"

        Editors picks are a means of recommending specific pages that might not organically come high up in search results. E.g recommending your primary donation page to a user searching with a less common term like \"giving\".

        \n" +" " +msgstr "\n

        編者精選是用於推薦部份在搜尋排名不高的頁面。 例如用關鍵字推薦使用者到捐獻頁面。

        \n " + +#: .\templates\wagtailsearch\editorspicks\add.html:12 +msgid "" +"\n" +"

        The \"Search term(s)/phrase\" field below must contain the full and exact search for which you wish to provide recommended results, including any misspellings/user error. To help, you can choose from search terms that have been popular with users of your site.

        \n" +" " +msgstr "\n

        下面的關鍵字必須是你希望推薦結果的完整關鍵字,包括任何可能拼錯的詞。 提示,你可以從熱門的關鍵字選擇。

        \n " + +#: .\templates\wagtailsearch\editorspicks\add.html:25 +#: .\templates\wagtailsearch\editorspicks\edit.html:19 +msgid "Save" +msgstr "儲存" + +#: .\templates\wagtailsearch\editorspicks\confirm_delete.html:3 +#, python-format +msgid "Delete %(query)s" +msgstr "刪除 Delete %(query)s" + +#: .\templates\wagtailsearch\editorspicks\confirm_delete.html:5 +#: .\templates\wagtailsearch\editorspicks\edit.html:20 +#: .\templates\wagtailsearch\editorspicks\includes\editorspicks_form.html:6 +msgid "Delete" +msgstr "刪除" + +#: .\templates\wagtailsearch\editorspicks\confirm_delete.html:9 +msgid "" +"Are you sure you want to delete all editors picks for this search term?" +msgstr "你確定想要刪除所有搜尋到的編者精選?" + +#: .\templates\wagtailsearch\editorspicks\confirm_delete.html:12 +msgid "Yes, delete" +msgstr "是的,刪除" + +#: .\templates\wagtailsearch\editorspicks\edit.html:3 +#, python-format +msgid "Editing %(query)s" +msgstr "編輯 %(query)s" + +#: .\templates\wagtailsearch\editorspicks\edit.html:5 +msgid "Editing" +msgstr "編輯" + +#: .\templates\wagtailsearch\editorspicks\index.html:3 +msgid "Search Terms" +msgstr "搜尋關鍵字" + +#: .\templates\wagtailsearch\editorspicks\index.html:17 +msgid "Editor's search picks" +msgstr "編者精選" + +#: .\templates\wagtailsearch\editorspicks\index.html:18 +msgid "Add new editor's pick" +msgstr "新增新的編者精選" + +#: .\templates\wagtailsearch\editorspicks\list.html:8 +msgid "Search term(s)" +msgstr "搜尋關鍵字" + +#: .\templates\wagtailsearch\editorspicks\list.html:9 +msgid "Editors picks" +msgstr "編者精選" + +#: .\templates\wagtailsearch\editorspicks\list.html:10 +#: .\templates\wagtailsearch\queries\chooser\results.html:9 +msgid "Views (past week)" +msgstr "觀看 (上周)" + +#: .\templates\wagtailsearch\editorspicks\list.html:17 +msgid "Edit this pick" +msgstr "編輯這個精選" + +#: .\templates\wagtailsearch\editorspicks\list.html:23 +msgid "None" +msgstr "沒有" + +#: .\templates\wagtailsearch\editorspicks\results.html:5 +#, python-format +msgid "" +"\n" +" There is one match\n" +" " +msgid_plural "" +"\n" +" There are %(counter)s matches\n" +" " +msgstr[0] "\n 有一個符合" +msgstr[1] "\n 有 $(counter)s 個符合" + +#: .\templates\wagtailsearch\editorspicks\results.html:18 +#, python-format +msgid "Sorry, no editor's picks match \"%(query_string)s\"" +msgstr "對不起,沒有任何編者精選符合 \"%(query_string)s\"" + +#: .\templates\wagtailsearch\editorspicks\results.html:21 +#, python-format +msgid "" +"No editor's picks have been created. Why not add one?" +msgstr "沒有任何編者精選。 為什麼不 建立一個呢?" + +#: .\templates\wagtailsearch\editorspicks\includes\editorspicks_form.html:4 +msgid "Move up" +msgstr "往上移動" + +#: .\templates\wagtailsearch\editorspicks\includes\editorspicks_form.html:5 +msgid "Move down" +msgstr "往下移動" + +#: .\templates\wagtailsearch\editorspicks\includes\editorspicks_form.html:10 +msgid "Editors pick" +msgstr "編者精選" + +#: .\templates\wagtailsearch\editorspicks\includes\editorspicks_formset.html:14 +msgid "Add recommended page" +msgstr "新增推薦頁面" + +#: .\templates\wagtailsearch\queries\chooser\chooser.html:2 +msgid "Popular search terms" +msgstr "熱門的關鍵字" + +#: .\templates\wagtailsearch\queries\chooser\chooser.html:10 +msgid "Search" +msgstr "搜尋" + +#: .\templates\wagtailsearch\queries\chooser\results.html:8 +msgid "Terms" +msgstr "關鍵字" + +#: .\templates\wagtailsearch\queries\chooser\results.html:23 +msgid "No results found" +msgstr "沒有找到任何結果" + +#: .\views\editorspicks.py:41 +msgid "Search editor's picks" +msgstr "搜尋編者精選" + +#: .\views\editorspicks.py:75 +msgid "Editor's picks for '{0}' created." +msgstr "編者精選'{0}'已建立。" + +#: .\views\editorspicks.py:103 +msgid "Editor's picks for '{0}' updated." +msgstr "編者精選'{0}'已更新。" + +#: .\views\editorspicks.py:122 +msgid "Editor's picks deleted." +msgstr "編者精選已刪除" diff --git a/wagtail/wagtailsearch/tests/test_backends.py b/wagtail/wagtailsearch/tests/test_backends.py index f34594d6b..bd4a64926 100644 --- a/wagtail/wagtailsearch/tests/test_backends.py +++ b/wagtail/wagtailsearch/tests/test_backends.py @@ -2,7 +2,7 @@ from django.test import TestCase from django.test.utils import override_settings from django.conf import settings from django.core import management -import unittest +from wagtail.tests.utils import unittest from wagtail.wagtailsearch import models, get_search_backend from wagtail.wagtailsearch.backends.db import DBSearch from wagtail.wagtailsearch.backends import InvalidSearchBackendError diff --git a/wagtail/wagtailsearch/tests/test_queries.py b/wagtail/wagtailsearch/tests/test_queries.py index fd1c6c1b1..7086d46f4 100644 --- a/wagtail/wagtailsearch/tests/test_queries.py +++ b/wagtail/wagtailsearch/tests/test_queries.py @@ -1,9 +1,8 @@ from django.test import TestCase from django.core import management from wagtail.wagtailsearch import models -from wagtail.tests.utils import login +from wagtail.tests.utils import login, unittest from StringIO import StringIO -import unittest class TestHitCounter(TestCase): diff --git a/wagtail/wagtailsearch/urls/admin.py b/wagtail/wagtailsearch/urls/admin.py index 27f687b64..62b9d72c6 100644 --- a/wagtail/wagtailsearch/urls/admin.py +++ b/wagtail/wagtailsearch/urls/admin.py @@ -1,13 +1,13 @@ -from django.conf.urls import patterns, url +from django.conf.urls import url +from wagtail.wagtailsearch.views import editorspicks, queries -urlpatterns = patterns( - "wagtail.wagtailsearch.views", - url(r"^editorspicks/$", "editorspicks.index", name="wagtailsearch_editorspicks_index"), - url(r"^editorspicks/add/$", "editorspicks.add", name="wagtailsearch_editorspicks_add"), - url(r"^editorspicks/(\d+)/$", "editorspicks.edit", name="wagtailsearch_editorspicks_edit"), - url(r"^editorspicks/(\d+)/delete/$", "editorspicks.delete", name="wagtailsearch_editorspicks_delete"), +urlpatterns = [ + url(r"^editorspicks/$", editorspicks.index, name="wagtailsearch_editorspicks_index"), + url(r"^editorspicks/add/$", editorspicks.add, name="wagtailsearch_editorspicks_add"), + url(r"^editorspicks/(\d+)/$", editorspicks.edit, name="wagtailsearch_editorspicks_edit"), + url(r"^editorspicks/(\d+)/delete/$", editorspicks.delete, name="wagtailsearch_editorspicks_delete"), - url(r"^queries/chooser/$", "queries.chooser", name="wagtailsearch_queries_chooser"), - url(r"^queries/chooser/results/$", "queries.chooserresults", name="wagtailsearch_queries_chooserresults"), -) + url(r"^queries/chooser/$", queries.chooser, name="wagtailsearch_queries_chooser"), + url(r"^queries/chooser/results/$", queries.chooserresults, name="wagtailsearch_queries_chooserresults"), +] diff --git a/wagtail/wagtailsearch/urls/frontend.py b/wagtail/wagtailsearch/urls/frontend.py index ef3764814..dcaebe2c5 100644 --- a/wagtail/wagtailsearch/urls/frontend.py +++ b/wagtail/wagtailsearch/urls/frontend.py @@ -1,8 +1,8 @@ -from django.conf.urls import patterns, url +from django.conf.urls import url +from wagtail.wagtailsearch.views import search -urlpatterns = patterns( - 'wagtail.wagtailsearch.views', - url(r'^$', 'search', name='wagtailsearch_search'), - url(r'^suggest/$', 'search', {'use_json': True}, name='wagtailsearch_suggest'), -) +urlpatterns = [ + url(r'^$', search, name='wagtailsearch_search'), + url(r'^suggest/$', search, {'use_json': True}, name='wagtailsearch_suggest'), +] diff --git a/wagtail/wagtailsearch/views/frontend.py b/wagtail/wagtailsearch/views/frontend.py index 500c4920c..33c15924e 100644 --- a/wagtail/wagtailsearch/views/frontend.py +++ b/wagtail/wagtailsearch/views/frontend.py @@ -73,11 +73,11 @@ def search( for result in search_results: result_specific = result.specific - search_results_json.append({ - attr: getattr(result_specific, attr) + search_results_json.append(dict( + (attr, getattr(result_specific, attr)) for attr in json_attrs if hasattr(result_specific, attr) - }) + )) return HttpResponse(json.dumps(search_results_json)) else: diff --git a/wagtail/wagtailsearch/wagtail_hooks.py b/wagtail/wagtailsearch/wagtail_hooks.py new file mode 100644 index 000000000..b8bbd2c06 --- /dev/null +++ b/wagtail/wagtailsearch/wagtail_hooks.py @@ -0,0 +1,11 @@ +from django.conf.urls import include, url + +from wagtail.wagtailadmin import hooks +from wagtail.wagtailsearch.urls import admin as admin_urls + + +def register_admin_urls(): + return [ + url(r'^search/', include(admin_urls)), + ] +hooks.register('register_admin_urls', register_admin_urls) diff --git a/wagtail/wagtailsnippets/locale/zh_TW/LC_MESSAGES/django.mo b/wagtail/wagtailsnippets/locale/zh_TW/LC_MESSAGES/django.mo new file mode 100644 index 000000000..d3f5578db Binary files /dev/null and b/wagtail/wagtailsnippets/locale/zh_TW/LC_MESSAGES/django.mo differ diff --git a/wagtail/wagtailsnippets/locale/zh_TW/LC_MESSAGES/django.po b/wagtail/wagtailsnippets/locale/zh_TW/LC_MESSAGES/django.po new file mode 100644 index 000000000..e89aca887 --- /dev/null +++ b/wagtail/wagtailsnippets/locale/zh_TW/LC_MESSAGES/django.po @@ -0,0 +1,126 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Wagtail\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2014-03-10 12:25+0200\n" +"PO-Revision-Date: 2014-02-28 16:07+0000\n" +"Last-Translator: wdv4758h \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: zh_TW\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: .\templates\wagtailsnippets\chooser\choose.html:2 +msgid "Choose" +msgstr "選擇" + +#: .\templates\wagtailsnippets\edit_handlers\snippet_chooser_panel.html:10 +#, python-format +msgid "Choose another %(snippet_type_name)s" +msgstr "選擇另外一個 %(snippet_type_name)s" + +#: .\templates\wagtailsnippets\edit_handlers\snippet_chooser_panel.html:11 +#, python-format +msgid "Choose %(snippet_type_name)s" +msgstr "選擇 %(snippet_type_name)s" + +#: .\templates\wagtailsnippets\snippets\confirm_delete.html:3 +#, python-format +msgid "Delete %(snippet_type_name)s - %(instance)s" +msgstr "刪除 %(snippet_type_name)s - %(instance)s" + +#: .\templates\wagtailsnippets\snippets\confirm_delete.html:6 +#: .\templates\wagtailsnippets\snippets\edit.html:20 +msgid "Delete" +msgstr "刪除" + +#: .\templates\wagtailsnippets\snippets\confirm_delete.html:10 +#, python-format +msgid "Are you sure you want to delete this %(snippet_type_name)s?" +msgstr "你確定想要刪除這個 %(snippet_type_name)s嗎?" + +#: .\templates\wagtailsnippets\snippets\confirm_delete.html:13 +msgid "Yes, delete" +msgstr "是的,刪除" + +#: .\templates\wagtailsnippets\snippets\create.html:3 +#, python-format +msgid "New %(snippet_type_name)s" +msgstr "新的 %(snippet_type_name)s" + +#: .\templates\wagtailsnippets\snippets\create.html:6 +msgid "New" +msgstr "新建" + +#: .\templates\wagtailsnippets\snippets\create.html:17 +#: .\templates\wagtailsnippets\snippets\edit.html:17 +msgid "Save" +msgstr "儲存" + +#: .\templates\wagtailsnippets\snippets\edit.html:3 +#, python-format +msgid "Editing %(snippet_type_name)s - %(instance)s" +msgstr "編輯 %(snippet_type_name)s - %(instance)s" + +#: .\templates\wagtailsnippets\snippets\edit.html:6 +msgid "Editing" +msgstr "編輯" + +#: .\templates\wagtailsnippets\snippets\index.html:3 +msgid "Snippets" +msgstr "片段" + +#: .\templates\wagtailsnippets\snippets\list.html:8 +msgid "Title" +msgstr "標題" + +#: .\templates\wagtailsnippets\snippets\type_index.html:3 +#, python-format +msgid "Snippets %(snippet_type_name_plural)s" +msgstr "%(snippet_type_name_plural)s 片段" + +#: .\templates\wagtailsnippets\snippets\type_index.html:10 +#, python-format +msgid "Snippets %(snippet_type_name_plural)s" +msgstr "%(snippet_type_name_plural)s 片段" + +#: .\templates\wagtailsnippets\snippets\type_index.html:13 +#, python-format +msgid "Add %(snippet_type_name)s" +msgstr "新增 %(snippet_type_name)s" + +#: .\templates\wagtailsnippets\snippets\type_index.html:23 +#, python-format +msgid "" +"No %(snippet_type_name_plural)s have been created. Why not add one?" +msgstr "" +"沒有任何 %(snippet_type_name_plural)s 片段。為什麼不建立一個?" + +#: .\views\snippets.py:127 +msgid "{snippet_type} '{instance}' created." +msgstr "已建立 {snippet_type} '{instance}'" + +#: .\views\snippets.py:134 +msgid "The snippet could not be created due to errors." +msgstr "片段因有錯誤而無法建立。" + +#: .\views\snippets.py:168 +msgid "{snippet_type} '{instance}' updated." +msgstr "已經更新 {snippet_type} '{instance}'。" + +#: .\views\snippets.py:175 +msgid "The snippet could not be saved due to errors." +msgstr "片段因有錯誤而無法儲存。" + +#: .\views\snippets.py:204 +msgid "{snippet_type} '{instance}' deleted." +msgstr "已刪除 {snippet_type} '{instance}'" diff --git a/wagtail/wagtailsnippets/permissions.py b/wagtail/wagtailsnippets/permissions.py index 57441f356..5a730909b 100644 --- a/wagtail/wagtailsnippets/permissions.py +++ b/wagtail/wagtailsnippets/permissions.py @@ -19,10 +19,12 @@ def user_can_edit_snippet_type(user, content_type): def user_can_edit_snippets(user): """ true if user has any permission related to any content type registered as a snippet type """ + snippet_content_types = get_snippet_content_types() if user.is_active and user.is_superuser: - return True + # admin can edit snippets iff any snippet types exist + return bool(snippet_content_types) - permissions = Permission.objects.filter(content_type__in=get_snippet_content_types()).select_related('content_type') + permissions = Permission.objects.filter(content_type__in=snippet_content_types).select_related('content_type') for perm in permissions: permission_name = "%s.%s" % (perm.content_type.app_label, perm.codename) if user.has_perm(permission_name): diff --git a/wagtail/wagtailsnippets/tests.py b/wagtail/wagtailsnippets/tests.py index 95cbacbf0..782fe9087 100644 --- a/wagtail/wagtailsnippets/tests.py +++ b/wagtail/wagtailsnippets/tests.py @@ -5,7 +5,7 @@ when you run "manage.py test". Replace this with more appropriate tests for your application. """ -import unittest +from wagtail.tests.utils import unittest from django.test import TestCase diff --git a/wagtail/wagtailsnippets/urls.py b/wagtail/wagtailsnippets/urls.py index 927c30c28..eb7dc0112 100644 --- a/wagtail/wagtailsnippets/urls.py +++ b/wagtail/wagtailsnippets/urls.py @@ -1,16 +1,16 @@ -from django.conf.urls import patterns, url +from django.conf.urls import url +from wagtail.wagtailsnippets.views import chooser, snippets -urlpatterns = patterns( - 'wagtail.wagtailsnippets.views', - url(r'^$', 'snippets.index', name='wagtailsnippets_index'), +urlpatterns = [ + url(r'^$', snippets.index, name='wagtailsnippets_index'), - url(r'^choose/$', 'chooser.choose', name='wagtailsnippets_choose_generic'), - url(r'^choose/(\w+)/(\w+)/$', 'chooser.choose', name='wagtailsnippets_choose'), - url(r'^choose/(\w+)/(\w+)/(\d+)/$', 'chooser.chosen', name='wagtailsnippets_chosen'), + url(r'^choose/$', chooser.choose, name='wagtailsnippets_choose_generic'), + url(r'^choose/(\w+)/(\w+)/$', chooser.choose, name='wagtailsnippets_choose'), + url(r'^choose/(\w+)/(\w+)/(\d+)/$', chooser.chosen, name='wagtailsnippets_chosen'), - url(r'^(\w+)/(\w+)/$', 'snippets.list', name='wagtailsnippets_list'), - url(r'^(\w+)/(\w+)/new/$', 'snippets.create', name='wagtailsnippets_create'), - url(r'^(\w+)/(\w+)/(\d+)/$', 'snippets.edit', name='wagtailsnippets_edit'), - url(r'^(\w+)/(\w+)/(\d+)/delete/$', 'snippets.delete', name='wagtailsnippets_delete'), -) + url(r'^(\w+)/(\w+)/$', snippets.list, name='wagtailsnippets_list'), + url(r'^(\w+)/(\w+)/new/$', snippets.create, name='wagtailsnippets_create'), + url(r'^(\w+)/(\w+)/(\d+)/$', snippets.edit, name='wagtailsnippets_edit'), + url(r'^(\w+)/(\w+)/(\d+)/delete/$', snippets.delete, name='wagtailsnippets_delete'), +] diff --git a/wagtail/wagtailsnippets/wagtail_hooks.py b/wagtail/wagtailsnippets/wagtail_hooks.py new file mode 100644 index 000000000..3745d1a51 --- /dev/null +++ b/wagtail/wagtailsnippets/wagtail_hooks.py @@ -0,0 +1,38 @@ +from django.conf import settings +from django.conf.urls import include, url +from django.core import urlresolvers +from django.utils.html import format_html +from django.utils.translation import ugettext_lazy as _ + +from wagtail.wagtailadmin import hooks +from wagtail.wagtailadmin.menu import MenuItem + +from wagtail.wagtailsnippets import urls +from wagtail.wagtailsnippets.permissions import user_can_edit_snippets + + +def register_admin_urls(): + return [ + url(r'^snippets/', include(urls)), + ] +hooks.register('register_admin_urls', register_admin_urls) + + +def construct_main_menu(request, menu_items): + if user_can_edit_snippets(request.user): + menu_items.append( + MenuItem(_('Snippets'), urlresolvers.reverse('wagtailsnippets_index'), classnames='icon icon-snippet', order=500) + ) +hooks.register('construct_main_menu', construct_main_menu) + + +def editor_js(): + return format_html(""" + + + """, + settings.STATIC_URL, + 'wagtailsnippets/js/snippet-chooser.js', + urlresolvers.reverse('wagtailsnippets_choose_generic') + ) +hooks.register('insert_editor_js', editor_js) diff --git a/wagtail/wagtailusers/locale/zh_TW/LC_MESSAGES/django.mo b/wagtail/wagtailusers/locale/zh_TW/LC_MESSAGES/django.mo new file mode 100644 index 000000000..59297ccaa Binary files /dev/null and b/wagtail/wagtailusers/locale/zh_TW/LC_MESSAGES/django.mo differ diff --git a/wagtail/wagtailusers/locale/zh_TW/LC_MESSAGES/django.po b/wagtail/wagtailusers/locale/zh_TW/LC_MESSAGES/django.po new file mode 100644 index 000000000..24b9b8d3c --- /dev/null +++ b/wagtail/wagtailusers/locale/zh_TW/LC_MESSAGES/django.po @@ -0,0 +1,181 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Wagtail\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2014-03-14 23:02+0200\n" +"PO-Revision-Date: 2014-03-14 21:12+0000\n" +"Last-Translator: wdv4758h \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: zh_TW\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: .\forms.py:12 .\forms.py:73 +msgid "Administrator" +msgstr "管理者" + +#: .\forms.py:14 +msgid "If ticked, this user has the ability to manage user accounts." +msgstr "如果已勾選,這個使用者將有權限管理使用者帳號" + +#: .\forms.py:17 .\forms.py:58 +msgid "Email" +msgstr "電子信箱" + +#: .\forms.py:18 .\forms.py:59 +msgid "First Name" +msgstr "名" + +#: .\forms.py:19 .\forms.py:60 +msgid "Last Name" +msgstr "姓" + +#: .\forms.py:46 +msgid "A user with that username already exists." +msgstr "一個使用者已經佔用這個名字" + +#: .\forms.py:47 +msgid "The two password fields didn't match." +msgstr "密碼不符合" + +#: .\forms.py:50 .\templates\wagtailusers\list.html:15 +msgid "Username" +msgstr "使用者名稱" + +#: .\forms.py:53 +msgid "Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only." +msgstr "必填,30個字元以内。只允許字母、數字、@/./+/-/_" + +#: .\forms.py:55 +msgid "This value may contain only letters, numbers and @/./+/-/_ characters." +msgstr "這個值只能含有字母、數字、@/./+/-/_" + +#: .\forms.py:63 +msgid "Password" +msgstr "密碼" + +#: .\forms.py:66 +msgid "Leave blank if not changing." +msgstr "如果不修改請留空" + +#: .\forms.py:68 +msgid "Password confirmation" +msgstr "密碼確認" + +#: .\forms.py:70 +msgid "Enter the same password as above, for verification." +msgstr "輸入和上面一樣的密碼用於驗證" + +#: .\forms.py:75 +msgid "Administrators have the ability to manage user accounts." +msgstr "管理者們有管理使用者帳號的權限" + +#: .\templates\wagtailusers\create.html:4 +#: .\templates\wagtailusers\create.html:8 +#: .\templates\wagtailusers\create.html:35 +msgid "Add user" +msgstr "新增使用者" + +#: .\templates\wagtailusers\create.html:12 +#: .\templates\wagtailusers\edit.html:12 +msgid "Account" +msgstr "帳號" + +#: .\templates\wagtailusers\create.html:13 +#: .\templates\wagtailusers\create.html:28 +#: .\templates\wagtailusers\edit.html:13 +msgid "Roles" +msgstr "角色" + +#: .\templates\wagtailusers\edit.html:4 .\templates\wagtailusers\edit.html:8 +msgid "Editing" +msgstr "編輯" + +#: .\templates\wagtailusers\edit.html:30 .\templates\wagtailusers\edit.html:37 +msgid "Save" +msgstr "儲存" + +#: .\templates\wagtailusers\index.html:4 +#: .\templates\wagtailusers\index.html:17 +msgid "Users" +msgstr "使用者" + +#: .\templates\wagtailusers\index.html:18 +msgid "Add a user" +msgstr "新增使用者" + +#: .\templates\wagtailusers\list.html:7 +msgid "Name" +msgstr "名字" + +#: .\templates\wagtailusers\list.html:22 +msgid "Level" +msgstr "等級" + +#: .\templates\wagtailusers\list.html:23 +msgid "Status" +msgstr "狀態" + +#: .\templates\wagtailusers\list.html:36 +msgid "Admin" +msgstr "管理" + +#: .\templates\wagtailusers\list.html:37 +msgid "Active" +msgstr "啟用" + +#: .\templates\wagtailusers\list.html:37 +msgid "Inactive" +msgstr "未啟用" + +#: .\templates\wagtailusers\results.html:5 +#, python-format +msgid "" +"\n" +" There is one match\n" +" " +msgid_plural "" +"\n" +" There are %(counter)s matches\n" +" " +msgstr[0] "\n 有一個符合" +msgstr[1] "\n 有 $(counter)s 個符合" + +#: .\templates\wagtailusers\results.html:18 +#, python-format +msgid "Sorry, no users match \"%(query_string)s\"" +msgstr "對不起,沒有任何使用者符合 \"%(query_string)s\"" + +#: .\templates\wagtailusers\results.html:21 +#, python-format +msgid "" +"There are no users configured. Why not add some?" +msgstr "沒有使用者. 為什麼不 新增一個?" + +#: .\views\users.py:21 .\views\users.py:28 +msgid "Search users" +msgstr "搜尋使用者" + +#: .\views\users.py:75 +msgid "User '{0}' created." +msgstr "使用者 '{0}' 已建立" + +#: .\views\users.py:78 +msgid "The user could not be created due to errors." +msgstr "使用者因有錯誤而無法建立。" + +#: .\views\users.py:94 +msgid "User '{0}' updated." +msgstr "使用者 '{0}' 已更新" + +#: .\views\users.py:97 +msgid "The user could not be saved due to errors." +msgstr "使用者因有錯誤而無法儲存。" diff --git a/wagtail/wagtailusers/urls.py b/wagtail/wagtailusers/urls.py index 7832ddba5..87b3d3f5c 100644 --- a/wagtail/wagtailusers/urls.py +++ b/wagtail/wagtailusers/urls.py @@ -1,8 +1,8 @@ -from django.conf.urls import patterns, url +from django.conf.urls import url +from wagtail.wagtailusers.views import users -urlpatterns = patterns( - 'wagtail.wagtailusers.views', - url(r'^$', 'users.index', name='wagtailusers_index'), - url(r'^new/$', 'users.create', name='wagtailusers_create'), - url(r'^(\d+)/$', 'users.edit', name='wagtailusers_edit'), -) +urlpatterns = [ + url(r'^$', users.index, name='wagtailusers_index'), + url(r'^new/$', users.create, name='wagtailusers_create'), + url(r'^(\d+)/$', users.edit, name='wagtailusers_edit'), +] diff --git a/wagtail/wagtailusers/wagtail_hooks.py b/wagtail/wagtailusers/wagtail_hooks.py new file mode 100644 index 000000000..d006a4d93 --- /dev/null +++ b/wagtail/wagtailusers/wagtail_hooks.py @@ -0,0 +1,23 @@ +from django.conf.urls import include, url +from django.core import urlresolvers +from django.utils.translation import ugettext_lazy as _ + +from wagtail.wagtailadmin import hooks +from wagtail.wagtailadmin.menu import MenuItem + +from wagtail.wagtailusers import urls + + +def register_admin_urls(): + return [ + url(r'^users/', include(urls)), + ] +hooks.register('register_admin_urls', register_admin_urls) + + +def construct_main_menu(request, menu_items): + if request.user.has_module_perms('auth'): + menu_items.append( + MenuItem(_('Users'), urlresolvers.reverse('wagtailusers_index'), classnames='icon icon-user', order=600) + ) +hooks.register('construct_main_menu', construct_main_menu)