Creating a Custom Content Type with Paster in Plone 3

Exclusive offer: get 50% off this eBook here
Plone 3 Products Development Cookbook

Plone 3 Products Development Cookbook — Save 50%

70 simple but incredibly effective recipes for creating your own feature rich, modern Plone add-on products by diving into its development framework

$26.99    $13.50
by Juan Pablo Giménez Marcos F. Romero | May 2010 | Content Management Open Source

The Plone Content Management System is one of the best open source CMS because, by using Plone’s development framework, you can extend its functionality according to the specific requirements of your website. The Plone framework has lots of components that can be used to create add-ons or extensions called Plone Products. You can optimize your site for improved usability, accessibility, and security, by creating custom Plone products.

In this article by Juan Pablo Giménezand Marcos F. Romero, author of Plone 3 Products Development Cookbook, we will cover the creation of Archetypes content types from scratch by hand... kind of. We will actually use paster to automatically create most of it.

(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" xmlns:i18n=
    "http://xml.zope.org/namespaces/i18n">
    ...
    <action title="View" 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 new... drop-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.

Plone 3 Products Development Cookbook 70 simple but incredibly effective recipes for creating your own feature rich, modern Plone add-on products by diving into its development framework
Published: May 2010
eBook Price: $26.99
Book Price: $44.99
See more
Select your format and quantity:

(Further resources on Plone see here.)

There's more…

We have definitely made some changes in the code. And this time it wasn't paster, it was us! Wouldn't it be advisable to test the package?

./bin/instance fg

 

Wow! What happened? Did we get a failure?

Normally, you would add a pdb line before the reported as failing comment:

>>> import pdb; pdb.set_trace()

Or you might consider dumping browser.contents into a file to check what is wrong.

>>> open('/tmp/contents.html', 'w').write(browser.contents)

We'll skip that and jump straight into the problem. The new file field in our upgraded Video content type is required. We should adjust our functional test to upload a file.Open the README.txt file in the pox.video package and search for the following lines to make the changes below (you must change it twice in README.txt):

>>> browser.getControl(name='title').value = 'Video Sample'
>>> browser.getControl('Save').click()
>>> 'Please correct the indicated errors.' in browser.contents
True
>>> 'File is required, please correct.' in browser.contents
True
>>> import cStringIO
>>> browser.getControl(name='file_file').\
... add_file(cStringIO.StringIO('File contents'), \
... 'text/plain', 'test.txt')
>>> browser.getControl('Save').click()
>>> 'Changes saved' in browser.contents
True

We first try to save the video with no uploaded file and get a validation error message. Then we create and upload an on-the-fly text file and everything works fine.

See also

  • Creating an Archetypes product with paster
  • Adding a custom validator to a content type

Adding fields to a content type

According to "Video metadata model" available in Microformats.org (http://microformats.org/wiki/video-metadata-model) the Video content type, as a mere copy of ATFile, has just one metadata field left: creation date. We are now going to add this missing field with a different name—Original date—to avoid mistaking it with the creation date of the object.

Getting started

As we are going to run paster local commands, we must move inside the product folder

cd ./src/pox.video

How to do it…

To add new fields in the Video class schema run the following command:

paster addcontent atschema

How it Works..

The above paster addcontent command with the atschema template will start a short wizard asking for some options, to give us with all the code we need:

Option Value
Enter content_class_filename video
Enter field_name originalDate
Enter field_type datetime
Enter widget_type default, or hit Enter
Enter field_label Original date
Enter field_desc Date this video was recorded

Hit Enter to accept the next default values. You may get a warning message but almost no output. However, your video.py file will be upgraded. The VideoSchema definition now has this new field including a specific validator:

atapi.DateTimeField(
'originalDate',
storage=atapi.AnnotationStorage(),
widget=atapi.CalendarWidget(
label=_(u"Original date"),
description=_(u"Date this video was recorded"),
),
validators=('isValidDate'),
),

There's also a new ATSchema to Python Property bridge within the class body:

originalDate = atapi.ATFieldProperty('originalDate')

As we said in Adding a content type into a product, AnnotationStorage is not recommended for schema fields, so you can safely remove the storage=atapi.AnnotationStorage() line and then remove the Python bridge.

And last, but not least, the interfaces/video.py file has also changed:

class IVideo(Interface):
"""FLV video file"""

# -*- schema definition goes here -*-
originalDate = schema.Date(
title=_(u"Original date"),
required=False,
description=_(u"Date this video was recorded"),
)

Again, if you are keeping AttributeStorage, the default one, instead of AnnotationStorage, remove this schema definition.

There's more…

We should update the test to verify everything is still working and, of course, any update works as expected. Search for the first saving action and change the test according to the following:

>>> browser.getControl(name='title').value = 'Video Sample'
>>> browser.getControl(name='originalDate_year').value = \
... ('2009',)
>>> browser.getControl(name='originalDate_month').value = \
... ('12',)
>>> browser.getControl(name='originalDate_day').value = \
... ('31',)
>>> browser.getControl('Save').click()

To set a datetime field, we must fill all of its date combo-boxes; time is not required though. Now run the test; you should see no failures:

./bin/instance test -s pox.video

See also

  • Adding a content type into a product
  • Adding a custom validator to a content type

Adding a custom validator to a content type

Given that we plan to show an embedded video player when opening a Video page in our website, we must ensure that users won't be able to upload any kind of file other than FLV videos.

Archetypes can validate field inputs in different ways; the most common one is the use of a validators property inside the schema field definition, as it's used in the previous section for the originalDate field.

We prefer using a more modern way to call a validator, a subscription adapter, which is basically a special class that declares to adapt another one and, in the case of Archetypes validators, it implements a specific event interface.

How to do it…

  1. Create this validators.py file inside the contents sub-package of your product:

    from zope.interface import implements
    from zope.component import adapts
    from Products.Archetypes.interfaces import IObjectPostValidation
    from pox.video import videoMessageFactory as _
    from pox.video.interfaces import IVideo
    from flvlib.tags import FLV

    # class name could be any one
    class ValidateFLVFile(object):
    """
    Checks if file field has a real FLV file
    """
    implements(IObjectPostValidation)
    adapts(IVideo)
    field_name = 'file'

    def __init__(self, context):
    self.context = context

    def __call__(self, request):
    value = request.form.get(self.field_name + '_file',
    request.get(self.field_name + '_file', None))
    if value is not None:
    flv_file=FLV(value)
    try:
    flv_file.parse_header()
    except:
    return {self.field_name :
    _(u"Uploaded file is not FLV")}
    if not flv_file.has_video:
    return {self.field_name :
    _(u"Uploaded file is not FLV")}
    # Returning None means no error
    return None

  2. Configure the validator as a subscription adapter. Open the configure.zcml file inside the contents sub-package and add these lines at the bottom of the file (before the </configure> close tag):

    <subscriber
    provides= "Products.Archetypes.interfaces.
    IObjectPostValidation"
    factory=".validators.ValidateFLVFile"
    />

  3. Add dependent packages in our egg: Open the setup.py file in the root pox.video folder of your product and modify the install_requires variable of the setup call:

    setup(name='pox.video',
    ...
    install_requires=['setuptools',
    # -*- Extra requirements: -*-
    'flvlib',
    ],
    ....

  4. Rebuild, relaunch your instance, and reinstall the product:

    ./bin/buildout
    ./bin/instance fg

How it works…

When Archetypes-based content types are being saved, after schema-defined validation has taken place, all factory classes providing or implementing IObjectPostValidation interface (an event interface actually) will be called as event handlers. According to their return values, the content type will be saved or returned to the user to fix the presented errors.

The adapts(IVideo) line in the code of Step 1 is vital and introduces a key concept of Zope 3 component architecture, adapters. We can alter the original behavior of a class object by means of another class (that is, we don't need to modify the source code of the first one).

The rest of the code is the validation itself. When calling the validation routine, there's a request argument available from which we can get all submitted fields (including the file we want to check). The expected return value can be:

  • A dictionary with an error message for every non-passing field
  • None, in which case validation succeeds

There's also an IObjectPreValidation interface whose subscription adapters will be called before schema-specific validation takes place.

The difference between this subscription adapter and other adapters is that it implements an event interface, that's why it must be configured as <subscriber /> instead of a regular <adapter />.

Finally, as we have used a special Python package (flvlib), we can include it as an egg dependency in the product in order to tell the buildout process to download and install it automatically.

There's more…

At this stage, you already know that our doctest in the README.txt file will fail. Until now we were uploading a plain text file. But the new validator will make things stop working or at least, it should.

You might want to download the source files accompanying from its web page to get the video.flv file mentioned below. You could also use any other FLV video, and update paths and filenames accordingly.

To check that validation is working as expected, add these lines in the README.txt file:

>>> browser.getControl('Save').click()
>>> 'Please correct the indicated errors.' in browser.contents
True
>>> 'File is required, please correct.' in browser.contents
True
>>> import cStringIO
>>> browser.getControl(name='file_file').\
... add_file(cStringIO.StringIO('File contents'), \
... 'text/plain', 'test.txt')
>>> browser.getControl('Save').click()
>>> 'Uploaded file is not FLV' in browser.contents
True

We make sure that uploading anything but an FLV file is not allowed.

>>> import os
>>> pkg_home = os.path.dirname(pox.video.tests.__file__)
>>> samplesdir = os.path.join(pkg_home, 'samples')

Then get the location of a samples folder inside the tests sub-package. In that folder, there's a video.flv file that we will upload:

>>> browser.getControl(name='file_file').add_file( \
... file(os.path.join(samplesdir, 'video.flv')).read(), \
... 'application/x-flash-video', 'video.flv')
>>> browser.getControl('Save').click()
>>> 'Changes saved' in browser.contents
True

There's another upload attempt at the end of the test. Modify it as well:

>>> browser.getControl(name='title').value = 'Video Sample'
>>> browser.getControl(name='file_file').add_file( \
... file(os.path.join(samplesdir, 'video.flv')).read(), \
... 'application/x-flash-video', 'video.flv')
>>> browser.getControl('Save').click()
>>> 'Changes saved' in browser.contents
True

That's it. Test your instance to see if everything is okay.

See also

  • Changing the base class in paster content types
  • Subscribing to others' events

Modifying the view of a content type with jQuery

Normally, after creating a content type, you'd like to make some visual adjustments for users to view it according to the website specifications. Given that this is directly related to the theming phase, which is not covered in this article, we won't dive into those details. However, we will make some minor changes in the basic view template and supply an inline video player.

Getting ready

We will use Flowplayer (a GPL-licensed video player) to watch videos online.

There is a Plone add-on product named collective.flowplayer that works perfectly well for these requirements and it has even more options than we are developing here. The following is just for demonstration purposes. More information about this package is at: http://pypi.python.org/pypi/collective.flowplayer.

As suggested by the online documentation at http://flowplayer.org/documentation, we will use <a />-tagged HTML elements pointing to FLV files and turn them into video players via JavaScript code.

More information about Flowplayer is available at their website: http://flowplayer.org/.

How to do it…

  1. Install Flowplayer. Create a new flowplayer folder inside the browser sub-package of your product. Download Flowplayer and add the following files inside it (filenames may change according to the latest release):

    • flowplayer-3.1.5.swf: the base player.
    • flowplayer.controls-3.1.5.swf: a plugin with play button, stop button, and other controls. It's called automatically by the base FlowPlayer SWF file.
    • flowplayer.min-3.1.5.js: a minified JavaScript file containing all the necessary code to use Flowplayer.
  2. Create this video.js JavaScript file in the same browser folder:

    /* When DOM element is ready
    * call the following function
    */
    jQuery(document).ready(function() {
    /* Get all links, inside the automatically generated view
    * template, pointing to the download link of a FLV file
    */

    jQuery.each(jQuery('#archetypes-fieldname-file span
    a[href$=".flv/at_download/file"]'), function() {
    /* Get the jQueried link and
    * a copy of it to be inserted later
    */
    var jq = jQuery(this);
    var clone = jq.clone()

    /* Change some style options, like display (required)
    * width and height, depending of the video dimensions
    */
    jq.attr('style', 'display: block; width: 425px; height:
    300px;');
    /* Create a flash object based on given SWF file and passing
    * a config object to the *flashvars* parameter
    * Notice the use of encodeURIComponent to prevent flowplayer
    * from throwing an error while trying to load its controls
    * plugin
    */
    jq.flashembed(encodeURIComponent('++resource++pox.video.flowplayer/flowplayer-3.1.5.swf'), {
    config: {
    clip: {
    autoPlay: false,
    autoBuffering: true,
    url: this.href
    }
    }
    });
    /* Copy the link below the flash video player
    * to provide the user a download link
    */
    jq.after(clone);
    });
    });

  3. Register the new flowplayer directory available in the website. In the configure.zcml file in the browser package, add the following block:

    <browser:resourceDirectory
    name="pox.video.flowplayer"
    directory="flowplayer"
    />

  4. Add a new jsregistry.xml file inside the profiles/default folder of your product:

    <?xml version="1.0"?>
    <object name="portal_javascripts" meta_type="JavaScripts Registry"
    autogroup="False">
    <javascript cacheable="True" compression="safe" cookable="True"
    enabled="True" expression=""
    id="++resource++pox.video.flowplayer/flowplayer.min-3.1.5.js"
    inline="False"/>
    <javascript cacheable="True" compression="none" cookable="True"
    enabled="True" expression=""
    id="++resource++pox.video.flowplayer/video.js"
    inline="False"/>
    </object>

  5. Launch the Zope instance and re-install the product.

How it works…

The video.js file is a jQuery based JavaScript that adds a Flowplayer whenever it finds particular HTML elements in the web page. Refer to the inline comments to understand how it works.

jQuery JavaScript library is by default included with Plone

That file and the previous three ones mentioned in the preceding Step 1 should be available on our website to be used from the Video view template. To achieve this, we must define browser resources as in Step 3.

If we had a single or particular files to publish as browser resources, we could have used the <browser:resource /> directive for each of them instead of <browser:resourceDirectory />.

This change will let all files in the browser/flowplayer folder be available via a URL like http://localhost:8080/plone/++resource++pox.video.flowplayer/file.txt. This is what we have used in the video.js JavaScript above to create the embedded flash object.

Step 4 is to register the two new JavaScript files in portal_javascripts. This tool merges several JavaScript source files into a single one so that fewer requests are made by the browser.

The GenericSetup handlers, called during product installation, make it easy to add, remove, or modify configuration options in almost every tool available in Plone. These handlers can read XML files with particular syntax and translate them into the proper settings.

If you go to portal_javascripts tool, you'll be able to automatically make an association between every <javascript /> directive and every JavaScript resource in ZMI.

Once reinstalled, when opening a Video, you should see something like this:

There's more…

How could we test these little changes we have made? We could just try to access every resource's URL and check if they are available as intended. But would that be a real functional test from the user standpoint? Certainly not.

What the final user would like to test is whether uploaded videos are really playable online. Given that zope.testbrowser doesn't support JavaScript, we must create a Selenium test.

we must create a seleniumtests sub-package within tests:

cd ./src/pox.video/pox/video/tests/
mkdir seleniumtests
cd seleniumtests

The __init__.py file inside seleniumtests must import the tests modules we are going to run:

echo import testFlowPlayer >> __init__.py

Then we place the testFlowPlayer.py file inside the same folder. The source Selenium testing code of this file is not fully copied here due to its length, you can find it in the downloadable code associated with this article. However, we want to highlight the following line in the test:

self.failUnless(sel.is_element_present("//*[@id=\"archetypes-fieldname-file\"]/span/a/object"))

This is the way Selenium can verify if there's a specific HTML element inside a web page. The XPath expression here is almost the same as the one in the jQuery we used for fetching links to be converted in video.js of the previous section.

Feel free to check the whole testFlowPlayer.py for other details, like how to upload a file.

To run this test, you should have a running instance and then, in another shell, run the following command:

.\bin\seleniumRunner -s Products.poxContentTypes -i plone

In Windows, run:

python .\bin\seleniumRunner -s Products.poxContentTypes -i plone

See also

  • Creating the user interface for Zope 3 content types
  • Using Selenium functional tests

Summary

In this article we have covered the creation of Archetypes content types from scratch by hand... kind of. We will actually use paster to automatically create most of it.


Further resources on this subject:

 

 

 

 


Plone 3 Products Development Cookbook 70 simple but incredibly effective recipes for creating your own feature rich, modern Plone add-on products by diving into its development framework
Published: May 2010
eBook Price: $26.99
Book Price: $44.99
See more
Select your format and quantity:

About the Author :


Juan Pablo Giménez

Juan Pablo Giménez is a programmer with more than 10 years of expertise in the FOSS field. He started as a C programmer and Linux sysadmin but quickly turned to web development, founding and directing Rcom, an IT firm based in Rosario, Argentina. Five years ago he started working with Plone, his last and true love.

Marcos F. Romero

Marcos F. Romero has been a software developer since 1997. Since 1999 he has worked on numerous projects of sites and web applications. In 2007 he started to participate in Plone projects. For over 10 years he has been interested in Usability as a discipline applicable to everyday activities, which he can luckily actively employ in Inter-Cultura, a company that specializes in this discipline, where he has been working for several years.

Books From Packt


Plone 3 Multimedia
Plone 3 Multimedia

Plone 3 Site Administration
Plone 3 Site Administration

Plone 3 Intranets
Plone 3 Intranets

Plone 3 for Education
Plone 3 for Education

Plone 3 Theming
Plone 3 Theming

jQuery 1.4 Reference Guide
jQuery 1.4 Reference Guide

Practical Plone 3: A Beginner's Guide to Building Powerful Websites
Practical Plone 3: A Beginner's Guide to Building Powerful Websites

Wordpress and Flash 10x Cookbook
Wordpress and Flash 10x Cookbook

Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software