Development Environment
Be sure to have a python in the 2.4 branch installed
Make sure you have subversion installed
Install easy_install
$ wget http://peak.telecommunity.com/dist/ez_setup.py
$ sudo python2.4 ez_setup.pyInstall ZopeSkel
$ sudo easy_install -U ZopeSkelThe -U here tells easy_install to update the package if it's already installed and there's a new version
Create a new buildout
I like to put my buildouts on a VM in a directory called /opt/buildouts
You may need to do this as root and set permissions accordingly
$ mkdir /opt/buildouts
$ cd /opt/buildouts
$ paster create -t plone3_buildout addressbookdev
[answer questions]
Some hints on the questions:
Enter zope2_install (Path to Zope 2 installation; leave blank to fetch one) ['']:Normally leave this blank
Enter plone_products_install (Path to directory containing Plone products; leave blank to fetch one) ['']:Normally leave this blank
Enter zope_user (Zope root admin user) ['admin']:
Enter zope_password (Zope root admin password) ['']: admin
Enter http_port (HTTP port) [8080]:
It's pretty easy to change the username, password, port, debug mode, verbose security in
buildout.cfglaterEnter debug_mode (Should debug mode be "on" or "off"?) ['off']: on
Enter verbose_security (Should verbose security be "on" or "off"?) ['off']: onI like to turn on verbose secruity and debug mode since this is a development buildout
Requirements
Project Description
A basic list of contacts. The list needs to be searchable, and provide a simple management interface. Search results need to be exportable in multiple formats. Contacts should be categorized with an affiliation.
There should probably be a section here about spec'ing the data requirements, perhaps with an ER diagram as a means of illustrating the data model.
Actors
Based on the project description, define the actors involved. Don't forget to include other systems that may be interfacing (RDBMS, desktop apps, web services, etc) as actors as well.
These actors can help define necessary roles in the Plone/Zope context.
Manager: Global administrator.
User: General user with access to view contacts
Administrator: Person who handles the organization of the contacts
Use Case Diagram
Using the project description, the high level use cases are defined for each actor. Use stories or use cases written as prose would also be acceptible here (and more so in some circumstances)
I opted to roll Add/Delete/Modify of the major content types into common "Manage" use cases. If the permissions for each action (add/delete/modify) were more granular, it would be smart to define them all explicitly in the use case diagram.

