A Simple AT Product
Plone Developer Manual is a comprehensive guide to Plone programming.
1. Introduction
Introducing a sample AT Product and the contents of the tutorial.
In this part of the manual, we discuss a sample AT Product to explain CMF/Archetypes practices. We will be building a product called example.archetype, which will implement a content type (InstantMessage) that members with specific rights can use to add messages readable by other members. However, as you may have guessed, this is more a learning example than a usable product for a real website application.
What is a Product ? A product - a Zope product to be precise - is a third party add-on that can be integrated to provide additional functionality. It is a code package written using the Python language and conventions.
In order to understand this section you will need to have some prior knowledge of working on the file system and programming protocols common to Python and Zope.
The example.archetype product features the following CMF and Archetypes capabilities:
- basic fields and widgets;
- defining and using a vocabulary for a field with a selection widget;
- defining specific "Add" permissions for the contents.
The code of the product can be downloaded here: http://plone.org/products/example.archetype/
2. Product package layout
Conventions and techniques for organizing the package for an AT product.
Following Zope, Plone and AT's conventions, the content of our example product pakage will look like this:
- __init__.py
- configure.zcml
- config.py
- interfaces.py
- content
- __init__.py
- message.py
- profiles
- default
- browser
- __init__.py
- configure.zcml
- instantmessage.pt
- tests
- __init__.py
- base.py
- test_setup.py
What is the purpose of these files and directories?
- __init__.py: The usual "Python package" initialization module;
- configure.zcml: Using Zope's new Configuration Markup Language (ZCML), this file configures the services or behaviour the Zope server needs to load at startup;
- config.py: Provides configuration variables for the product;
- interfaces.py: Where you define interfaces describing what the packages' classes will do;
- content: Contains the modules providing the implementation of the content types.
In this case, it contains the message.py file where the
'InstantMessage' class should be defined;
- profiles/default:
Contains a set of XML files that are needed to provide the settings that will be used by Plone's Quick-Installer tool when installing the product within Plone; this is what we call an Extension Profile, an artifact of Zope CMF's GenericSetup technology. Note that this replaces the old way of doing based on the Extensions/Install. More precisely, since Plone 3.0, you do not need that old-style technique;
- browser: The sub-package where the developer can add specific presentation code such as browser views and templates; the contained configure.zcml is used to provide these components registration.
- tests: Contains the unit tests code for the product.
If you have ZopeSkel installed, you can use the following command to create a similar structure:
paster create -t archetype example.archetype
Now we will go through the files one by one and add what we need to produce our application.
3. The interfaces module
The module where you define interfaces describing what your content class(es) will do.
Why do you need interfaces?
Interfaces are useful to describe what a class will do. They are a kind of contract between a class and the components that class interact with. Starting a content management functionality package with writing interfaces is recommended practice as it helps document your code. In addition to that, Zope Component Architecture (ZCA) allows us to use interfaces as components for adapting a class (which is useful as new user requirements appear) and thus specializing its behaviour.
The interface for the Instant Message class
This is done by convention in the interfaces.py file, that you need to add at the root of the package.
First, we need an import from Zope's zope.interface module, which is included into Zope 2's distribution since version 2.8:
from zope.interface import Interface
Following ZCA naming conventions (interface names start with an I), we define the IInstantMessage interface we need for the InstantMessage class that we will define later:
class IInstantMessage(Interface):
"""
Interface for the InstantMessage class.
"""
That's it!
We could add attribute definitions to it using the zope.interface.Attribute class, but this is not mandatory. When an interface is defined as above, without any function nor attribute, we call it a "marker interface" meaning that it will be used simply to "mark" the instances of the class that implements it.
More information about interfaces in the context of Archetypes can be found in the b-org tutorial - Interfaces section. For a detailed presentation of interfaces and their usage patterns, read the doctests document available from Zope's documentation site.
4. The configuration module
The configuration details for your content type, in config.py.
First, we have to import a class from Archetypes:
from Products.Archetypes.atapi import DisplayList
Displaylist is a data container we use when displaying
pulldowns/radiobuttons/checkmarks with different choices. Let's say we wanted
priorities on our instant messages, and we wanted those to be High, Normal
and Low. We will specify these later in the file.
The next two lines set the project (Product in Zope) name, and point to the
skin directory. PROJECTNAME should reference the name of the package: example.archetype.
PROJECTNAME = "example.archetype"
Now, we need to specify our 'Priority' pulldown. It should look like this, using the DisplayList utility class that Archetypes has provided for exactly that purpose:
MESSAGE_PRIORITIES = DisplayList((
('high', 'High Priority'),
('normal', 'Normal Priority'),
('low', 'Low Priority'),
))
Python notes:
-
The reason for double parantheses is that DisplayList is a class that you pass a tuple of tuples to.
We also need to define the "Add" permission(s) for the content type(s):
ADD_CONTENT_PERMISSIONS = {
'InstantMessage': 'example.archetype: Add InstantMessage',
}
We recommend using the standard way of naming permissions: '<ProductName>: <Permission>'. This will group the related permissions together within the ZMI (Security tab), and allow the Administrator to recognize which permissions belong to which Product.
Note that, unless you have an advanced case which needs custom security settings, you don't need to define your own permissions for the "edit" and "view" of the content. In this simple case you will just reuse, in the modules where needed, the generic permissions defined in CMFCore.permissions: "View", "Modify portal content"...5. The startup module
The initialization module (__init__.py) provides the script that is run when Zope is started.
Before starting the usual Zope product initialization code, we need to define a Message Factory for when this product is internationalized.
from zope.i18nmessageid import MessageFactory
exampleMessageFactory = MessageFactory('example.archetype')
The defined MessageFactory object will be imported with the special name "_" in most modules, and strings like _(u"message") will then be extracted by i18n tools for translation.
Now, we import some useful stuff from the Archetypes API:process_types is useful to get the product's content types,
associated constructors, and Factory Type Information (FTI) data structures, while listTypes can be used to list the types available in the product.
We also need to import the utils module from CMFCore to be able to use its ContentInit class later.
from Products.Archetypes.atapi import process_types from Products.Archetypes.atapi import listTypes from Products.CMFCore import utils
Python notes:
-
Factory Type Information (FTI): Part of a CMF portal's configuration, the FTI for a content type is the data structure that holds the information needed to expose a content type within the portal. From the integrator's perspective, the FTI is the object (Factory-based Type Information object) within the portal_types component that tells CMF and Plone how to create a content from the type and how to display it.
-
How exactly does 'listTypes' work: See those registerType() calls in your content type modules? Notice how we also import those modules (but do nothing with the import) in the 'content' package's __init__.py. The registerType() call tells AT about the type so that listTypes() can find it later.
from content import messageNow, we import the configuration module, in order to have access to the variables it contains, such as the "Add" permission setting:
import config
Now for the real action. You define a function that is required by Zope and CMF internals to initialize our content type(s):
def initialize(context):
The first part of the code of this function generates the content types, the constructors and the Factory-based Type Informations (or FTIs) required to make your types work with the CMF:
content_types, constructors, ftis = process_types(
listTypes(config.PROJECTNAME),
config.PROJECTNAME)
The second part instantiates an object of the class ContentInit (from CMFCore), and registers your types in the CMF:
utils.ContentInit(
"%s Content" % config.PROJECTNAME,
content_types = content_types,
permission = config.ADD_CONTENT_PERMISSIONS['InstantMessage'],
extra_constructors = constructors,
fti = ftis,
).initialize(context)
Handling several content types
There is a better way to write the code that initializes the content type class with its "Add" permission and constructor, so that it still works if you define several content types. This is useful if you plan to later augment your product with additional types.
Here is the improved code:
def initialize(context):
content_types, constructors, ftis = process_types(
listTypes(config.PROJECTNAME),
config.PROJECTNAME)
# We want to register each type with its own permission,
# this will afford us greater control during system
# configuration/deployment (credit : Ben Saller)
allTypes = zip(content_types, constructors)
for atype, constructor in allTypes:
kind = "%s: %s" % (config.PROJECTNAME, atype.portal_type)
utils.ContentInit(kind,
content_types = (atype,),
permission = config.ADD_CONTENT_PERMISSIONS[atype.portal_type],
extra_constructors = (constructor,),
fti = ftis,
).initialize(context)
Python notes:
-
We can use the "ADD_CONTENT_PERMISSIONS[atype.portal_type]" construct because ADD_CONTENT_PERMISSIONS references a dictionary in which the keys are the potential content types names.
-
The zip() function is a Python built-in that pairs up elements of two lists. In this case, "allTypes" will be a list of tuples containing a content type from "content_types" and the corresponding constructor from "constructors".
-
If you have several content types, you should not forget to import each content module, as is done for the message example discussed here !
6. The content package and its modules
Now we are ready for the core of the product, i.e. the content class definition module (content/message.py).
Since it provides a Python (sub)package, the 'content' directory contains 2 modules:
- the usual __init__ module that initializes the package,
- the message module (message.py) where we will define the 'InstantMessage' class.
The message module
First imports we need
We start the message module by adding the general Zope-related imports we need, such as the implements function from the zope.interface module:
from zope.interface import implements
We need to use a few classes and/or functions provided by
the core of our codebase, i.e. CMF/Archetypes. It is possible to have access to all the classes and helper functions
made
publicly available by Archetypes, by importing its façade or API module (Products.Archetypes.atapi) this way:
from Products.Archetypes import atapi
i18n support
It is always a good idea to have an i18n-enabled application. To start using Zope's i18n support, let's import the MessageFactory object created in the product's startup module:
from example.archetype import exampleMessageFactory as _
The MessageFactory referenced with the _ symbol can now be used to provide i18nized labels, descriptions, and all the miscellaneous text snippets that are injected in the UI, also known as "messages". For a content type implementation, this is useful for UI widgets; for example to define the label of the content title field widget, we could define label = _(u'Title'). (See later for how we make use of this tool/practice.)
ATContentTypes-based schema definition
You can base your implementation directly on these stock Archetypes schemas. But you can add better support for Plone's UI and content management policies (such as the parameters that allow showing/hiding contents in the navigation menu), by basing the implementation on ATContentTypes' base schema, ATContentTypeSchema. To be compatible with that schema, you will also need to inherit from ATContentTypes' ATCTContent base class.
Let's add the import of modules we need for that:
from Products.ATContentTypes.content import base from Products.ATContentTypes.content import schemataThen, we import things internal to our product package, such as our defined interface(s) and the configuration module (for access to things such as
PROJECTNAME and MESSAGE_PRIORITIES):
from example.archetype.interfaces import IInstantMessage from example.archetype import config
Now, we have everything we need to start building the schema, and then the class that will use it. We start out by copying ATContentTypes' ATContentTypeSchema, and we extend it by adding our specific fields and/or overriden field properties.
schema = schemata.ATContentTypeSchema.copy() + atapi.Schema((
atapi.StringField('priority',
vocabulary = config.MESSAGE_PRIORITIES,
default = 'normal',
widget = atapi.SelectionWidget(label = _(u'Priority')),
),
atapi.TextField('body',
searchable = 1,
required = 1,
allowable_content_types = ('text/plain',
'text/structured',
'text/html',),
default_output_type = 'text/x-html-safe',
widget = atapi.RichWidget(label = _(u'Message body')),
),
))
Notes:
-
To instantiate an Archetypes schema object, you pass a tuple of field objects to the 'Schema' class.
We define the body of the InstantMessage object using a RichWidget, so the user can use formatting with a WYSIWYG editor.
The full list of out-of-the-box available Fields and Widgets can be found in the Fields section at the end of the manual. You can find more 3rd party fields and widgets here.
Content-type class definition
The last step is to create the class for the InstantMessage content. It inherits from ATContentTypes' ATCTContent, which itself is based on AT's BaseContent, which automatically gives its 'id' and 'title' attributes, and the entire Dublin Core metadata set (Title, Description, Creator, CreationDate, etc):
class InstantMessage(base.ATCTContent):
"""An Archetype for an InstantMessage application"""
implements(IInstantMessage)
schema = schema
The first information we add for the class definition is saying that it implements the IInstantMessage interface that we have previously defined (in interfaces.py) and imported.
implements(IInstantMessage)
The next thing is assigning the reference of the Archetypes schema, using the schema class attribute.
schema = schema
The content class definition is done. Now, we are ready to activate the content type in Archetypes'
internal types registry. This is done using the helper function called registerType.
atapi.registerType(InstantMessage, config.PROJECTNAME)
Congratulations! You have just created your first Archetype for Plone! It allows you to handle the content of an instant message with Zope-based persistent objects which:
- can be added within your Plone site,
- published by the Zope Publisher, which means you can visit them via their URLs, etc...
- searched since they are automatically indexed,
- etc...
But wait! You have some final packaging work to do to ease installation of the product within your Plone site.
Notes:
-
At the content class level, you could also provide the 'actions' attribute useful for defining the settings of the type's actions (for the portal_actions tool). In Plone 3, this is no more needed, since this is part of the FTI's configuration details, and should be provided using GenericSetup, in the types-related XML files (i.e. 'profiles/default/types/InstantMessage.xml'). Same for the aliases.
The __init__ module
The trick here is to simply import the message module so that all the code of that module gets interpreted as soon as the Python interpreter initializes the package.
import message
7. Adding a custom view for the content
Providing the custom presentation template for the InsantMessage, using Zope's browser layer mechanism.
The browser layer concept
A browser layer is a concept introduced by Zope Component Architecture (Zope 3), and which can be used in Plone. It is useful for registering views and resources (images, CSS, JS) for the site, in a way that they can override default elements (which are implicitly registered for the default browser layer) or be overriden when needed, even through the ZMI. A browser layer is similar in purpose to a CMF skin layer, but is implemented differently.
To add a browser layer to your product, you need 3 steps:
- Define the marker interface for the browser layer (for example,
example.archetype.interfaces.IInstantMessageSpecific.) - Add an XML file in your extension profile named
browserlayer.xmlproviding the browser layer settings to the site. (This step is covered later as part of the various product setup details.) - Register (using ZCML) your browser views, templates and resources.
Defining the browser layer interface
Add a marker interface for the browser layer (in interfaces.py):
from plone.theme.interfaces import IDefaultPloneLayer class IInstantMessageSpecific(IDefaultPloneLayer): """Marker interface that defines a Zope 3 skin layer for this product. """
Adding and registering the browser template
To provide a custom view template for your content type, you need a page template called
instantmessage.pt in the browser/ directory, and a ZCML declaration in the configure.zcml to associate the template to the IInstantMessageSpecific Zope 3 skin layer.
<configure xmlns="http://namespaces.zope.org/zope" xmlns:browser="http://namespaces.zope.org/browser" i18n_domain="example.archetype" > <browser:page for="example.archetype.interfaces.IInstantMessage" layer="example.archetype.interfaces.IInstantMessageSpecific" name="instantmessage_view" template="instantmessage.pt" permission="zope2.View" /> </configure>
Here is the example template code:
<html metal:use-macro="here/main_template/macros/master"
i18n:domain="plone" >
<body>
<div metal:fill-slot="main"
tal:define="priority here/getPriority;
priority_color python:(priority == 'high' and 'red') or (priority == 'low' and 'green') or ''" >
<h1 tal:content="context/Title"
tal:attributes="style string:color:$priority_color" >
Title
</h1>
<p tal:content="structure here/getBody" />
<div class="documentByLine">
Message by <span tal:content="context/Creator" />
with <strong tal:content="priority" /> priority.
-
<span tal:replace="python:here.toLocalizedTime(context.CreationDate(),long_format=1)" />
</div>
</div>
</body>
</html>
Python notes:
-
The new methods we use on the content object (getPriority, getBody, etc), called the "accessors", are generated by Archetypes as part of its internal mechanisms, based on the field definition in the content schema; so if the field is called 'priority', there is a generated method called 'getPriority' responsible to return the stored value on the object. Note that the code of the method is not available somewhere for modification ; "generated" here means it is available in the server's memory, within Archetypes engine's registries, when the Zope server has started.
After the product installation step, which we still have to discuss (see later), Plone should be able to find this template and use it as the content object's default view when you invoke the content's URL.
8. Installing the product
Ensuring the product elements (types, browser layers, resources) are correctly installed.
In this part, we will provide the code to be executed when the integrator "adds", i.e. installs, the InstantMessage product to the Plone site. This aspect of the product code is called the "Extension Profile" (or "Setup Profile") and is managed under the hood by a machinery called GenericSetup.
For more about GenericSetup, its possibilities, and how a developer uses it, read the GenericSetup tutorial.The setup profile files (profiles/default)
The setup profile is composed of a set of GenericSetup XML files containing setup declarations.
Type declaration and definition
First, we provide the files needed for adding the types to CMF's types registry (portal_types): types.xml and types/InstantMessage.xml.
In types.xml, within the <object name="portal_types" ... /> element, add the setup code for the type(s) you want to install:
<?xml version="1.0"?>
<object name="portal_types" meta_type="Plone Types Tool">
<property
name="title">Controls the available content types in your portal</property>
<object name="InstantMessage"
meta_type="Factory-based Type Information with dynamic views"/>
</object>
The name property of the <object> node constitutes the called portal type name of the content-type, a CMF concept which supports two things:
- Dynamic typing: objects can change their content
type during their lifetime. To do this use
_setPortalTypeName(<type>). - You can have arbitrarily many different content types
using the same base class (and having therefore the same
meta_type) but differing in their Factory Type Information (FTI) settings.
The portal type name was formerly set in a content-type class attribute called portal_type, which is no longer necessary.
The name of the file inside the profiles/default/types folder must match the portal type name, with spaces converted to underscores whenever necessary. So, in types/InstantMessage.xml, add the code for the InstantMessage FTI object:
<?xml version="1.0"?>
<object name="InstantMessage"
meta_type="Factory-based Type Information with dynamic views"
i18n:domain="example.archetype" xmlns:i18n="http://xml.zope.org/namespaces/i18n">
<property name="title" i18n:translate="">Example AT - InstantMessage</property>
<property name="description"
i18n:translate="">An example type (InstantMessage) discussed in the AT Developer Manual.</property>
In these first lines we give the content-type a title and a description.
The title property indicates the user-friendly name of the content-type. This is what's supposed to be used in the user
interface, and can be accessed using the <fti>.title_or_id() or the Type() methods, which
both return the content-type title if it exists or the content-type id
otherwise. Like portal type, this property was formerly set in a the content-type class attribute called archetype_name, which is no longer neccessary.
<property name="content_meta_type">InstantMessage</property> <property name="content_icon">document_icon.gif</property> <property name="product">example.archetype</property> <property name="factory">addInstantMessage</property>
The meta_type property of the object is a Zope concept to organize object classification or containment.
For historical reasons, it is used in
CMF in some places because first versions of
CMF didn't have today's portal_type. Also note that Archetypes uses the content-type class name as the meta_type value, unless given explicitly.
The content_icon property specifies the icon image file which will be shown in the Plone UI for this content-type. This icon image file must be accessible from the context of the content-type, and
therefore should be placed into a CMF skin layer (the CMF way) or in a browser resource directory (the Zope 3 way).
The factory property indicates the factory function which
will be used to create and initialize new content objects of this type.
This factory is automatically generated by the Archetypes framework,
when the product is initialized (via the code in the startup module), and is always named add<content-meta-type>. The factory is also associated with a certain product by means of the product property.
<property name="immediate_view">atct_edit</property> <property name="global_allow">True</property> <property name="filter_content_types">False</property> <property name="allow_discussion">False</property>
The global_allow property determines if the content-type will be available to be added from anywhere in the site.
The filter_content_types property, paired with allowed_content_types, controls what content-types will be addable inside the current one.
With allow_discussion, we specify whether or not comments will be allowed by default on this content-type.
<property name="default_view">@@instantmessage_view</property>
<property name="view_methods">
<element value="@@instantmessage_view" />
</property>
<alias from="(Default)" to="@@instantmessage_view" />
<alias from="edit" to="atct_edit" />
<alias from="sharing" to="@@sharing" />
<alias from="view" to="@@instantmessage_view" />
Here we define CMF views (templates) and aliases that map content-type methods to views.
<action title="View" action_id="view" category="object" condition_expr=""
url_expr="string:${object_url}/" visible="True">
<permission value="View" />
</action>
<action title="Edit" action_id="edit" category="object" condition_expr=""
url_expr="string:${object_url}/edit" visible="True">
<permission value="Modify portal content" />
</action>
</object>
The <action> elements register type-specific actions for the content-type. The object category makes the render as tabs in the Plone UI.
- The
url_expris a TALES expression that defines the URL from where the action will be triggered and should match one of the method aliases defined above. Hence, theeditaction points tostring:${object_url}/edit, which means that if you are at/path/to/objectand clickedit, you will go to/path/to/object/edit./editthen gets recognized as a method alias, which points to the page templateatct_edit, causing Zope to render/path/to/object/atct_edit. - The
<permission /> element specifies a guard permission for this
action. If the user's role doesn't have this permission, the action
won't be available and the corresponding action tab won't be shown.
- In addition to the former criteria, the
condition_expris a TALES expression which will be evaluated to decide if the action is available or not. - The
visibleattribute indicates wheter the action tab will be visible or hidden. If it's set to False, the tab won't appear even when the action is available, but the exposed page will still be accesible from the associated URL.
- Defining new content-type actions this way, i.e. using GenericSetup, supersedes the old
updateActionsfunction fromATContentTypes.content.base. - Don't worry. You don't have to type all this XML each time you create a new content-type; since most of it is boilerplate (XML is very verbose) you can copy & paste an already working example (like the CMFPlone ones) and modify only the changing bits.
Type factory
We also need the file useful for setting the type against Plone's factory tool (portal_factory): factorytool.xml.
This is needed so that when a user adds a content object and then
clicks Cancel in the edit form, a stale object won't be lying around.)
<?xml version="1.0"?> <object name="portal_factory" meta_type="Plone Factory Tool"> <factorytypes> <type portal_type="InstantMessage"/> </factorytypes> </object>
Roles - Permissions mapping
For our content type(s) to be usable, we need to assign the required "Add" permission to the Plone site's default roles: Contributor, Owner, and Manager. This is done using the rolemap.xml file as follows:
<?xml version="1.0"?>
<rolemap>
<permissions>
<permission name="example.archetype: Add InstantMessage" acquire="True">
<role name="Manager"/>
<role name="Owner"/>
<role name="Contributor"/>
</permission>
</permissions>
</rolemap>
Browser skin layer
In order to install our browser skin layer, we also add a browserlayer.xml file with the following code:
<?xml version="1.0"?>
<layers>
<layer name="example.archetype"
interface="example.archetype.interfaces.IInstantMessageSpecific" />
</layers>
Registering our setup profile
This last step ensures everything can work. We update the package's configure.zcml file with the code snippet that will load the extension profile:
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:five="http://namespaces.zope.org/five"
xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
i18n_domain="example.archetype" >
<five:registerPackage package="." initialize=".initialize" />
<include package=".browser" />
<genericsetup:registerProfile
name="default"
title="Example Archetype content - InstantMessage"
directory="profiles/default"
description="Extension profile for Example AT - InstantMessage"
provides="Products.GenericSetup.interfaces.EXTENSION"
/>
</configure>
Restarting Zope
Now that you have a first version of your product ready to be tested, and installed via your buildout, you need to (re)start Zope.
Quick-installing the product
Back in the Plone configuration (or Plone control panel), when you visit the "Add/Remove Products" interface or the portal_quickinstaller tool through the ZMI (at the root of the site), you can see the product show up under the category of "installable products".
Select and click the button to install the product. If everything goes fine, the product should be installed, and you're ready to start using it!
9. Basic integration tests
No product is complete without tests.
To build high-quality software, you must provide automatic tests - often known as "unit" tests (though tests for Archetypes products tend to be "integration" tests, strictly speaking).
The tutorial on testing and test-driven development is essential reading if you want to write high-quality software (and you don't know the techniques it advocates already). Please refer to it for details.
The example.archetype product contains basic tests that prove that the product is properly installed, that it registers its types, and that an InstantMessage object can actually be instantiated. If it contained more functionality, there would have been more tests, but even simple integration tests like this can be surprisingly useful - if you accidentally broke the content type with some change, you'd notice that it failed to install or instantiate.
The tests are in the "tests" directory. The file "base.py" contains some base classes that are used for tests, to ensure the site is properly set up:
import unittest
from zope.testing import doctestunit
from zope.component import testing
from Testing import ZopeTestCase as ztc
from Products.Five import zcml
from Products.Five import fiveconfigure
from Products.PloneTestCase import PloneTestCase as ptc
from Products.PloneTestCase.layer import PloneSite
from Products.PloneTestCase.layer import onsetup
@onsetup
def setup_product():
"""Set up the package and its dependencies.
The @onsetup decorator causes the execution of this body to be deferred
until the setup of the Plone site testing layer. We could have created our
own layer, but this is the easiest way for Plone integration tests.
"""
fiveconfigure.debug_mode = True
import example.archetype
zcml.load_config('configure.zcml', example.archetype)
fiveconfigure.debug_mode = False
ztc.installPackage('example.archetype')
setup_product()
ptc.setupPloneSite(products=['example.archetype'])
class InstantMessageTestCase(ptc.PloneTestCase):
"""Base class for integration tests.
This may provide specific set-up and tear-down operations, or provide
convenience methods.
"""
The actual tests are in "test_setup.py":
from base import InstantMessageTestCase
from example.archetype.interfaces import IInstantMessage
class TestProductInstall(InstantMessageTestCase):
def afterSetUp(self):
self.types = ('InstantMessage',)
def testTypesInstalled(self):
for t in self.types:
self.failUnless(t in self.portal.portal_types.objectIds(),
'%s content type not installed' % t)
def testPortalFactoryEnabled(self):
for t in self.types:
self.failUnless(t in self.portal.portal_factory.getFactoryTypes().keys(),
'%s content type not installed' % t)
class TestInstantiation(InstantMessageTestCase):
def afterSetUp(self):
# Adding an InstantMessage anywhere - can only be done by a Manager or Portal Owner
self.setRoles(['Manager'])
self.portal.invokeFactory('InstantMessage', 'im1')
def testCreateInstantMessage(self):
self.failUnless('im1' in self.portal.objectIds())
def testInstantMessageInterface(self):
im = self.portal.im1
self.failUnless(IInstantMessage.providedBy(im))
def test_suite():
from unittest import TestSuite, makeSuite
suite = TestSuite()
suite.addTest(makeSuite(TestProductInstall))
suite.addTest(makeSuite(TestInstantiation))
return suite
To run these tests within your buildout environment:
./bin/instance test -s example.archetype
You may see output like:
Ran 4 tests with 0 failures and 0 errors in 0.119 seconds.
If there was an error with one or more of the tests, you'd be told here!
Please refer to the testing tutorial for more about writing tests - and writing good tests - and how to run them.
10. Troubleshooting
When creating new content types, many factors can silently fail due to human errors in the complex content type setup chain and security limitations. The effect is that you don't see your content type in Add drop down menu. Here are some tips for debugging.
1. Is your product broken due to Python import time errors? Check the *Zope Management Interface (ZMI from now on) â Control Panel â Products*. Turn on Zope debugging mode to trace import errors. To see error messages directly in the console with buildout, use *bin/instance fg*.
2. Have you rerun the quick installer (GenericSetup) after creating/modifying the content type? If not, (re)install the product from the *Plone Control Panel â Add-on Products* or from the *ZMI â portal_quickinstaller*.
3. Do you have a correct Add permission for the product? Check the call of the ``ContentInit()`` method inside the *__init__.py* file. See `The startup module <http://plone.org/documentation/manual/archetypes-developer-manual/a-semi-realistic-example/the-startup-module>`_.
4. Does it show up in the portal factory? Check *ZMI â portal_factory* and *factorytool.xml*.
5. Is it correctly registered as a portal type and implictly addable? Check *ZMI â portal_types*. Check *default/profiles/type/yourtype.xml*.
6. Does it have a correct product name defined? Check *ZMI â portal_types*.
7. Does it have a proper factory method? Check *ZMI â types_tool*. Check Zope logs for ``_queryFactory`` and import errors.
8. Does it register itself with Archetypes? Check *ZMI â archetypes_tool*. Make sure that you have ``ContentInit`` properly run in your *__init__.py*. Make sure that all modules having Archetypes content types defined and ``registerType()`` calls are imported in *__init__py*.
