ZopeMag's mascot the ZOPE fish


Article Finder
People
Issue 8 - Revision 8  /   September 26, 2004 


 
  ZopeMag Links:
Latest Issue
About the Fish
Issue 10
Issue 09
Issue 08
Issue 07
Issue 06
Issue 05
Issue 04
Issue 03
Issue 02
Issue 01
 
 
Downloads
     
  Letter from the Editor:
   Issue 8

Interviews:
Each issue we interview important people in the Zope world.

  Stéfane Fermigier

Articles:
Throughout the quarter we cover topics of interest to Zope developers, designers, and users.

  Fine Grained Permissions

  Archetypes Part III

  Intro to Silva

  Profiling Zope

  Intro to CPS

Product Review:
Too many Products, too little time? ZopeMag keeps you up-to-date which Zope Products are worthwhile checking out.

  DocFinderTab
  mxODBC DA


Guides:
This quarter we bring you a new SuperGuide. Our miniGuides and SuperGuides give you the background knowledge you need to mastering Zope.

  miniGuide to Zope Hosting
  SuperGuide - Zope for Newbies (Part II)
 
 
Downloads
     
  URLs / Download
Products we talk about in this issues Articles and Reviews

     

Illustration by Lia Avant
Archetypes Cover
Archetypes - Part III

Archetypes: (Part III)
- Customizing Views
- - - - - - - - - - - -

By Sidnei da Silva | Jul 21, 2004

print

Abstract

In the previous article you learned what Archetypes is and how to create a minimal
Archetypes-based product from scratch.

In the present article you are going to learn:

  • How to make small customizations to your product views
  • How to reuse parts of existing views and widgets in your own customized forms
  • How to link additional Javascript and CSS files to support your custom widgets
