Creating a Custom Content Type with Paster in Plone 3

0
116
17 min read

(Further resources on Plone see here.)

we have used a graphic application (ArgoUML) to draw a UML model that was automatically transformed by ArchGenXML into an Archetypes-based content type for Plone.

In this article, we’ll create a content type product with another tool, paster. It’s not a graphic application, but it’s as easy to use as writing a few characters.

We used paster earlier to create a buildout-based Zope instance and an egg-structured Plone product. Here we’ll use it to create a full Archetype product, its schema fields, and even the required tests to make sure everything is working as intended

Creating an Archetypes product with paster

There are several steps to take with paster to produce a full and useful content type. The first one should be the creation of the structure, meaning the product directory organization.

Getting ready

The final destination of this product, at least at development stage, is the src folder of our buildout directory. There is where we place our packages source code while we are working on them, until they become eggs (to see how to turn them into eggs read Submitting products to an egg repository). Thus go to your buildout directory and then get inside the src folder:

cd ./src

Make sure you have the latest ZopeSkel installed. ZopeSkel is the name of a Python package with a collection of skeletons and templates to create commonly used Zope and Plone projects via a paster command.

easy_install -U ZopeSkel

 

How to do it…

  1. Create a package for the new add-on product: We are going to create a new package called pox.video. The pox prefix is taken from PloneOpenX (the website we are working on) and will be the namespace of our product.
    paster create -t archetype
  2. Fix the main configure.zcml file to prevent errors:

    Open the just created configure.zcml file in the pox/video folder and comment the internationalization registration like this:

    <!-- <i18n:registerTranslations directory="locales" /> -->

  3. Update the Zope instance with the new product:

    To let Zope know that there’s new code to be used, let’s update our instance buildout.cfg file.

    In the main [buildout] section, modify the eggs and develop parameters like this:

    [buildout]...
    eggs =
    ...
    pox.video
    ...
    develop =
    src/pox.video

  4. Automatically add the product in a Plone site:

    We can install our brand new product automatically during buildout. So add a pox.video line inside the [plonesite] part’s products parameter:

    [plonesite]recipe = collective.recipe.plonesite
    ...
    products =
    ...
    pox.video

  5. Rebuild and relaunch the Zope instance:

    Build your instance and, if you want to, launch it to check that the pox.video product is installed (not strictly necessary though).

    ./bin/buildout
    ./bin/instance fg

How it works…

So far we have a skeleton product, which is composed basically of boilerplate (we will build on it further). However, it has all the necessary code to be installed, which is important.

The paster command of Step 1 in How to do it… creates a package using the archetype available template. When run, it will output some informative text and then a short wizard will be started to select some options. The most important are the first five ones:

Option Value
Enter project name pox.video
Expert mode? Choose whatever option you like.
Project Title Video
Version 1.0
Description Video content type for PloneOpenX website

Add whatever you want to the remaining options (if you chose other than easy mode), or just hit Enter to each one. After selecting the last option, you’ll get an output like this (a little longer actually):

Creating template basic_namespace
Creating directory .pox.video
Creating template archetype
Recursing into +namespace_package+
Recursing into +package+
Recursing into content
Recursing into interfaces
Recursing into portlets
Recursing into profiles
Recursing into tests
...

The project you just created has local commands. These can be used from within the product.

usage: paster COMMAND

Commands:
addcontent Adds plone content types to your project

For more information: paster help COMMAND

The first group of lines tells us something about the created directory structure. We have a pox.video (project name) folder, containing a pox (namespace) folder, which contains a video (package) folder, which in turn contains several sub-packages: content, interfaces, portlets, profiles, and tests. In the following sections, we are going to deal with all of them except portlets, which will be tackled in Creating a portlet package, Customizing a new portlet according to our requirements, and Testing portlets.

The second group of lines (after the ellipsis) gives us very important information: we can use particular local commands inside our fresh product. More of this in the next section.

Step 2 in the preceding procedure is to tell Zope about the new available package. By adding pox.video in the eggs parameter, we add it in Zope’s PYTHONPATH. We also have to add the package’s location in the develop parameter. If not, the buildout process would try to fetch it from some of the URLs listed in the find-links parameter.

During start up, Zope 2 loads (Five does the job actually) configuration files, usually configure.zcml, for all the products and packages inside the folders that are listed in the [instance] section’s products parameter. For other Python packages outside those folders, a ZCML slug is required for the product to be loaded.

