Writing Your First Widget ========================= In this tutorial, we will build a Slideshow widget. You probably want to read the :doc:`widgy-mezzanine-tutorial` to get a Widgy site running before you do this one. We currently have a static slideshow that we need to make editable. Users need to be able to add any number of slides. Users also want to be able to change the delay between each slide. Here is the current slideshow HTML that is using `jQuery Cycle2`_: .. code-block:: html+django
Cute cat Fuzzy kitty Another cute cat Awwww
.. seealso:: :django:ttag:`templatetag` This template tag allows inserting the ``{{`` and ``}}`` characters needed by Cycle2. .. _jQuery Cycle2: http://jquery.malsup.com/cycle2/ 1. Write the Models -------------------- The first step in writing a widget is to write the models. We are going to make a new Django app for these widgets. .. code-block:: sh $ python manage.py startapp slideshow (Don't forget to add ``slideshow`` to your :django:setting:`INSTALLED_APPS`). Now let's write the models. We need to make a ``Slideshow`` model as the container and a ``Slide`` model that represents the individual images. :: # slideshow/models.py from django.db import models import widgy from widgy.models import Content @widgy.register class Slideshow(Content): delay = models.PositiveIntegerField(default=2, help_text="The delay in seconds between slides.") accepting_children = True editable = True @widgy.register class Slide(Content): image = models.ImageField(upload_to='slides/', null=True) caption = models.CharField(max_length=255) editable = True All widget classes inherit from :class:`widgy.models.base.Content`. This creates the relationship with :class:`widgy.models.base.Node` and ensures that all of the required methods are implemented. In order to make a widget visible to Widgy, you have to add it to the registry. There are two functions in the :mod:`widgy` module that help with this, :func:`widgy.register` and :func:`widgy.unregister`. You should use the :func:`widgy.register` class decorator on any model class that you wish to use as a widget. Both widgets need to have :attr:`~widgy.models.base.Content.editable` set to ``True``. This will make an edit button appear in the editor, allowing the user to set the ``image``, ``caption``, and ``delay`` values. ``Slideshow`` has :attr:`~widgy.models.base.Content.accepting_children` set to ``True`` so that you can put a ``Slide`` in it. The default implementation of :meth:`~widgy.models.base.Content.valid_parent_of` checks :attr:`~widgy.models.base.Content.accepting_children`. We only need this until we override :meth:`~widgy.models.base.Content.valid_parent_of` in :ref:`Step 3 `. .. note:: As you can see, the ``image`` field is ``null=True``. It is necessary for all fields in a widget either to be ``null=True`` or to provide a default. This is because when a widget is dragged onto a tree, it needs to be saved without data. :class:`CharFields ` don't need to be ``null=True`` because if they are non-NULL, the default is an empty string. For most other field types, you must have ``null=True`` or a default value. Now we need to generate migration for this app. .. code-block:: sh $ python manage.py schemamigration --initial slideshow And now run the migration. .. code-block:: sh $ python manage.py migrate 2. Write the Templates ----------------------- After that, we need to write our templates. The templates are expected to be named ``widgy/slideshow/slideshow/render.html`` and ``widgy/slideshow/slide/render.html``. To create the slideshow template, add a file at :file:`slideshow/templates/widgy/slideshow/slideshow/render.html`. .. code-block:: html+django {% load widgy_tags %}
{% for child in self.get_children %} {% render child %} {% endfor %}
For the slide, it's :file:`slideshow/templates/widgy/slideshow/slide/render.html`. .. code-block:: html+django {{ self.caption }} .. seealso:: :meth:`Content.get_templates_hierarchy ` Documentation for how templates are discovered. The current ``Slideshow`` instance is available in the context as ``self``. Because jQuery Cycle2 only accepts milliseconds instead of seconds for the delay, we need to add a method to the ``Slideshow`` class. :: class Slideshow(Content): # ... def get_delay_milliseconds(self): return self.delay * 1000 The :class:`~widgy.models.base.Content` class mirrors several methods of the :mod:`TreeBeard API `, so you can call :meth:`~widgy.models.base.Content.get_children` to get all the children. To render a child :class:`~widgy.models.base.Content`, use the :func:`~widgy.templatetags.widgy_tags.render` template tag. .. caution:: You might be tempted to include the HTML for each ``Slide`` inside the render template for ``Slideshow``. While this does work, it is a violation of the single responsibility principle and makes it difficult for slides (or subclasses thereof) to change how they are rendered. .. _slideshow-compatibility: 3. Write the Compatibility --------------------------- Right now, the ``Slideshow`` and ``Slide`` render and could be considered complete; however, the way we have it, ``Slideshow`` can accept any widget as a child and a ``Slide`` can go in any parent. To disallow this, we have to write some :ref:`Compatibility ` methods. :: class Slideshow(Content): def valid_parent_of(self, cls, obj=None): # only accept Slides return issubclass(cls, Slide) class Slide(Content): @classmethod def valid_child_of(cls, parent, obj=None): # only go in Slideshows return isinstance(parent, Slideshow) Done. Addendum: Limit Number of Children ---------------------------------- Say you want to limit the number of ``Slide`` children to 3 for your ``Slideshow``. You do so like this:: class Slideshow(Content): def valid_parent_of(self, cls, obj=None): if obj in self.get_children(): # If it's already one of our children, it is valid return True else: # Make sure it's a Slide and that you aren't full return (issubclass(cls, Slide) and len(self.get_children()) < 3)