Requirements
  • You know how to write Page Templates (See read this ZopeMag article if you don't)
  • You know how to work with CMF portal_skins
  • You have Archetypes (1.3beta2+) installed and running
  • You have CMFPlone (2.0.3+) installed and running
  • You have the product created in the second Archetypes article installed and running
    (optionally you can download it here) – OR
  • You have another Archetypes-based product installed that you can play with while
    reading the article


Customizing Archetypes Views

It's very common for people building Archetypes products to need to add extra stuff to the
autogenerated views. For example, think of an e-commerce site where you use an existing
framework for e-commerce like the old CMFCommerce, or the shiny-new PloneMall.
Imagine that you want to build your own Archetypes-based product to present the products
to be sold. You would probably want to add a form below the content view so the user can
choose custom properties before he adds what he wants to buy to the shopping cart. You might
also want to replace the default listing of fields by a custom hand-built layout which best presents
your product(s).

The good news: Archetypes makes it very easy to do this kind of customization. In fact, it's so
flexible that once you get used to it, you will never want to go back to the old way of creating
products!

"Talk is cheap. Show me the code" -- Linux Torvalds

Ok, so now that we've made you curious enough about it, it's time to show you the code, and the available points of customization in Archetypes' autogenerated views.

<metal:main_macro define-macro="main"
          tal:define="portal_type python:here.getPortalTypeName().lower().replace(' ', '_');
                  base_macros here/base/macros;
                  view_template python:'%s_view' % portal_type;
                  view_macros python:path('here/%s/macros|nothing' % view_template);
                  header_macro view_macros/header | header_macro | base_macros/header;
                  body_macro view_macros/body | body_macro | base_macros/body;
                  folderlisting_macro view_macros/folderlisting | folderlisting | base_macros/folderlisting;
                  footer_macro view_macros/footer | footer_macro | base_macros/footer;
                  errors python:request.get('errors', {})">
<metal:use_header use-macro="header_macro" />
<metal:use_body use-macro="body_macro" />
<metal:use_body use-macro="folderlisting_macro" />
<metal:use_footer use-macro="footer_macro" />
</metal:main_macro>
                    

Let's get down and dirty, and see what's going on here. The very first thing that happens is that the portal type of the content being rendered is called and normalized, first by lowercasing the string and then by replacing empty spaces with underscores. Then '_view' is appended to the resulting string.

This is what happens if you have a 'News Item' being rendered:

In [1]: s = 'News Item'


In [2]: '%s_view' % s.lower().replace(' ', '_') Out[2]: 'news_item_view'

Then, using this name, a template is looked up using the 'path()' built-in of Page Templates. If the template cannot be found, or an object with that name is found that doesn't have a 'macros' subitem, or the object is a template but doesn't have the needed macro, the following happens:

  1. First, an attempt is made to fall back to a global defined variable named '<macro_name>_macro', where <macro_name> can be one of ('header', 'body', 'footer')
  2. If this variable is not defined, a fallback occurs to the default defined macros on the 'base' template.

Once you've understood this, you should be able to make the more common customizations in such a way that you don't need to customize the whole 'base_view' or 'base_edit' template, or even create a complete template from scratch. All you have to do is to create a basic template with a special name containing just the macros you are interested in overriding. The benefit you gain from doing this is that your templates remain clean and simple, and the likelihood that your templates will break when you upgrade Archetypes or Plone is very small.

Reusing existing macros

Now, suppose you do want to write a complete template from scratch. Actually, you probably wouldn't want to write the whole thing, unless you really know what you are doing. It would also mean you're reinventing the wheel (and though we do that a lot in the Zope world, it is rarely justified).

Suppose you want to reuse the part that contains the autogenerated form widgets. It's probably a good idea to do so, because the respective templates are somewhat complex, and require a deep understanding of what is going on. This part of the templates is located at the 'edit_macros' template. Reuse does depend on a couple of variables being defined, which is done on the 'base_edit' template, just before the macros are called from 'edit_macros'.

Here's an excerpt from 'base_edit' defining the variables:

...
define="errors options/state/getErrors | nothing;
    Iterator python:modules['Products.Archetypes'].IndexIterator
    schematas here/Schemata;
    fieldsets python:[key for key in schematas.keys() if key != 'metadata'];
    default_fieldset python:(not schematas or schematas.has_key('default')) and 'default' or fieldsets[0];
    fieldset request/fieldset|options/fieldset|default_fieldset;
    fields python:schematas[fieldset].fields();
...
                        

One thing you will notice here is that it defines what fieldsets are shown, and how they are ordered. By default, the same order is used that is returned by the 'here/Schemata' call. Schemata is based on an OrderedDict, so it keeps the same ordering, which is also the order by which keys are created. You could as well use your own custom sorting based, say, on manual ordering or alphanumeric. You make the choice.

It also defines which fields are going to be shown, which by default are all the fields from the current 'fieldset' (just another name for 'schemata'). Here, too, you could choose to make a different set of fields available, and even order the fields in a different way.

Back to our topic: to reuse the autogenerated edit form on your own template, you need to do two things:

  1. Define a couple of variables that the template expects to find.
  2. (Optionally) Change the way some of those variables are defined so that it matches the way you want the form to look.
  3. Call the macro that generates the form by using:

<metal:block use-macro=”here/edit_macros/macros/body” />
                        

Let me give you a more concrete example. Suppose you installed the 'Calendaring' product from the Collective project. Calendaring installs a 'Calendar' and an 'Event', which are both Archetypes-based content objects. Here's a screenshot of the default autogenerated view.

Screenshot 1

Now, let's say you want to override the view for the Calendar to display a custom piece of information. All you have to do is to create a Page Template named 'calendar_view' in the 'custom' folder of 'portal_skins' with the following content:

<metal:block define-macro="body">
   <h1>I am an overridden body macro!</h1>
</metal:block>
                        

And now, a screenshot of the modified view:

Screenshot 2

As you should have noticed, the change only took three lines. No Python, no funky installation scripts. It's impossible to be simpler!

Reusing existing widgets

For widgets the 'incantation' needed is very similar. Suppose you want to customize one of your widgets to have a different look when being rendered in 'view' mode than other widgets of the same type. The first thing that comes to mind would be to create a whole new widget, or maybe to customize the widget template. Both of these are valid approaches, but they can be very complex if you've never done them before. Instead, we are going to show you how to customize a single 'mode' of a single widget with the least amount of code, while the same look as other widgets is retained for the remaining modes.

All widget classes in Archetypes support overriding the widget template by passing the template name as a keyword argument, which is looked up in the skins. What we need to do to customize a widget view is:

  1. Create a template with 3 macros (one for each 'rendering mode'): 'view', 'edit' and 'search'.
  2. Name this template in the widget constructor, so that the widget finds our template.

Let's look at how to create a simple template for a widget, based on the string widget template, and then at how to make the same widget use our brand-new template.

For this example we will use the 'Event' content type of the Calendaring product. Here you can see a screenshot of the default view.

Screenshot 3

For our template, we will create a macro named 'view', and then reuse the macros 'edit' and 'search' from the default string widget. I won't go into all the details as the code should be quite straightforward to read. Our custom 'view' mode simply renders a string backwards.

<metal:block define-macro="view"
          tal:define="value python:list(accessor());
                           dummy python:value.reverse();
                           value python:''.join(value)">
    <tal:block replace="structure value" />
</metal:block>
<metal:block define-macro="edit">
    <metal:block use-macro="here/widgets/string/macros/edit"/>
</metal:block>
<metal:block define-macro="search">
    <metal:block use-macro="here/widgets/string/macros/search"/>
</metal:block>
                        

Now, to hook this into an existing Archetype:

  1. Create a Page Template named 'reverse_string_widget' in your 'custom' folder, inside 'portal_skins'.
  2. Find a StringWidget in your existing Archetype and add “macro='reverse_string_widget”' to the widget constructor.

Done! Restart your Zope instance and visit an instance of your Archetype to make sure the content of your field is being rendered backwards in 'view' mode.

Screenshot 4

As you can see here, I've set the 'Location', 'Contact Name' and 'Contact Email' fields to use the 'reversed_string_widget' template.

One example of this system metadata is the “searchable” parameter we described above. When Archetypes renders the value to be indexed in the “SearchableText” index (default in CMF), it goes looking for fields with the searchable flag on and tries to concatenate all their values together to form a long string to be indexed.

Another example of this system metadata is the “index” parameter of fields. This parameter is used for specifying:

Archetypes is all about Schemas. Schemas are used to describe the various facets of your content type – the structure and type of your fields, where the values will be stored and also the widgets that will be used to present them. Schemas also hold some very low-level properties of your content type, which are mainly used as “system metadata” for bootstrapping and fine-tuning some underlying features.

A different way of doing this, which might work best if you don't want to/cannot change the schema definition is to just name your template 'at_widget_string' instead of 'reversed_string_widget'. This way, Archetypes will automagically pick your template instead of the default string widget template. However, be aware that this will affect all string widgets.

Here you can see a screenshot of the resulting view if we rename 'reversed_string_widget' to 'at_widget_string'. Note that all StringWidgets are rendered backwards. Also note that 'Attendees' is not rendered backwards. This is because it is not a StringWidget, but a LinesWidget.

Spicing up your widgets: Adding Javascript and CSS

Once you get comfortable with modifying existing widgets, and as the need for more complex user interaction grows on your application, you will tend towards a richer UI, where the user can interact with dynamic elements on a page. For creating and manipulating such dynamic elements, you are probably going to use Javascript. You may also add some special styling to your widgets using CSS.

However, when you start adding things like this, the size of your page keeps growing. And even worse: If you add these resources to your site template directly, they will be loaded with every page of your site, regardless of whether they are actually used or not.

A good example of this problem was Plone itself. During the development phase of the 2.0 release a dynamic calendar popup widget was introduced. The calendar was amazing, and it was very well-skinned, too. However, the Javascript and CSS for just the calendar were more than one-third of the total page size of an out-of-the-box Plone site. This was fixed quickly by enabling the inclusion of the Javascript and CSS on just the pages where it was actually needed. The problem with this is that you have to add the resources manually on each template where it is needed.

For Archetypes, we needed to come up with a more generic solution, one which was suitable not only for the CalendarWidget but also for other widgets that could make use of Javascript and CSS in the future.

The solution was to add two properties to widgets, named 'helper_css' and 'helper_js' . These two properties take a list of names, which are then looked up in the skins and included dynamically in the <head> part of the generated page. It is obvious that even if you have multiple widgets of the same kind on a page, the Javascript and CSS only have to be included once for the page. In the case of CalendarWidget the order in which the Javascript files were included did matter. So, in order to fulfill these requirements, the complete list of files to be included is computed once, and then uniqueified.

Here's the listing for the CalendarWidget definition, so you can see for yourself how to make use of this feature:

class CalendarWidget(TypesWidget):
        _properties = TypesWidget._properties.copy()
        _properties.update({
        'macro' : "widgets/calendar",
        'format' : '', # time.strftime string
        'helper_js': ('jscalendar/calendar_stripped.js',
        'jscalendar/calendar-en.js'),
        'helper_css': ('jscalendar/calendar-system.css',),
        })
                        

Conclusion

Through this article you have learned how to make simple and effective modifications to Archetypes views and widgets without modifying the original templates. These are very powerful features which have been present in Archetypes for a while, but highly under-documented. With the experience you have gained through this article, you should now be able to build truly custom solutions that will be more robust to changes and last longer through subsequent software upgrades.

I hope you enjoyed the article. Many thanks to ZopeMag for making this possible.


Sidnei da Silva:

Sidnei da Silva started to work with python just after leaving the ISP he was working for, as PHP programmer and doing Sysadmin tasks. Actually, he fell into python by accident . On the company he started, X3ng , with a few friends from University, they decided to use Zope, so he's got Zope installed and started playing with ZClasses, to drop it right away a few hours later for writing python products. He used to do python programming for food, until a good friend from Texas opened his eyes: now he does python for food and a beer .


shim
shim  ZopeMag is committed to bringing you the best in Zope Documentation. shim
shim


Home   Subscribe   FAQ   Contact   Write for us   Privacy Policy   Weekly News   PyZine   opensourcexperts.com  

Reproduction of material from any of ZopeMag's pages without prior written permission is strictly prohibited. Copyright 2003 - 2005 ZopeMag Zope/Plone hosting by Nidelven IT