Fortunately, from Plone 3.3 onwards, the ZCML slug is not needed if packages to be installed use z3c.autoinclude, which automatically detects and includes ZCML files.

Although we were not aware of that, when we created the pox.video package with paster, z3c.autoinclude was added as an entry point in the setup.py file. Open it in the main pox.video folder to check it:

...
setup(name='pox.video',
version=version,
description="Video content type for PloneOpenX website",
...
entry_points="""
# -*- entry_points -*-
[z3c.autoinclude.plugin] target = plone
""",
...
)

For those packages that don’t have this feature, we must explicitly insert a reference to the package in the zcml parameter of the [instance] section like we did in Taking advantage of an enhanced interactive Python debugger with ipdb:

[instance]
...

# If you want to register ZCML slugs for any packages,
# list them here.
# e.g. zcml = my.package my.other.package
zcml =
iw.debug

There’s more…

Do not forget to test your changes (paster changes in fact)!

Fortunately, paster creates automatically a tests sub-package and a package-level README.txt file with the first part of a test (logging into our website). Feel free to take a look at it, as it is a very good example of doctest. Nevertheless, it really doesn’t test too much for the time being. It will be more productive after adding some features to the product.

You may find it really useful to read the content types section from the online Plone Developer Manual at http://plone.org/documentation/manual/developer-manual/archetypes.

See Also

  • Submitting products to an egg repository
  • Taking advantage of an enhanced interactive Python debugger with ipdb
  • Adding a content type into a product
  • Adding fields to a content type
  • Adding a custom validator to a content type
  • Creating a portlet egg with paster
  • Customizing a new portlet according to our requirements
  • Testing portlets

Adding a content type into a product

In Creating an Archetypes product with paster, we were able to create a package shell with all the necessary code to install a product, although it was unproductive.

We are now going to add some useful functionality by means of, again, our dear paster.

Getting ready

When we ran paster in Creating an Archetypes product with paster, we highlighted some of its output, copied below:

The project you just created has local commands. These can be used 
from within the product.

Paster local commands are available inside the project folder. So let’s move inside it:

cd ./src/pox.video

How to do it…

To add a new content type inside the product, run the following command:

paster addcontent contenttype

How it works…

This will run the addcontent paster command with its contenttype template. After a short wizard asking for some options, it will produce all the code we need.

Option Value
Enter Video
Enter contenttype_description FLV video file
Enter folderish False
Enter global_allow True
Enter allow_discussion True/False, whatever

You’ll get an output like this:

...
Inserting from README.txt_insert into /pox.video/pox/video/README.txt
Recursing into content
Recursing into interfaces
Recursing into profiles
Recursing into default
Recursing into types

If you need more than just one content type in your product, you can run the paster addcontent contenttype command as many times as you want.

There’s no need to modify, buildout.cfg file, as we have already made all the required changes. If you didn’t make these modifications, please refer to Creating an Archetypes product with paster.

Open the interface file in ./src/pox.video/pox/video/interface/video.py:

from zope import schema
from zope.interface import Interface

from pox.video import videoMessageFactory as _

class IVideo(Interface):
"""Description of the Example Type"""

# -*- schema definition goes here -*-

Empty interfaces, like this one, are called marker interfaces. Although they provide some information (they can be used to associate a class with some functionality as we will see in Using the ZCA to extend a third party product: Collage), they lack attributes and methods information (that is, their promised functionalities), and consequently and worse, they don’t document.

Interfaces don’t exist in Python. However, Zope 3 has incorporated this concept to let components interact easier. All attributes and methods declarations in interfaces are a contract (not a binding one, though) with the external world. For more information about zope.interface, visit http://wiki.zope.org/Interfaces/FrontPage.

The new content type class is in the video.py file located in the ./src/pox.vieo/pox/video/content package. Let’s go through it and explain its pieces.

"""Definition of the Video content type
"""

from zope.interface import implements, directlyProvides
from Products.Archetypes import atapi
from Products.ATContentTypes.content import base

from Products.ATContentTypes.content import schemata
from pox.video import videoMessageFactory as _
from pox.video.interfaces import IVideo
from pox.video.config import PROJECTNAME

All paster-generated content types inherit from basic ATContentTypes, which is good given the large number of products available for them.

Check the Products.ATContentTypes package for plenty of good working examples.

VideoSchema = schemata.ATContentTypeSchema.copy() + atapi.Schema((
# -*- Your Archetypes field definitions here ... -*-
))

