Development Environment

Be sure to have a python in the 2.4 branch installed

Make sure you have subversion installed

  1. Install easy_install

    $ wget http://peak.telecommunity.com/dist/ez_setup.py
    $ sudo python2.4 ez_setup.py
  2. Install ZopeSkel

    $ sudo easy_install -U ZopeSkel

    The -U here tells easy_install to update the package if it's already installed and there's a new version

  3. 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.cfg later

    • Enter debug_mode (Should debug mode be "on" or "off"?) ['off']: on
      Enter verbose_security (Should verbose security be "on" or "off"?) ['off']: on

      I 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.

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.

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)

Class diagram

Code Skeleton

  1. Create the main product skeleton in the src directory of your buildout

    $ cd /opt/buildouts/addressbookdev/src
    $ paster create -t archetype cpc.addressbook

    The 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.py file that the archetype template puts in thr root of your egg.

    • Enter title (The title of the project) ['Plone Example']: CPC Addressbook

      Remember this is the name of the egg, not your product

    • Enter namespace_package (Namespace package (like plone)) ['plone']: cpc

      It'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']: addressbook

      It'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]: True

      If you're developing for plone, you will almost always want to say True here

    • Enter 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.py later

    • Enter 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.

  2. Create skeleton code for each content type.

    Call paster addcontent from the root of your egg

    This won't work anywhere else. This is because when you use the archetype template, 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 List

      This 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 book

      This 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]: True

      This 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)

  3. Create the skeleton for each view

    $ paster addcontent view

    Only 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.

  4. 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.addressbook
                 
  5. Bootstrap and Build Out

    $ cd /opt/buildouts/addressbookdev
    $ python2.4 bootstrap.py
    $ bin/buildout
  6. Test 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.py gives an example of how to implemnt the various kinds of tests, and handles the usual boilerplate setup/teardown stuff

    There are many different ways to implement tests. This doesn't necessarily reflect best practices.

    1. Move the tests.py file into the tests directory and rename it to template.py

      This 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.py
    2. Make a copy of template.py for this first functional test

      It'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.py
    3. Create the docttest file someplace that makes sense.

      $ mkdir doctests
      $ touch basicfunctionality.txt
    4. Edit 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.txt instead of the path relative to the location of the test suite

      I 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 install

      Paste this code into /opt/buildouts/addressbookdev/src/cpc.addressbook/cpc/addressbook/tests/test_basicfunctionality.py

      import 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_run
    5. Write your functional doctest.

      The test plone instance is available as a global variable in your doctest called portal

      Use the Browser() object to simulate basic interaction

      Copy and paste this code into /opt/buildouts/addressbookdev/src/cpc.addressbook/cpc/addressbook/tests/doctests/basicfunctionality.txt

      Functional 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.

    6. Have the Zope Testrunner run the doctest

      $ cd /opt/buildouts/addressbookdev
      $ bin/instance test -scpc.addressbook

      You'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.

  1. Clean up the generic setup profile a little bit

    Set the filter_content_types property for your folderish classes, and set the allowed_content_types property to specify what content types can be put into your folderish type

    The 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, edit Contact_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

  2. Take out the allowed_interface attribute, and change the allowed_interface attribute in browser/configure.zcml to 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"
        />
    ...
    
  3. ZopeSkel currently neglects to put a version.txt file 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.txt

    This can also be accomplished by adding a metadata.xml file to profiles/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.py file in the root of your egg

  4. The 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__.py

    Change 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.