Class Diagram
Using the use cases, you identify the major content types that will be in use in the system, and start to work on the implementation details in the Class Diagram.
The class diagram is also a good place to define the different browser views that will be used
The Class Diagram shows the classes and packages that will be created in the next steps, and how they interrelate. Use composition to show containment. Perhaps aggregation would be more appropriate?
Since browser views can be tied to a specific interface, I use realizations to show which classes have which views available. I stereotyped the view classes with <<browser page>> to indicate that they were going to be implemented as Zope3-style browser pages (ZopeSkel will implement the views in this manner later)
Code Skeleton
Create the main product skeleton in the
srcdirectory of your buildout$ cd /opt/buildouts/addressbookdev/src
$ paster create -t archetype cpc.addressbookThe name specified here [
cpc.addressbook] has to correspond to the namespace [cpc] and package name [addressbook] that the archetype template will ask for.I like to use "cpc" as the namespace since it identifies the product as CPC sponsored development
[answer questions]Some hints on the questions:
Most of these parameters are specific to the egg you are creating. They're always a good idea to fill out, but if you don't plan on distributing your product via the cheesshop they're not always important. Most of these parameters can be changed later in the
setup.pyfile that the archetype template puts in thr root of your egg.Enter title (The title of the project) ['Plone Example']: CPC AddressbookRemember this is the name of the egg, not your product
Enter namespace_package (Namespace package (like plone)) ['plone']: cpcIt's important that this correspond to the first part of the name you specified earlier
Enter package (The package contained namespace package (like example)) ['example']: addressbookIt's important that this correspond to the second part of the name you specified earlier
Enter zope2product (Are you creating a Zope 2 Product?) [False]: TrueIf you're developing for plone, you will almost always want to say
TruehereEnter version (Version) ['0.1']:
Enter description (One-line description of the package) ['']: Maintain a list of contacts
Enter long_description (Multi-line description (in reST)) ['']:
Enter author (Author name) ['Plone Foundation']: Josh Johnson
Enter author_email (Author email) ['plone-developers@lists.sourceforge.net']: jj@email.unc.edu
Enter keywords (Space-separated keywords/tags) ['']: address book cpc addressbook contacts phone directory
Enter url (URL of homepage) ['http://svn.plone.org/svn/plone/plone.example']: http://www.unc.edu/~jj/plone/index.html
Enter license_name (License name) ['GPL']:
Again, this is all egg-specific stuff, that you can change in
setup.pylaterEnter zip_safe (True/False: if the package can be distributed as a .zip file) [False]:Zope won't be able to use this product unless this is set to
False, since it can't get to the configure.zcml inside of a zipped up egg.
Create skeleton code for each content type.
Call
paster addcontentfrom the root of your eggThis won't work anywhere else. This is because when you use the
archetypetemplate, it creates paster plugins in your main egg that allow these special templates to be invoked$ cd /opt/buildouts/addressbookdev/src/cpc.addressbook
$ paster addcontent contenttype
[answer questions]Hints on the questions:
Enter contenttype_name (Content type name ) ['Example Type']: Contact ListThis corresponds to the "friendly" name of the content type
What you put here is concated as camel-case, which (should) match what was in the UML diagram
Enter contenttype_description (Content type description ) ['Description of the Example Type']: A folder containing contact information, an address bookThis is used in the tool tip for the content type's "Add New..." link, so keep your users in mind here
Enter folderish (True/False: Content type is Folderish ) [False]: TrueThis type contains other content, so it's "Folderish"
Enter global_allow (True/False: Globally addable ) [True]:Can the content be added anywhere in the plone site?
Enter allow_discussion (True/False: Allow discussion ) [False]:Should the content type have it's built-in discussion forum turned on by default?
This will create a python package at cpc.addressbook.content.XXXXXX, where XXXXX is the name of the class you specified, in lower case (so, for the example above, cpc.addressbook.content.contactlist). It will define an archetype-based class in that module corresponding to the name you specified verbatim (ContactList)
Create the skeleton for each view
$ paster addcontent viewOnly one question is asked, the name of the view. Use the name of the class you specified in the Class Diagram. This will create a python package at cpc.addressbook.browser.xxxxxxview, where xxxxxx is the lowercased name that you specified. It will contain a corresponding template with the name xxxxxxview.pt.
Wire your egg into your buildout
Add the following lines to
/opt/buildouts/addressbookdev/buildout.cfg:[buildout] ... eggs = cpc.addressbook ... develop = src/cpc.addressbook ... [instance] zcml = cpc.addressbookBootstrap and Build Out
$ cd /opt/buildouts/addressbookdev
$ python2.4 bootstrap.py
$ bin/buildoutTest the skeleton using a functional test.
I'm implementing this early test as a doctest. The code generated in
/opt/buildouts/addressbookdev/src/cpc.addressbook/cpc/addressbook/tests.pygives an example of how to implemnt the various kinds of tests, and handles the usual boilerplate setup/teardown stuffThere are many different ways to implement tests. This doesn't necessarily reflect best practices.
Move the
tests.pyfile into thetestsdirectory and rename it totemplate.pyThis allows you to just copy it as a basis for each test suite
$ cd /opt/buildouts/addressbookdev/src/cpc.addressbook/cpc/addressbook/
$ mv tests.py tests/template.pyMake a copy of
template.pyfor this first functional testIt's important that any test suites in this directory have file names that begin with the word "test"
Feel free to be as descriptive as possible with the names of the test suites. This will help you keep track of what each one does.
$ cd tests
$ cp template.py test_basicfunctionality.pyCreate the docttest file someplace that makes sense.
$ mkdir doctests
$ touch basicfunctionality.txtEdit the new test suite so it picks up your doctest file.
For more information, see Martin Aspeli's Testing Tutorial @ plone.org
All I did to the basic template was comment out the fucntional doctest example, and put in the path to my doctest file.
I also added an after setup method where it creates a new user just for testing.
Aparently the paths are relative to the product root, so I had to specify
tests/doctests/basicfunctionality.txtinstead of the path relative to the location of the test suiteI also specified some options to the doctest suite. I'm telling it to report only the first failure, normalize white space, and treat elpisis (...) as an indication I want them to match any character.
I had to change the approach here, the way the tests.py file is doing it, installing the product in the layer, wasn't working with
PloneTestCase.setupPloneSite(), which is necessary to get your product to installPaste this code into
/opt/buildouts/addressbookdev/src/cpc.addressbook/cpc/addressbook/tests/test_basicfunctionality.pyimport unittest import doctest 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 ptc.setupPloneSite() import cpc.addressbook from Products.PloneTestCase.layer import onsetup, onteardown class TestCase(ptc.FunctionalTestCase): def afterSetUp(self): """ set up our test user """ # add one manager user for doing broad functional tests self.portal.acl_users._doAddUser('test', 'test', ['Manager'], []) @onsetup def setup_product(): """ Install our product in the eyes of Five and Zope 2 """ fiveconfigure.debug_mode = True zcml.load_config('configure.zcml', cpc.addressbook) fiveconfigure.debug_mode = False ztc.installPackage('cpc.addressbook') setup_product() ptc.setupPloneSite(products=['cpc.addressbook']) def test_suite(): return unittest.TestSuite([ # Unit tests #doctestunit.DocFileSuite( # 'README.txt', package='cpc.addressbook', # setUp=testing.setUp, tearDown=testing.tearDown), #doctestunit.DocTestSuite( # module='cpc.addressbook.mymodule', # setUp=testing.setUp, tearDown=testing.tearDown), # Integration tests that use PloneTestCase #ztc.ZopeDocFileSuite( # 'README.txt', package='cpc.addressbook', # test_class=TestCase), ztc.FunctionalDocFileSuite( 'tests/doctests/basicfunctionality.txt', package='cpc.addressbook', test_class=TestCase, optionflags=doctest.REPORT_ONLY_FIRST_FAILURE | doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS), ]) if __name__ == '__main__': unittest.main(defaultTest='test_suite')You don't have to create a separate test suite for each doctest. You can group them together by adding more items to the list where the FunctionalDocFileSuite is defined
Add the bold lines to
/opt/buildouts/addressbookdev/src/cpc.addressbook/cpc/addressbook/tests/test_basicfunctionality.py. This will set up the functional doctest defined below.... def test_suite(): return unittest.TestSuite([ ... ztc.FunctionalDocFileSuite( 'tests/doctests/basicfunctionality.txt', package='cpc.addressbook', test_class=TestCase, optionflags=doctest.REPORT_ONLY_FIRST_FAILURE | doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS), ztc.FunctionalDocFileSuite( 'tests/doctests/contact.txt', package='cpc.addressbook', test_class=TestCase, optionflags=doctest.REPORT_ONLY_FIRST_FAILURE | doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS), ]) ...There is an advantage to running your test suite in separate files. It allows you run just one test:
$ bin/instance test -mcpc.addressbook.tests.test_the_module_I_want_to_runWrite your functional doctest.
The test plone instance is available as a global variable in your doctest called
portalUse the
Browser()object to simulate basic interactionCopy and paste this code into
/opt/buildouts/addressbookdev/src/cpc.addressbook/cpc/addressbook/tests/doctests/basicfunctionality.txtFunctional testing of basic setup of the CPC Address Book product ================================================================= Set up the browser object ------------------------- >>> from Products.Five.testbrowser import Browser >>> browser = Browser() >>> browser.handleErrors = True >>> portal_url = portal.absolute_url() Log In ------ Open the main page >>> browser.open(portal_url) Log in using the login portlet >>> browser.getControl(name='__ac_name').value = 'test' >>> browser.getControl(name='__ac_password').value = 'test' >>> browser.getControl(name='submit').click() Verify the login was successful >>> "You are now logged in" in browser.contents True Check Add Menu For ContentList type ----------------------------------- >>> browser.getLink(url='createObject?type_name=Contact+List') <Link ...[IMG] Contact List... url='...createObject?type_name=Contact+List'>This is just enough to know if anything we did caused errors, and if we at least got our base content type in the "Add..." dropdown list, which is a pretty good indication that it installed properly.
Have the Zope Testrunner run the doctest
$ cd /opt/buildouts/addressbookdev
$ bin/instance test -scpc.addressbookYou'll know the tests ran successfully if you see the following output:
Ran 1 tests with 0 failures and 0 errors in 2.384 seconds.
Cleanup
ZopeSkel does a lot for you, but it will leave a few bits of cleanup that you have to do yourself.
Clean up the generic setup profile a little bit
Set the
filter_content_typesproperty for your folderish classes, and set theallowed_content_typesproperty to specify what content types can be put into your folderish typeThe file you need to edit is
/opt/buildouts/addressbookdev/src/cpc.addressbook/cpc/addressbook/profiles/default/types/XXXXXX.xml(where XXXXXXX is the name of the class you defined earlier, in this case, editContact_List.xml)... <property name="filter_content_types">True</property> <property name="allowed_content_types"> <element value="Contact" /> <element value="Affiliation" /> </property> ...This is only necessary if you want to restrict what content types are allowed inside of your content type, of course
Take out the
allowed_interfaceattribute, and change theallowed_interfaceattribute inbrowser/configure.zcmlto restrict your special views to specific content types.ZopeSkel also mis-spells the name of the view class it generated. Fix that as well. The class name is whatever you added as the view name (in this case, Contact Listing), with spaces removed and converted to lower case, then "View" is appended... (in this case, contactlistingView)
... <browser:page for="cpc.addressbook.interfaces.IContactList" name="contactlisting_view" class=".contactlistingview.contactlistingView" template="contactlistingview.pt"allowed_interface=".searchview.IContactListingView"permission="zope.Public" /> <browser:page for="cpc.addressbook.interfaces.IContactList" name="affiliationlisting_view" class=".affiliationlistingview.affiliationlistingView" template="affiliationlistingview.pt"allowed_interface=".affiliationlistingview.IaffiliationlistingView"permission="zope.Public" /> <browser:page for="cpc.addressbook.interfaces.IContactList" name="search_view" class=".searchview.searchView" template="searchview.pt"allowed_interface=".searchview.IsearchView"permission="zope.Public" /> ...ZopeSkel currently neglects to put a
version.txtfile in the root of the product egg. It's a really good idea to add one$ cd /opt/buildouts/addressbookdev/src/cpc.addressbook/cpc/addressbook/
$ echo "0.1" > version.txtThis can also be accomplished by adding a
metadata.xmlfile toprofiles/default<metadata> <description>This is the profile description</description> <version>1.0</version> </metadata>
It's probably a good idea to keep this version number synched with what's in the
config.pyfile in the root of your eggThe current version of ZopeSkel is missing some permission setup additions
For each content type you have, add:
<permission id="cpc.addressbook.AddContactList" title="cpc.addressbook: Add Contact List" />To
/opt/buildouts/addressbookdev/src/cpc.addressbook/cpc/addressbook/configure.zcml.Change the content type-specific stuff to match the content types you have.
- Then -
For each content type, add
setDefaultRoles('cpc.addressbook: Add Contact', ['Manager', 'Contributor'])Near the top (after the imports) of
/opt/buildouts/addressbookdev/src/cpc.addressbook/cpc/addressbook/__init__.pyChange the content type-specific stuff to match the content types you have.
Import into Subversion
At this point, it's a good idea to get your code into a version control system. We'll use subversion here.
To use CPC's subversion server, you must have a UNC onyen, and be specifically authorized to have access.
We've settled on a simple structure modeled after Martin Aspeli's sample code (each chapter is in subdirectories, one buildout each). The idea is to have special buildouts for deployment/configuration/special cases (e.g. production, integration testing, development), and then a full buildout for each plone product. This way you can easily pick up work on a product, see all of it's dependancies (just look in the buildout.cfg), etc.
Our repository is laid out like this:
https://svn.cpc.unc.edu/svn svn root
/svn/Plone all of our Plone projects
/svn/opt/buildouts instance buildouts (production, test, dev)
/svn/cpc.xxxxxyyyyy project buildouts:
where xxxx = project name
and yyyy = product name
/svn/cpc.yyyyyyy general use product buildouts
where yyyy = product name
/svn/Products old-style projects
/svn/Extensions instance-wide extensions (not in use atm)
If you'd like to browse the subversion repositories, you can point a web browser at https://svn.cpc.unc.edu/trac. You can also submit remedy tickets, view differences, and maintain documentation using Trac.
Each product or buildout directory has three subdirectories: tags, branches, and trunk.
Current (bleeding edge) development happens in
trunk.When you want to start new development, you copy
Example:trunkwith a descriptive name tobranches.
$ svn cp https://svn.cpc.unc.edu/svn/test/trunk http://svn.cpc.unc.edu/svn/test/branches/jj_newstuff
When you make a release (for user acceptance testing, integration testing, or deployment to production), you copy your
Examples:branchortrunkinto thetagsdirectory, with a descriptive name.
Making a release out of a branch
$ svn cp https://svn.cpc.unc.edu/svn/test/branches/jj_newstuff http://svn.cpc.unc.edu/svn/test/tags/UAT_10-05-200
Making a release out of trunk:
$ svn cp https://svn.cpc.unc.edu/svn/test/trunk http://svn.cpc.unc.edu/svn/test/tags/version1.0
To put your new project into subversion, follow these steps:
Remove any
.pycfilesThese were created when you ran
bin/instance test -s...$ cd /opt/buildouts/addressbookdev
$ find . -name "*.pyc" -exec rm '{}' ';'
Create a fresh buildout skeleton
This is only necessary if you've run
python bootstrap.pyandbin/buildoutbefore. This avoids having to manually delete/selectively copy any of the files that buildout has downloaded for you.$ cd /opt/buildouts
$ paster create -t plone3_buildout addressbookdev_import
It's a good idea to use a name that will remind you that it's a temporary folder
Copy your
buildout.cfgand the contents of yoursrcdirectory$ cd /opt/buildouts/addressbookdev_import
$ cp ../addressbookdev/buildout.cfg ./
$ cp -R ../addressbookdev/src/* ./src
Create the main directory, and the standard
branches,tags, andtrunkdirectories in the subversion repository.$ svn mkdir --username=xxx -m"initial directory setup" https://svn.cpc.unc/svn/cpc.addressbook
$ svn mkdir --username=xxx -m"initial directory setup" https://svn.cpc.unc/svn/cpc.addressbook/trunk
$ svn mkdir --username=xxx -m"initial directory setup" https://svn.cpc.unc/svn/cpc.addressbook/branches
$ svn mkdir --username=xxx -m"initial directory setup" https://svn.cpc.unc/svn/cpc.addressbook/tags
where
xxxis your username. The--username=xxxparameter isn't necessary if you are logged in to your develoment environment with the name name you use for subversion.The
-mparameter lets you specify a log entry on the command line, instead of bringing up a text editor.Import into the subversion server
$ cd /opt/buildouts/addressbookdev_import
$ svn import --username=xxx . https://svn.cpc.unc.edu/svn/cpc.addressbook/trunk
You'll be asked to provide a note. I usually go with 'Initial import', but feel free to elaborate
Remove the temporary directory
$ cd /opt/buildouts
$ rm -rf addressbookdev_import
Don't be aprehensive about this step! You've still got the original
/opt/buildouts/addressbookdevdirectory, and your new buildout is now safe in subversionCheck out the new buildout
$ cd /opt/buildouts
$ svn co https://svn.cpc.unc.edu/svn/cpc.addressbook/trunk cpc.addressbook
If you haven't done so already, set up your development environment to ignore
*.pycfilesOn unix, open
~/.subversion/config, uncomment the global_ignores line and add*.pycto the end.... ### Section for configuring miscelleneous Subversion options. [miscellany] .... global-ignores = *.o *.lo *.la #*# .*.rej *.rej .*~ *~ .#* .DS_Store *.pyc ...On windows, you need to edit a registry key. This can be accomplished by importing the following into your registry
REGEDIT4 ; Settings effecting all users on this machine! [HKEY_LOCAL_MACHINE\Software\Tigris.org\Subversion\Config\Miscellany] "enable-auto-props"="yes" "global-ignores"="*.a *.o *.pyc *.pyd *.pyo *.so *.class *.ear *.iml *.iws *.jar *.war *.*~"To import, save this file as
svn_global_ignore.regand then double click on it in windows explorer. You will need administrative privlidges.Tested briefly on Windows Vista. I'm not a registry guru, I snipped this code from a post on an enfold mailing list (see the thread for more info). Use at your own risk!
For the sake of convenience, I've posted the registry code: svn_global_ignore.reg.
Write Code
Fill out your interfaces in
/opt/buildouts/cpc.addressbook/src/cpc.addressbook/cpc/addressbook/interfaces.pyUse
zope.schemato define your properties.Use
zope.schema.containsto show which objects can be conained within your folderish typesfrom zope import schema from zope.interface import Interface from zope.app.container.constraints import contains from zope.app.container.constraints import containers from cpc.addressbook import addressbookMessageFactory as _ # -*- extra stuff goes here -*- class IAffiliation(Interface): """An affiliation used to describe contacts""" class IContact(Interface): """Basic information about a person and how to contact them""" firstname = schema.TextLine(title=_(u"First Name"), required=False) lastname = schema.TextLine(title=_(u"Last Name"), required=False) phone = schema.TextLine(title=_("Phone #"), required=False) office = schema.TextLine(title=_("Office"), required=False) affiliation = schema.Object(title=_("Affiliation"), required=False, schema=IAffiliation) email = schema.TextLine(title=_("E-mail Address"), required=False) photo = schema.Bytes(title=_("Photo"), required=False) class IContactList(Interface): """A folder containing contact information, an address book""" contains('cpc.addressbook.interfaces.IContact', 'cpc.addressbook.interfaces.IAffiliation')Define the Archetypes schema for each content type
We're using the Contact content type as an example here. The file where the schema is defined is located at
/opt/buildouts/cpc.addressbook/src/cpc.addressbook/cpc/addressbook/content/contact.py. Do the same for the other types you've created as you see fit.Don't bother to define schema fields for
titleanddescription. These are in all Archetypes Content Types by default. You can specify them if you'd like to change their behavior.Be SURE that every field in your interface schema above is present in your archetypes schema!
See the Archetypes Field Reference and Archetypes Widget Reference for the various field/widget types, and their properties
We're using
AnnotationStoragehere. It's the reccomended storage method for Archetypes schema fields.... from Products.ATReferenceBrowserWidget.ATReferenceBrowserWidget import ReferenceBrowserWidget ... ContactSchema = schemata.ATContentTypeSchema.copy() + atapi.Schema(( # -*- Your Archetypes field definitions here ... -*- atapi.StringField('firstname', required=False, searchable=True, storage=atapi.AnnotationStorage(), widget=atapi.StringWidget(label=_(u"First Name"), description=_(u"aka Given Name")) ), atapi.StringField('lastname', required=False, searchable=True, storage=atapi.AnnotationStorage(), widget=atapi.StringWidget(label=_(u"Last Name"), description=_(u"aka Family Name")) ), atapi.StringField('phone', required=False, searchable=True, storage=atapi.AnnotationStorage(), widget=atapi.StringWidget(label=_(u"Phone Number"), description=_(u"in (###) ###-#### format")) ), atapi.StringField('email', required=False, searchable=True, storage=atapi.AnnotationStorage(), widget=atapi.StringWidget(label=_(u"E-mail Address"), description=_(u"")) ), atapi.StringField('office', required=False, searchable=True, storage=atapi.AnnotationStorage(), widget=atapi.StringWidget(label=_(u"Office"), description=_(u"Free-form field describing this person's location")) ), atapi.ImageField('photo', required=False, storage=atapi.AnnotationStorage(), sizes={'thumb': (80,80), 'normal': (200,200)}, widget=atapi.ImageWidget(label=_(u"Photograph"), description=_(u"")) ), atapi.ReferenceField('affiliation', required=False, stroage=atapi.AnnotationStorage(), multiValued=True, relationship="contact_has_affiliation", allowed_types=('Affiliation',), widget=ReferenceBrowserWidget(label=_(u"Affiliation"), description=_(u""), addable=True, restrict_browsing_to_startup_directory=True, ) ), )) ...We add a special import above to get the ReferenceBrowserWidget. It's much more useful than the default reference widget.
Use
Products.Archetypes.ATFieldProperty()to bridge python-style attributes with Archetypes schemaRemember to use
ATReferenceFieldProperty()for referencesclass Contact(base.ATCTContent): ... title = atapi.ATFieldProperty('title') description = atapi.ATFieldProperty('description') firstname = atapi.ATFieldProperty('firstname') lastname = atapi.ATFieldProperty('lastname') phone = atapi.ATFieldProperty('phone') office = atapi.ATFieldProperty('office') affiliation = atapi.ATReferenceFieldProperty('affiliation') email = atapi.ATFieldProperty('email') photo = atapi.ATFieldProperty('photo') ...Code your browser views
How Do I...?
Set up workflow and permissions?
Plone's workflow is top-notch, and simple to implement. Martin Aspeli advocates creating simple content types and assigning permissions to them via workflow states (as opposed to setting specific permissions on each method).
Here I'll illustrate a simple workflow to transition a Contact object from a published state ('active') to a hidden state ('inactive'). This is a somewhat contrived example, since plone's default workflow handles these states for you.
ZopeSkel currently doesn't have any templates for creating workflows, or even the skeletons of one. This may be a good oppturnity to take a crack at writing a pastscript template.
My first instinct is to put workflow into the cpc.addressbook product itself, but one of the major points driven home by Martin is that workflow is a policy issue. It can be instance-specific, and could very depending on how the product is used. To that end, I think the best place for policy issues is in a separate policy product.
Write the Workflow Spec
When a contact is added to an address book, it should automatically be avialble for consumption by logged-in users. It can be "de-activated" by a reviewer, the owner, or a manager. De-activation hides the contact from being seen by anyone but the owner, a reviewer, or a manager. The contact can be altered at any time by the owner, a reviwer, or a manager.
As earlier, roles here can easily map to roles in Plone. Try to use the default roles as much as possible, but don't be afraid to create new roles for your implementation if the stock roles don't meet your requirements, this is a good way to identify when you need to make new ones
Diagram it in UML

Create a "policy" product
$ cd /opt/buildouts/addressbookdev/src $ paster create -t plone cpc.policyThe answers to the questions are the same as when you created the content product above
Create the basic skeleton for your workflow in
profiles/default/workflows/definition.xml<?xml version="1.0"?> <dc-workflow workflow_id="cpc_addressbook_workflow" title="CPC Addressbook Workflow" initial_state="active" state_variable="active_state"> </dc-workflow>Whatever you set
workflow_idto has to be unique.This is where you specify the initial state, using the
initial_stateattribute.The
state_variableattribute is required.Add your new workflow to
profiles/default/workflows.xml<?xml version="1.0"?> <object name="portal_workflow" meta_type="Plone Workflow Tool"> <object name="cpc_addressbook_workflow" meta_type="Workflow"/> </object>Bind your workflow to the relevant content types in
profiles/default/workflows/cpc_addressbook_workflow/definition.xmlThis goes into the main
<object>tag... <bindings> <type type_id="Contact"> <bound-workflow workflow_id="cpc_addressbook_workflow"/> </type> </bindings> ...Tell the workflow tool which permissions are being managed in
profiles/default/workflows/cpc_addressbook_workflow/definition.xml<dc-worflow workflow-id="cpc_addressbook_workflow"... <permission>View</permission> ... </dc-workflow>Since this workflow only deals with whether a
Contactis visible or not, theViewpermisson is all we have to deal with.Set up the states and permission maps in
profiles/default/workflows/cpc_addressbook_workflow/definition.xml<dc-worflow workflow-id="cpc_addressbook_workflow"... ... <state state_id="published" title="Published"> <permission-map name="View" acquired="False"> <permission-role>Editor</permission-role> <permission-role>Owner</permission-role> <permission-role>Manager</permission-role> <permission-role>Reader</permission-role> <permission-role>Contributor</permission-role> </permission-map> </state> <state state_id="inactive" title="Inactive"> <permission-map name="View" acquired="False"> <permission-role>Editor</permission-role> <permission-role>Owner</permission-role> <permission-role>Manager</permission-role> <permission-role>Reader</permission-role> <permission-role>Contributor</permission-role> </permission-map> </state> ... </dc-workflow>You normally don't want to set the
acquiredproperty to True.Set up the exit transitions
<dc-worflow workflow-id="cpc_addressbook_workflow"... ... <state state_id="published" title="Published"> ... <exit-transition transition_id="deactivate" /> </state> ... <state state_id="published" title="Published"> ... <exit-transition transition_id="deactivate" /> </state> ... </dc-workflow>This is jumping the gun a bit, since we haven't defined our transitions yet. Be sure to keep the transition id's consistent
Add a dependant product
I want to use the ATReferenceBrowserWidget to associate affiliations with contacts. This widget is provided by a product that ships with Plone, but it's not accessible through Products.Archetypes.atapi
In order to make this work currently, you have to add a Plone 2.5-style Install.py in an Extensions directory, in /opt/buildouts/addressbookdev/src/cpc.addressbook/cpc/addressbook/. It has to install your dependant product, and then install your genericsetup profile, since Install.py overrides GenericSetup.
import transaction
from Products.CMFCore.utils import getToolByName
from cpc.addressbook.config import PRODUCT_DEPENDENCIES, EXTENSION_PROFILES
def install(self, reinstall=False):
"""Install a set of products (which themselves may either use Install.py
or GenericSetup extension profiles for their configuration) and then
install a set of extension profiles.
One of the extension profiles we install is that of this product. This
works because an Install.py installation script (such as this one) takes
precedence over extension profiles for the same product in
portal_quickinstaller.
We do this because it is not possible to install other products during
the execution of an extension profile (i.e. we cannot do this during
the importVarious step for this profile).
"""
portal_quickinstaller = getToolByName(self, 'portal_quickinstaller')
portal_setup = getToolByName(self, 'portal_setup')
for product in PRODUCT_DEPENDENCIES:
if reinstall and portal_quickinstaller.isProductInstalled(product):
portal_quickinstaller.reinstallProducts([product])
transaction.savepoint()
elif not portal_quickinstaller.isProductInstalled(product):
portal_quickinstaller.installProduct(product)
transaction.savepoint()
for extension_id in EXTENSION_PROFILES:
portal_setup.runAllImportStepsFromProfile('profile-%s' % extension_id, purge_old=False)
product_name = extension_id.split(':')[0]
portal_quickinstaller.notifyInstalled(product_name)
transaction.savepoint()
This is pure boiler plate, so in config.py, we just need to add the product we want to the PRODUCT_DEPENDANCIES, and be sure the profile for our product is listed in EXTENSION_PROFILES
"""Common configuration constants
"""
PROJECTNAME = 'cpc.addressbook'
ADD_PERMISSIONS = {
# -*- extra stuff goes here -*-
'Affiliation': 'cpc.addressbook: Add Affiliation',
'Contact': 'cpc.addressbook: Add Contact',
'ContactList': 'cpc.addressbook: Add ContactList',
}
PRODUCT_DEPENDENCIES = ('ATReferenceBrowserWidget')
EXTENSION_PROFILES = ('cpc.addressbook:default',)
This also allows you to install the GenericSetup profiles from other products using the EXTENSION_PROFILES variable.
Generate my own title
By default, Archetypes asks the user to come up with a title, and it uses that to generate the id. There are times that you want to control that yourself. In the case of the address book, I want the title for a contact to be the persons first name and last name, concatenated by a space.
You can achieve this using the following steps:
Replace the
titlefield with aComputedFieldin your schema.You can achieve this by just adding a field named
'title'You can do this for any of the core fields to change field type, widget, etc
Set the
accessorproperty of your new field toTitleTitleis the specific name used in the Dublin Core metadata, and the usual way Plone accesses the title of content typesThe
accessorproperty can be used on any field to specify an alternative way of accessing it's value (as opposed to using the auto-generated getters)Set the
expressionproperty to call a method from your content classYou don't have to define a special method for this, any python expression will work, but it's always a good idea
The expression itself has access to a
contextvariable, which refers to the current content object. Acqusition is in effect.
...
ContactSchema = schemata.ATContentTypeSchema.copy() + atapi.Schema((
# -*- Your Archetypes field definitions here ... -*-
atapi.ComputedField('title',
accessor='Title',
storage=atapi.AnnotationStorage(),
expression="context.printname()"
),
...
class Contact(base.ATCTContent):
...
schema = ContactSchema
...
def printname(self):
""" Print the name in a usual fashion"""
return "%s %s" % (self.firstname, self.lastname)
...
Build a custom search/listing view
Most likely, you will want to depart from the stock Plone folder_contents view and create a custom listing, or create a special search interface. Even if you don't implement the search interface, the code is prety much the same.
Add indexes and metadata for each field (and any special methods) to your generic setup profile in
profiles/default/catalog.xml<?xml version="1.0" ?> <object name="portal_catalog" meta_type="Plone Catalog Tool"> <index name="firstname" meta_type="FieldIndex"> <indexed_attr value="firstname" /> </index> <index name="lastname" meta_type="FieldIndex"> <indexed_attr value="lastname" /> </index> <index name="email" meta_type="FieldIndex"> <indexed_attr value="email" /> </index> <index name="phone" meta_type="FieldIndex"> <indexed_attr value="phone" /> </index> <index name="office" meta_type="FieldIndex"> <indexed_attr value="office" /> </index> <index name="affiliation" meta_type="KeywordIndex"> <indexed_attr value="affiliationnames" /> </index> <column value="firstname" /> <column value="lastname" /> <column value="email" /> <column value="phone" /> <column value="office" /> <column value="affiliationnames" /> <column value="phototag" /> </object>The
<column>tag is used for metadata items. These will be available in objects returned byportal_catalog.You have to create an index for any properties that you want to sort by
The
<index>tag corresponds to the properties of indexes that you see in the ZMI underportal_catalog.I like to use
KeywordIndexes for values that may need to be searched in an 'or' type of query. The property/method that is indexed is expected to return a list.You can create indexes that are named differently than the property it indexes. (e.g.
affiliationabove)titleanddescriptionare already present in the stock Plone catalog.Add a view skeleton using ZopeSkel (if you haven't already)
I'm using the view I created earlier, that I named "Search".. This generates a
searchview.pyand asearchview.ptfile in thebrowserdirectory, and adds the zcml needed to make it work. The browser view class is namedSearchView.Define the interfaces in
searchview.py... class ISearchView(Interface): """ Search view interface """ def run(): """Actually perform the search, and return the search result""" def sortby(column): """Set the sort-by column""" def export(): """Depending on the Query String variable "to", return the current search result in the expected format""" def _export_excel(): """Export search to excel spreadsheet""" def _export_ldif(): """Export search to LDIF text""" def _export_roomlist(): """Export search to special "Room List" HTML-based output""" def filter(column): """Set the column to filter by""" ...These come right from the class diagram above
Implement the controller code in
searchview.pyclass SearchView(BrowserView): """ Search browser view """ implements(ISearchView) ... def run(self): """Actually perform the search, and return the search result""" search_params = {'portal_type': 'Contact', 'path': '/'.join(self.context.getPhysicalPath())} return self.portal_catalog(**search_params) ...If you're used to writing python scripts or methods in your content classes, you may notice that the location of
contextis a little different. In this casecontextis not global (as it is in python scripts), andcontext != self, as in a content class. Instead,contextis a property ofself.This is just the bare minimum search, just enough so we can create a functional test to verify the wiring is working.
Write the view code in
searchview.pt<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" metal:use-macro="here/main_template/macros/master" i18n:domain="cpc.addressbook"> <body> <div metal:fill-slot="main"> <tal:search tal:define="result view/run"> <table class="listing"> <tr> <th> </th> <th>First Name</th> <th>Last Name</th> <th>E-Mail Address</th> <th>Telephone</th> <th>Office</th> <th>Affiliation/Dept</th> </tr> <tr tal:repeat="row result"> <td tal:content="structure row/phototag"></td> <td tal:content="row/firstname" /> <td tal:content="row/lastname" /> <td tal:content="row/email" /> <td tal:content="row/office" /> <td tal:content="row/office" /> <td tal:content="row/affiliationnames" /> </tr> </table> </tal:search> </div> </body> </html>This varies slightly from the traditional Zope Page Template, in that there is an additional variable in use:
view(normally you just mess withcontextor it's older aliashere). In this case,viewrefers to the view class itself (notice how we usedview/runto get the search results), wherascontextrefers to the content object that the view is being applied to.Wire up the view
There are several ways to interact with the view.
Wire up the view so it shows as an view tab (along with edit/sharing/etc) for your content type
This goes into
profiles/default/types/ContentList.xml... <action title="Contact Search" action_id="search_view" category="object" condition_expr="" url_expr="string:${folder_url}/search_view" visible="True"> <permission value="View" /> </action> ...search_viewis the name given to the view by ZopeSkel in thebrowser/configure.zcmlOverride an existing view
This is also done in
profiles/default/types/ContentList.xml... <alias from="folder_contents" to="search_view" /> ...In this case, you will want to emulate the standard checkboxes and delete/transition buttons that the stock folder listing provides.
Add the view into the list of dynamic views
Since ZopeSkel sets up our content types to inherit from
ATContentTypesclasses, we get the "dynamic views" feature for free.This is probably the best course of action
In
profiles/default/types/ContactList.xml, addsearch_viewto the list ofview_methods, and (optionally) set the default view method tosearch_view... <property name="default_view">search_view</property> <property name="view_methods"> <element value="base_view" /> <element value="search_view" /> </property> ...Then, in
browser/configure.zcml, wire the menu item up the zope 3 way... <browser:menuItem for="cpc.addressbook.interfaces.IContactList" menu="plone_displayviews" title="Search" action="search_view" description="A locally searchable listing interface with icons and full affiliation lists" /> ...I'm not sure if this is really necessary
Use image scales to autogenerate thumbnails of an ImageField
The ImageField Archetypes schema field type has an api built in for scaling the images stored in them. But since we're using AnnotationStorage(), we have to do some work to access the api.
Set up the
sizesparameter in your schema... atapi.ImageField('photo', required=False, storage=atapi.AnnotationStorage(), sizes={'thumb': (80,80), 'normal': (200,200)}, widget=atapi.ImageWidget(label=_(u"Photograph"), description=_(u"")) ), ...Override
__bobo_traverse__so you can access the api in the standard way, via special URLSdef __bobo_traverse__(self, REQUEST, name): """Transparent access to image scales """ if name.startswith('photo'): field = self.getField('photo') image = None if name == 'photo': image = field.getScale(self) else: scalename = name[len('photo_'):] if scalename in field.getAvailableSizes(self): image = field.getScale(self, scale=scalename) if image is not None and not isinstance(image, basestring): # image might be None or '' for empty images return image return super(Contact, self).__bobo_traverse__(REQUEST, name)Replace the field name (in this case 'photo') with whatever the name of your field is
This code is taken almost verbatim from
Products.ATContentTypes.ATNewsItemAdd a method that you can call from page templates or add as a metadata column
def phototag(self, **kwargs): """Generate image tag using the api of the ImageField """ if 'scale' not in kwargs.keys(): kwargs['scale'] = 'thumb' return self.getField('photo').tag(self, **kwargs)All you really need here is
return self.getField('photo').tag(self, **kwargs). I added theifstatement above it to set a default thumbnail size. This allows me to put the output of this into a metadata column. That way, I can avoid the overhead and complexity of having to callgetField()directly in my templates.
Be sure to not try to access a size that you didn't define in your schema. You will get a 404 and no other error message.
Use Plone's batching (aka pagination!) in your Browser View
Zope provides a mechanism (called Batch) to break up a big data set into chunks or pages. Plone extends this mechanism (PloneBatch) and provdes a set of meTAL macros to create navigation UI.
The big benefit to using PloneBatch is it's what Plone uses, and it's a good idea to make your interfaces, even if they're mostly custom, conform to the stock plone look and feel as much as possible.
The "usual" way of interacting with PloneBatch involves some nasy mixing of presentation and controller logic, by mixing some direct calls to the PloneBatch machinery, directly in the page template:
These examples are based on the SearchView class created above.
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
lang="en"
metal:use-macro="here/main_template/macros/master"
i18n:domain="cpc.addressbook">
<body>
<div metal:fill-slot="main"
tal:define="results view/run;
Batch python:modules['Products.CMFPlone'].Batch;
b_size python:30;
b_start python:0;
b_start request/b_start | b_start;">
<div metal:use-macro="here/batch_macros/macros/navigation" />
<table class="listing">
<tr>
<th> </th>
<th>First Name</th>
<th>Last Name</th>
<th>E-Mail Address</th>
<th>Telephone</th>
<th>Office</th>
<th>Affiliation/Dept</th>
</tr>
<tr tal:repeat="row batch">
<td tal:content="structure row/phototag"></td>
<td tal:content="row/firstname" />
<td tal:content="row/lastname" />
<td tal:content="row/email" />
<td tal:content="row/office" />
<td tal:content="row/office" />
<td tal:content="row/affiliationnames" />
</tr>
</table>
<div metal:use-macro="here/batch_macros/macros/navigation" />
</div>
</body>
</html>
This is taken from a very nice tutorial.
This will work in a browser view template, but as I said before it's messy. Since ZopeSkel has gone through all the trouble of separating our controller and view code for us, we should maintain that attitude moving forward.
Here's a better approach for our situation:
Call
PloneBatchfrom your browser view code... from Products.CMFPlone.PloneBatch import Batch ... class SearchView(BrowserView): ... def run(self): """Actually perform the search, and return the search result""" search_params = {'portal_type': 'Contact', 'path': '/'.join(self.context.getPhysicalPath())} start = int(self.context.REQUEST.get('b_start', 0)) per_page = int(self.context.REQUEST.get('b_size', 50)) catalog = self.portal_catalog result = catalog(**search_params) batch = Batch(result, per_page, start) return batch;self.portal_catalogis a special property set up for us by the ZopeSkel template. Handy!The main parameters to
BatchareA sequence type (list/tuple)
The size of each "page" (aka batch size)
The page to start on.
I pull the
startandper_pagevariables out of the browser request, since that's how they'll be sent from the page template.The variable names in the request are important.
b_startandb_sizecorrespond to the variables used by the navigation macros to generate the navigation linksSet up your view template
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" metal:use-macro="here/main_template/macros/master" i18n:domain="cpc.addressbook"> <body> <div metal:fill-slot="main"> <tal:batch tal:define="batch view/run; template_id string:@@search_view"> <div metal:use-macro="here/batch_macros/macros/navigation" /> <table class="listing"> <tr> <th> </th> <th>First Name</th> <th>Last Name</th> <th>E-Mail Address</th> <th>Telephone</th> <th>Office</th> <th>Affiliation/Dept</th> </tr> <tr tal:repeat="row batch"> <td tal:content="structure row/phototag"></td> <td tal:content="row/firstname" /> <td tal:content="row/lastname" /> <td tal:content="row/email" /> <td tal:content="row/office" /> <td tal:content="row/office" /> <td tal:content="row/affiliationnames" /> </tr> </table> <div metal:use-macro="here/batch_macros/macros/navigation" /> </tal:batch> </div> </body> </html>I had to set the
template_idvariable to the name of the view, in this case@@search_view, since the batch navigation macros expect a content object and not a view. It's not technically a template, but the macro code just appends whatever you put intemplate_idto the end of the URL of the content object, so you can fudge it a bit here and make it workI tend to have very big result sets in my products, that show 50-100 items per page, so I put the navigation on the top and bottom of the listing table to keep it in reach. I'm not sure how plonish that is.
Extend a content type
Content types can be extended just like any other python class, but a few extra steps need to be taken due to archetypes registration mechanisms
Use zopeskel to create your content type
$ cd /opt/buildouts/addressbookdev/src/cpc.addressbook
$ paster addcontent contenttype
[answer questions]For this example, we're making a new type of contact called a Contractor
Import your base content type, and it's schema (in
content/contractor.py... from cpc.addressbook.content.contact import Contact, ContactSchema ...Copy the schema
Change
ContractorSchema = schemata.ATContentTypeSchema.copy() + atapi.Schema((
To
ContractorSchema = ContactSchema.copy() + atapi.Schema((
The goal here is to copy the schema defined in
contact.pyinstead of the generic schema inATContentTypeSchemaAdd new fields to the schema
... ContractorSchema = ContactSchema.copy() + atapi.Schema(( # -*- Your Archetypes field definitions here ... -*- atapi.StringField('company', required=False, searchable=True, storage=atapi.AnnotationStorage(), widget=atapi.StringWidget(label=_(u"Company Name"), description=_(u"Company this contractor works for")) ), )) ...Derive your new type from the parent type
... class Contractor(base.ATCTContent) ...Becomes
... class Contractor(Contact) ...Bridge the old attributes
... class Contractor(Contact) ... title = atapi.ATFieldProperty('title') description = atapi.ATFieldProperty('description') firstname = atapi.ATFieldProperty('firstname') lastname = atapi.ATFieldProperty('lastname') email = atapi.ATFieldProperty('email') phone = atapi.ATFieldProperty('phone') office = atapi.ATFieldProperty('office') photo = atapi.ATFieldProperty('photo') affiliation = atapi.ATReferenceFieldProperty('affiliation') ...I'm not sure if this is necessary. Need to test without it.
Bridge the new attributes
... class Contractor(Contact) ... affiliation = atapi.ATReferenceFieldProperty('affiliation') company = atapi.ATReferenceFieldProperty('company') ...Update the Interface
The interface can inherit from it's parent too
in
interfaces.py... class IContractor(Interface): ...Becomes
... class IContractor(IContact): ...
Remove a content type
Sometimes you may want to get rid of a content type. There are a lot of places where they show up.
We're assuming you want to remove the Affiliation type
Remove the content module file
$ rm /opt/buildouts/addressbookdev/src/cpc.addressbook/cpc/addressbook/content/affiliation.py
Remove the content module file
$ rm /opt/buildouts/addressbookdev/src/cpc.addressbook/cpc/addressbook/content/affiliation.py
Switch to "folderish"
People make mistakes. Content types evolve. If you happened to accidentially answer "False" when ZopeSkel asked "Is this content type folderish?", or change your mind, here are the steps to correct the problem:
Change the code in your content type class
... from Products.ATContentTypes.content import base ... AffiliationSchema = schemata.ATContentTypeSchema.copy() + atapi.Schema(( ... class Affiliation(base.ATCTContent): ...Becomes
... from Products.ATContentTypes.content import folder ... AffiliationSchema = folder.ATFolderSchema.copy() + atapi.Schema(( ... class Affiliation(folder.ATFolder): ...-
Add
folderish=Trueto the schema finalizationschemata.finalizeATCTSchema(AffiliationSchema, folderish=True, moveDiscussion=False) -
Change
object_urltofolder_urlinprofiles/types/affiliation.xml... <action title="View" action_id="view" category="object" condition_expr="" url_expr="string:${object_url}/" visible="True"> <permission value="View" /> </action> ...Becomes...
... <action title="View" action_id="view" category="object" condition_expr="" url_expr="string:${folder_url}/" visible="True"> <permission value="View" /> </action> ... Don't forget to set
filter_content_typesand/or specifyallowed_content_typesif needed inprofiles/types/affiliation.xml
Add a "standalone" page?
There are times when you need a sort of generic "page" that provides some sort of custom user interface, like a form or a report. Zope doesn't have a concept of a "standalone" page in the sense of dropping a file into a directory. In Zope (and by extension Plone), you must create a browser page or view, set up for any context. You can then simulate it being "static" by linking to it on the portal root, or some other convenient place. Portlets are one way of handling this linkage.
Add the view using ZopeSkel
Keep Your Interface Schema and Archetypes Schema Synched
Since we have to use a mix of Zope 3 technology (interfaces) and Plone's Zope 2 technology (Archetypes), we have to define the schema for our content types twice: once in the interface definition, and again for Archetypes.
As your project grows, it can be hard to keep the two schemas in sync. We can use a doctest to help us keep ourselves honest.
Create a new doctest file
Edit
/opt/buildouts/addressbookdev/src/cpc/addressbook/tests/test_basicfunctionalty.py, or start a new module like above
Interface Test
==============
This test checks that each content type:
1. implements the correct interface
2. the schema defined in the interface matches
the archetypes schema (at least in terms of field names)
see http://docs.python.org/lib/inspect-types.html
and http://mail.python.org/pipermail/python-list/2003-May/207203.html
and http://www-128.ibm.com/developerworks/library/l-pyint.html
Setup
-----
Import the content module
>>> import cpc.addressbook.content as content
>>> import cpc.addressbook.interfaces
Import the classes needed for introspection
>>> from Products.Archetypes.atapi import BaseObject
>>> import inspect
>>> from types import ModuleType
>>> import zope.schema
Gather up the classes that need to be tested
>>> modules = [val for val in content.__dict__.values() if isinstance(val, ModuleType)]
Get the archetypes-based classes in each module in cpc.(ATFolder and ATContent both derive from BaseObject somewhere in
their class hierarchy)
>>> archetypes = []
>>> for module in modules:
... archetypes = archetypes + [val for val in module.__dict__.values() if inspect.isclass(val) and issubclass(val, BaseObject)]
Control the sort order so it's predictable
>>> archetypes.sort(key=lambda x: x.__name__)
>>> archetypes
[<class 'cpc.addressbook.content.affiliation.Affiliation'>, <class 'cpc.addressbook.content.contact.Contact'>, <class 'cpc.addressbook.content.contactlist.ContactList'>]
Check each class to make sure it's interface exists and it implements it
Then compare the archetypes schema with the zope.schema in the interface
>>> len(archetypes)
3
>>> for aclass in archetypes:
... interface = 'I%s' % aclass.__name__
... print aclass.__name__
... print interface
... i = getattr(cpc.addressbook.interfaces, interface)
... o = aclass('temp')
... i.providedBy(o)
... ifields = zope.schema.getFieldsInOrder(i)
... ischema = zope.schema.getFields(i)
... for field in ifields:
... name = field[0]
... ifield = ischema[name]
... print name
... aclass.schema[name].widget.label == ifield.title
... print
Affiliation
IAffiliation
True
title
True
description
True
<BLANKLINE>
Contact
IContact
True
firstname
True
lastname
True
phone
True
office
True
affiliation
True
email
True
photo
True
<BLANKLINE>
ContactList
IContactList
True
<BLANKLINE>
Use a custom validator
TODO
Create Your Own Vocabulary
A "vocabulary", in the Archetypes sense, is a list of possible values passed to a field. The most obvious use is in selection widgets (ReferenceWidget, SelectionWidget, InandOutWidget, etc) which can use vocabularies to generate the list of possible values. When used in tandem with the enforce_vocabular field attribute, a vocabulary can be used to restrict even a free-form field to a specific list of values. Vocabularies can be specified in several ways.
A simple list:
...
atapi.StringField('office',
required=False,
searchable=True,
storage=atapi.AnnotationStorage(),
widget=atapi.SelectionWidget(label=_(u"Office"),
description=_(u"Free-form field describing this person's location")),
vocabulary=['Office 1', 'Office 2', 'Office 3'],
),
...
This uses the values in the list as the labels and as the values that are stored
A list of two tuples, each tuple containing (label, value):
...
atapi.StringField('office',
required=False,
searchable=True,
storage=atapi.AnnotationStorage(),
widget=atapi.SelectionWidget(label=_(u"Office"),
description=_(u"Free-form field describing this person's location")),
vocabulary=[('Office 1', 'OFF0001'), ('Office 2', 'OFF0002'), ('Office 3', 'OFF0003')],
),
...
A method (or python script, or method on a parent object) that returns a list or list of tuples:
...
atapi.StringField('office',
required=False,
searchable=True,
storage=atapi.AnnotationStorage(),
widget=atapi.SelectionWidget(label=_(u"Office"),
description=_(u"Free-form field describing this person's location")),
vocabulary='offices_vocabulary',
),
...
class Contact(base.ATCTContent):
...
def offices_vocabulary(self):
""" Provide a dynamic list of offices """
# This could be querying the catalog, a RDBMS, etc
return [('Office 1', 'OFF0001'), ('Office 2', 'OFF0002'), ('Office 3', 'OFF0003')]