Schemas specify the fields available in content types. In our case, the Video content type is a plain copy of ATContentTypeSchema, which already includes all fields necessary to support Dublin Core convention.

Dublin Core is supported thanks to the BaseObject and ExtensibleMetadata modules in the Products.Archetypes package.

VideoSchema here is the result of the addition (yes, we can actually add schemas) of two other schemas: the aforementioned ATContentTypeSchema and the new empty one created with the atapi.Schema(()) method, which expects a tuple argument (check the double brackets).

Up to ZopeSkel 2.16 (paster’s package) the storage of title and description fields are changed to AnnotationStorage. This reduces performance and therefore it would be better to change it by removing these lines letting Archetypes deal with regular AttributeStorage:

# Set storage on fields copied from ATContentTypeSchema,
# making sure
# they work well with the python bridge properties.
VideoSchema['title'].storage = atapi.
AnnotationStorage()
VideoSchema['description'].storage = atapi.
AnnotationStorage()

here are plans to remove this from ZopeSkel, but there’s no release date yet for it.

After schema definition, we call finalizeATCTSchema to re-order and move some fields inside our schema according to Plone standard. It’s advisable to get familiar with its code in the Products.ATContentTypes.content.schema module:

schemata.finalizeATCTSchema(VideoSchema, moveDiscussion=False)

Once defined its schema the real class is created. As we said earlier, it inherits from base.ATCTContent; this will be changed for our Video content type.

class Video(base.ATCTContent):
"""FLV video file"""
implements(IVideo)

meta_type = "Video"
schema = VideoSchema

title = atapi.ATFieldProperty('title')
description = atapi.ATFieldProperty('description')

# -*- Your ATSchema to Python Property Bridges Here ... -*-

atapi.registerType(Video, PROJECTNAME)

The first line in our class body specifies that it implements IVideo interface (interfaces/video.py file).

then VideoSchema is associated with the class.

ATFieldProperty is required to create ATSchema to Python Property bridges. These are recommended for fields of a schema using AnnotationStorage. If you still have title and description fields storage as AnnotationStorage, you should keep these lines. Otherwise you can safely remove them.

And finally, the atapi.registerType() call adds all getters and setters to the Video class. This is Archetypes’ magic. You define just a schema and Archetypes will automatically create all methods needed to interact with the class.

There’s more…

We do have some more interesting code now, that’s why we should be more careful with it and test it. Again, paster has appended several functional tests in the README.txt file, including the creation (as Manager and Contributor users), modification, and deletion of a Video object.

Test the product with the following command:

./bin/instance test -s pox.video

We’d like to highlight the block of statements regarding the creation of content as a contributor member:

Let's logout and then login as 'contributor', a portal member
that has the contributor role assigned.

>>> browser.getLink('Log out').click()
>>> browser.open(portal_url)
>>> browser.getControl(name='__ac_name').value = 'contributor'
>>> browser.getControl(name='__ac_password').value =
default_password
>>> browser.getControl(name='submit').click()

This contributor member isn’t mentioned anywhere inside the test. Nevertheless the login action doesn’t fail. How can that be possible if there’s no contributor member included by default in the PloneTestCase base class, like default_user or portal_owner?

If we check the base.py file inside the tests sub-package of our product, we’ll see that the FunctionalTestCase class has a special afterSetUp method, which is called just before the real test begins and registers the contributor member above.

Could we have created the user inside the test? Definitely, because test code is a set of Python statements and we can do whatever we want with them.

Is it sensible to perform this kind of set up actions inside the test code? Absolutely not. functional tests should be conceived as black-box tests, from the sheer end-user point of view. This means that code inside a functional test shouldn’t assume anything about the underlying environment, but behave as if a regular user were acting through the user interface. Anything we need during testing that shouldn’t be done by the user must be placed outside the test code, as in this example.

See also

  • Creating an Archetypes product with paster
  • Working with paster generated test suites
  • Zope Functional testing
  • Using the ZCA to extend a third party product: Collage

Changing the base class in paster content types

All paster-created (non-folderish) content types inherit from the basic ATCTContent class, which comes with ATContentTypeSchema. However, this is a very basic content type: title, description, and some more metadata fields. On top of this, we intend to upload videos to our website, not just text.

ATContentTypes are native to Plone and many community developers had released extensions or plugins for them, such as LinguaPlone. That’s why it is prudent to stay close to them. We are now going to change the ATCTContent parent class for ATFile to automatically inherit all the benefits, including the file upload field.

