Tutorial Part 2: A Content Type

Kotti’s default content types include Document, Image and File. In this part of the tutorial, we’ll add to these built-in content types by making a Poll content type which will allow visitors to view polls and vote on them.

Adding Models

When creating our add-on, the scaffolding added the file kotti_mysite/kotti_mysite/resources.py. If you open resources.py you’ll see that it already contains code for a sample content type CustomContent along with the following imports that we will use.

from kotti.resources import Content
from sqlalchemy import Column
from sqlalchemy import ForeignKey
from sqlalchemy import Integer

Add the following definition for the Poll content type to resources.py.

class Poll(Content):
    id = Column(Integer(), ForeignKey('contents.id'), primary_key=True)

    type_info = Content.type_info.copy(
        name=u'Poll',
        title=u'Poll',
        add_view=u'add_poll',
        addable_to=[u'Document'],
    )

Things to note here:

  • Kotti’s content types use SQLAlchemy for definition of persistence.
  • Poll derives from kotti.resources.Content, which is the common base class for all content types.
  • Poll declares a sqlalchemy.Column id, which is required to hook it up with SQLAlchemy’s inheritance.
  • The type_info class attribute does essential configuration. We refer to name and title, two properties already defined as part of Content, our base class. The add_view defines the name of the add view, which we’ll come to in a second. Finally, addable_to defines which content types we can add Poll items to.
  • We do not need to define any additional sqlalchemy.Column properties, as the title is the only property we need for this content type.

We’ll add another content class to hold the choices for the poll. Add this into the same resources.py file:

class Choice(Content):
    id = Column(Integer(), ForeignKey('contents.id'), primary_key=True)
    votes = Column(Integer())

    type_info = Content.type_info.copy(
        name=u'Choice',
        title=u'Choice',
        add_view=u'add_choice',
        addable_to=[u'Poll'],
    )

    def __init__(self, votes=0, **kwargs):
        super(Choice, self).__init__(**kwargs)
        self.votes = votes

The Choice class looks very similar to Poll. Notable differences are:

  • It has an additional sqla.Column property called votes. We’ll use this to store how many votes were given for the particular choice. We’ll again use the inherited title column to store the title of our choice.
  • The type_info defines the title, the add_view view, and that choices may only be added into Poll items, with the line addable_to=[u'Poll'].

Adding Forms and a View

Views (including forms) are typically put into a module called views. The Kotti scaffolding further separates this into view and edit files inside a views directory.

Open the file at kotti_mysite/kotti_mysite/views/edit.py. It already contains code for the CustomContent sample content type. We will take advantage of the imports already there.

import colander
from kotti.views.edit import ContentSchema
from kotti.views.form import AddFormView
from kotti.views.form import EditFormView
from pyramid.view import view_config

from kotti_mysite import _

Some things to note:

  • Colander is the library that we use to define our schemas. Colander allows us to validate schemas against form data.
  • Our class inherits from kotti.views.edit.ContentSchema which itself inherits from colander.MappingSchema.
  • _ is how we hook into i18n for translations.

Add the following code to views/edit.py:

class PollSchema(ContentSchema):
    """Schema for Poll"""

    title = colander.SchemaNode(
        colander.String(),
        title=_(u'Question'),
    )


class ChoiceSchema(ContentSchema):
    """Schema for Choice"""

    title = colander.SchemaNode(
        colander.String(),
        title=_(u'Choice'),
    )

The two classes define the schemas for our forms. The schemas specify which fields we want to display in the forms. We want to display the title field.

Let’s move on to building the actual forms. Add this to views/edit.py:

from kotti_mysite.resources import Choice
from kotti_mysite.resources import Poll


@view_config(name='edit', context=Poll, permission='edit',
             renderer='kotti:templates/edit/node.pt')
class PollEditForm(EditFormView):
    schema_factory = PollSchema


@view_config(name=Poll.type_info.add_view, permission='add',
             renderer='kotti:templates/edit/node.pt')
class PollAddForm(AddFormView):
    schema_factory = PollSchema
    add = Poll
    item_type = u"Poll"


@view_config(name='edit', context=Choice, permission='edit',
             renderer='kotti:templates/edit/node.pt')
class ChoiceEditForm(EditFormView):
    schema_factory = ChoiceSchema


@view_config(name=Choice.type_info.add_view, permission='add',
             renderer='kotti:templates/edit/node.pt')
class ChoiceAddForm(AddFormView):
    schema_factory = ChoiceSchema
    add = Choice
    item_type = u"Choice"