To put your new project into subversion, follow these steps:

  1. Remove any .pyc files

    These were created when you ran bin/instance test -s...

    $ cd /opt/buildouts/addressbookdev
    $ find . -name "*.pyc" -exec rm '{}' ';'
  2. Create a fresh buildout skeleton

    This is only necessary if you've run python bootstrap.py and bin/buildout before. 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

  3. Copy your buildout.cfg and the contents of your src directory

    $ cd /opt/buildouts/addressbookdev_import
    $ cp ../addressbookdev/buildout.cfg ./
    $ cp -R ../addressbookdev/src/* ./src
  4. Create the main directory, and the standard branches, tags, and trunk directories 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 xxx is your username. The --username=xxx parameter isn't necessary if you are logged in to your develoment environment with the name name you use for subversion.

    The -m parameter lets you specify a log entry on the command line, instead of bringing up a text editor.

  5. 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

  6. 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/addressbookdev directory, and your new buildout is now safe in subversion

  7. Check out the new buildout

    $ cd /opt/buildouts
    $ svn co https://svn.cpc.unc.edu/svn/cpc.addressbook/trunk cpc.addressbook
  8. If you haven't done so already, set up your development environment to ignore *.pyc files

    On unix, open ~/.subversion/config, uncomment the global_ignores line and add *.pyc to 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.reg and 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

  1. Fill out your interfaces in /opt/buildouts/cpc.addressbook/src/cpc.addressbook/cpc/addressbook/interfaces.py

    Use zope.schema to define your properties.

    Use zope.schema.contains to show which objects can be conained within your folderish types

    from 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')
           
  2. 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 title and description. 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 AnnotationStorage here. 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 schema

    Remember to use ATReferenceFieldProperty() for references

    class 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')
    ...
           
  3. 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.

  1. 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

  2. Diagram it in UML

    State diagram

  3. Create a "policy" product

    $ cd /opt/buildouts/addressbookdev/src $ paster create -t plone cpc.policy

    The answers to the questions are the same as when you created the content product above

  4. 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_id to has to be unique.

    This is where you specify the initial state, using the initial_state attribute.

    The state_variable attribute is required.

  5. 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>
        
  6. Bind your workflow to the relevant content types in profiles/default/workflows/cpc_addressbook_workflow/definition.xml

    This goes into the main <object> tag

    ...
        <bindings>
            <type type_id="Contact">
               <bound-workflow workflow_id="cpc_addressbook_workflow"/>
            </type>
        </bindings>
    ...
        
  7. 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 Contact is visible or not, the View permisson is all we have to deal with.

  8. 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 acquired property to True.

  9. 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:

  1. Replace the title field with a ComputedField in 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

  2. Set the accessor property of your new field to Title

    Title is the specific name used in the Dublin Core metadata, and the usual way Plone accesses the title of content types

    The accessor property can be used on any field to specify an alternative way of accessing it's value (as opposed to using the auto-generated getters)

  3. Set the expression property to call a method from your content class

    You 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 context variable, 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.

  1. 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 by portal_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 under portal_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. affiliation above)

    title and description are already present in the stock Plone catalog.

  2. 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.py and a searchview.pt file in the browser directory, and adds the zcml needed to make it work. The browser view class is named SearchView.

  3. 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

  4. Implement the controller code in searchview.py

    class 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 context is a little different. In this case context is not global (as it is in python scripts), and context != self, as in a content class. Instead, context is a property of self.

    This is just the bare minimum search, just enough so we can create a functional test to verify the wiring is working.

  5. 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>&nbsp;</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 with context or it's older alias here). In this case, view refers to the view class itself (notice how we used view/run to get the search results), wheras context refers to the content object that the view is being applied to.

  6. Wire up the view

    There are several ways to interact with the view.

    1. 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_view is the name given to the view by ZopeSkel in the browser/configure.zcml

    2. Override 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.

    3. Add the view into the list of dynamic views

      Since ZopeSkel sets up our content types to inherit from ATContentTypes classes, we get the "dynamic views" feature for free.

      This is probably the best course of action

      In profiles/default/types/ContactList.xml, add search_view to the list of view_methods, and (optionally) set the default view method to search_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.

  1. Set up the sizes parameter 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""))
        ),
    ...
           
  2. Override __bobo_traverse__ so you can access the api in the standard way, via special URLS

       def __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.ATNewsItem

  3. Add 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 the if statement 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 call getField() 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>&nbsp;</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:

  1. Call PloneBatch from 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_catalog is a special property set up for us by the ZopeSkel template. Handy!

    The main parameters to Batch are

    • A sequence type (list/tuple)

    • The size of each "page" (aka batch size)

    • The page to start on.

    I pull the start and per_page variables 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_start and b_size correspond to the variables used by the navigation macros to generate the navigation links

  2. Set 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>&nbsp;</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_id variable 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 in template_id to the end of the URL of the content object, so you can fudge it a bit here and make it work

    I 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

  1. 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

  2. Import your base content type, and it's schema (in content/contractor.py

    ...
    from cpc.addressbook.content.contact import Contact, ContactSchema
    ...
            
  3. 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.py instead of the generic schema in ATContentTypeSchema

  4. Add 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"))
        ),
    ))
    ...
        
  5. Derive your new type from the parent type

    ...
    class Contractor(base.ATCTContent)
    ...
        

    Becomes

    ...
    class Contractor(Contact)
    ...
        
  6. 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.

  7. Bridge the new attributes

    ...
    class Contractor(Contact)
    ...
        affiliation = atapi.ATReferenceFieldProperty('affiliation')
        company = atapi.ATReferenceFieldProperty('company')
    ...
        
  8. 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

  1. Remove the content module file

    $ rm /opt/buildouts/addressbookdev/src/cpc.addressbook/cpc/addressbook/content/affiliation.py
  2. 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:

  1. 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):
            ...
            
  2. Add folderish=True to the schema finalization

            schemata.finalizeATCTSchema(AffiliationSchema, folderish=True, moveDiscussion=False)
            
  3. Change object_url to folder_url in profiles/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>
    ...
            
  4. Don't forget to set filter_content_types and/or specify allowed_content_types if needed in profiles/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.

  1. 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.

  1. 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')]