How to do it…

Open the video.py file inside the content sub-package of your product and make these changes. Be aware of commented lines — they can be just removed, but we wanted to keep them here to remark what’s going on.

  1. Import the base class and interface to be used:

    from Products.Archetypes import atapi
    # from Products.ATContentTypes.content import base
    from Products.ATContentTypes.content import file
    from Products.ATContentTypes.interface.file import IATFile
    from Products.ATContentTypes.content import schemata

    This way we are importing interface and class of the out-of-the-box File content type.

  2. Change original schema:

    # VideoSchema = schemata.ATContentTypeSchema.copy() +
    # atapi.Schema((
    VideoSchema = file.ATFileSchema.copy() + atapi.Schema((

    # -*- Your Archetypes field definitions here ... -*-
    ))

    Now, our VideoSchema includes File fields.

  3. Change the base class and implemented interface:

    # class Video(base.ATCTContent):
    class Video(file.ATFile):
    """
    pox Video
    """
    # implements(IVideo)
    implements(IATFile,IVideo

    The last step is to change the parent class of Video so that now it inherits from ATFile instead of just ATCTContent. And then we adjust the interfaces this class now implements.

  4. Change the view action URL: Open profiles/default/types/Video.xml file and amend the url_expr attribute in View action by adding /view.

    <?xml version="1.0"?>
    <object name="Video"
    meta_type="Factory-based Type Information with dynamic views"
    i18n_domain="pox.video" action_id="view" category=
    "object" condition_expr=""
    url_expr="string:${object_url}/view" visible="True">
    <permission value="View" />
    </action>
    ...
    </object>

  5. Tell Plone to use the new action URL in listings: Add a propertiestool.xml file inside the profiles/default folder with this code:

    <?xml version="1.0"?>
    <object name="portal_properties" meta_type=
    "Plone Properties Tool">
    <object name="site_properties" meta_type="Plone Property Sheet">
    <property name="typesUseViewActionInListings" type=
    "lines" purge="False">
    <element value="Video"/>
    </property>
    </object>
    </object>

  6. Relaunch your instance and reinstall the product. By restarting the Zope instance all of the latest changes will be applied:

    ./bin/instance fg

    Go to http://localhost:8080/plone/prefs_install_products_form and reinstall the product.

  7. Create a new Video object:Inside your Plone site, click on the Add newdrop-down menu and select the Video option. It should look like this.

How it works…

Since the first creation of the almost empty product (full of boilerplate, though), in Creating an Archetypes product with paster, we haven’t tried to use it, except for the tests we have run. We can now say that it has grown up and it’s ready to be seen in action.

In Steps 1 to 3 above, we changed some of the basics in paster’s original class and its schema to inherit all the benefits of another existing content type: ATFile.

If you had tried to create a Video content type before Step 4, after saving, you would have been automatically prompted to download the file you just uploaded. Why’s that? The reason is we inherited our class from ATFile, which has a special behavior (like ATImage) regarding its URLs.

Files and images uploaded to a Plone site (using regular content types) are downloadable via their natural URL. For example, if you browse to http://yoursite.com/document.pdf, you will be asked to download the file. Alternatively, if you want to open the web page with a download link and metadata, you should use http://yoursite.com/document.pdf/view.

That’s why we had to change the view URL for our content type in Video.xml (Step 4) for users to be able to open an inline video player (as we plan to).

All files included in the profiles folder are used by GenericSetup during installation of the product and are to give some information that Python code doesn’t provide, such as whether we’ll let users post comments inside the content types (allow_discussion).

Plone object listings (including search results) tend to create links to contents without the /view suffix. We must explicitly tell Plone that when listing videos, the suffix should be appended to prevent a download attempt. Fortunately, Plone has foreseen this could have happened. Thus there’s no need to modify or override every single list. If the content type name is listed in the special typesUseViewActionInListings property, it will work as expected.

Plone object listings (including search results) tend to create links to contents without the /view suffix. We must explicitly tell Plone that when listing videos, the suffix should be appended to prevent a download attempt. Fortunately, Plone has foreseen this could have happened. Thus there’s no need to modify or override every single list. If the content type name is listed in the special typesUseViewActionInListings property, it will work as expected.

Changes in Step 5 will make the site_properties update its typesUseViewActionInListings property. By including purge=”False” in <property /> tag, we prevent other existing values (typically File and Image) in the property from being removed.

LEAVE A REPLY

Please enter your comment!
Please enter your name here