Using the AddFormView and EditFormView base classes from Kotti, these forms are simple to define. We associate the schemas defined above, setting them as the schema_factory for each form, and we specify the content types to be added by each.

We use @view_config to add our views to the application. This takes advantage of a config.scan() call in __init__.py discussed below. Notice that we can declare permission, context, and a template for each form, along with its name.

Wiring up the Content Types and Forms

Before we can see things in action, we need to add a reference to our new content types in kotti_mysite/kotti_mysite/__init__.py.

Open __init__.py and modify the kotti_configure method so that the settings['kotti.available_types'] line looks like this.

  def kotti_configure(settings):
        ...
      settings['pyramid.includes'] += ' kotti_mysite'
      settings['kotti.available_types'] += (
          ' kotti_mysite.resources.Poll' +
          ' kotti_mysite.resources.Choice')
      settings['kotti.fanstatic.view_needed'] += (
          ' kotti_mysite.fanstatic.css_and_js')
      ...

Here, we’ve added our two content types to the site’s available_types, a global registry. We also removed the CustomContent content type included with the scaffolding.

Notice the includeme method at the bottom of __init__.py. It includes the call to config.scan() that we mentioned above while discussing the @view_config statements in our views.

def includeme(config):
    ...
    config.scan(__name__)

You can see the Pyramid documentation for scan for more information.

Adding a Poll and Choices to the site

Let’s try adding a Poll and some choices to the site. Start the site up with the command

bin/pserve app.ini

Login with the username admin and password qwerty and click on the Add menu button. You should see a few choices, namely the base Kotti classes Document, File and Image and the Content Type we added, Poll.

Lets go ahead and click on Poll. For the question, let’s write “What is your favourite color?”. Now let’s add three choices, “Red”, “Green” and “Blue” in the same way we added the poll. Remember that you must be in the context of the poll to add each choice.

If we now go to the poll we added, we can see the question, but not our choices, which is definitely not what we wanted. Let us fix this, shall we?

Adding a custom View to the Poll

First, we need to write a view that will send the needed data (in our case, the choices we added to our poll). Here is the code, added to view.py.

from kotti_mysite.fanstatic import css_and_js
from kotti_mysite.resources import Poll


@view_defaults(context=Poll)
class PollViews(BaseView):
    """ Views for :class:`kotti_mysite.resources.Poll` """

    @view_config(name='view', permission='view',
                 renderer='kotti_mysite:templates/poll.pt')
    def poll_view(self):
        css_and_js.need()
        choices = self.context.children
        return {
            'choices': choices,
        }

Since we want to show all Choices added to a Poll we can simply use the children attribute. This will return a list of all the ‘children’ of a Poll which are exactly the Choices added to that particular Poll. The view returns a dictionary of all choices under the keyword ‘choices’. The keywords a view returns are automatically available in it’s template.

Next on, we need a template to actually show our data. It could look something like this. Create a folder named templates and put the file poll.pt into it.

<!DOCTYPE html>
<html xmlns:tal="http://xml.zope.org/namespaces/tal"
      xmlns:metal="http://xml.zope.org/namespaces/metal"
      metal:use-macro="api.macro('kotti:templates/view/master.pt')">

  <article metal:fill-slot="content" class="poll-view content">
    <h1>${context.title}</h1>
    <ul>
        <li tal:repeat="choice choices">${choice.title}</li>
    </ul>
  </article>

</html>

The first 6 lines are needed so our template plays nicely with the master template (so we keep the add/edit bar, base site structure etc.). The next line prints out the context.title (our question) inside the <h1> tag and then prints all choices (with links to the choice) as an unordered list.

Note

We are using two ‘magically available’ attributes in the template - context and choices.

  • context is automatically available in all templates and as the name implies it is the context of the view (in this case the Poll we are currently viewing).
  • choices is available because we sent it to the template in the Python part of the view. You can of course send multiple variables to the template, you just need to return them in your Python code.

With this, we are done with the second tutorial. Restart the application, take a look at the new Poll view and play around with the template until you are completely satisfied with how our data is presented.

Note

If you will work with templates for a while (or any time you’re developing basically) using the pyramid ‘reload_templates’ and ‘debug_templates’ options is recommended, as they allow you to see changes to the template without having to restart the application. These options need to be put in your configuration INI under the ‘[app:kotti]’ section.

[app:kotti]
pyramid.reload_templates = true
pyramid.debug_templates = true

In the next tutorial, we will learn how to enable our users to actually vote for one of the Poll options.