source: products/quintagroup.themetemplate/trunk/quintagroup/themetemplate/README.txt @ 3050

Last change on this file since 3050 was 3050, checked in by mylan, 13 years ago

#238: Added comment about reaason of usage '--overwrite' option

File size: 21.0 KB
RevLine 
[1005]1qplone3 theme template
2======================
3
[1012]4quintagroup.themetemplate is an enhanced "Plone 3 Theme" template from Zopeskel,
5that includes addcontent local command, which allows you to extend base Plone theme
6by additional elements, such as: skin layers, portlets, viewlets, css and js resources,
7and objects in zexp files. This package is an analogue of Archetype template in terms
8of its functionality.
[1011]9
[1013]10quintagroup.themetemplate package is used for development of all Quintagroup themes
[2573]11for Plone 3 at http://skins.quintagroup.com.
[1013]12
[1005]13Contents
14--------
151. Overview
162. Creating theme package
173. Extending theme
184. Release notes
19
20Overview
21========
22
[1011]23This theme template allows you to create initial theme package skeleton,
24i.e. create plone3 theme python package with nested namespace (this is different from
25deafult plone3_theme template in Zopeskel)
[1005]26
[1011]27After that you can extend theme package by the following elements:
28
[2573]29- skin-layer(s)
30- portlet(s)
31- viewlet(s)
32- css, js resource(s)
33- objects in zexp files
[1005]34
35Creation of a package is performed with *paster create* PasteScript command.
36Theme extending with other resources can be done with *paster addcontent*
37local ZopeSkel command (extended in this product).
38
39Creating theme package
40======================
41
[3050]42In the demo below we'll use "--overwrite" option as multiple templates are
43running one after another, and some same-named files should be overwritten
44without extra questions.
45
[1005]46Let's create plone-3 theme python package.
47Use `paster create` command for that::
48
[3048]49    >>> paster('create -t qplone3_theme quintagroup.theme.example --overwrite')
50    paster create -t qplone3_theme quintagroup.theme.example ...
[1005]51    ...
52
[2573]53You got standard python package content with
[1005]54
[2573]55- *quintagroup* upper level namespace.
56- *quintagroup.theme.example-configure.zcml* - zcml file for adding into package-includes directory
57
[1005]58Check that::
59
[2570]60    >>> package_dir = 'quintagroup.theme.example'
61    >>> objects = ['setup.py', 'quintagroup', 'quintagroup.theme.example-configure.zcml']
62    >>> objects.sort()
63    >>> [o for o in objects if o in os.listdir(package_dir)]
64    ['quintagroup', 'quintagroup.theme.example-configure.zcml', 'setup.py']
[1005]65
66
67*qplone3_theme* template - creates theme with nested namespace.
68
[2573]69By default - theme is placed in *quintagroup.theme.<3rd part of dotted package name> namespace*
[1005]70
[2570]71in our case - quintagroup.theme.example
[1005]72
73So check namespaces::
[1017]74
[2570]75    >>> theme_namespace = os.path.join(package_dir,'quintagroup','theme','example')
[1005]76    >>> os.path.isdir(theme_namespace)
77    True
78
79Theme holds 3 subdirectories (browser, profiles, skins)::
[1017]80
[1005]81    >>> cd(theme_namespace)
82    >>> dirs = ('skins', 'browser', 'profiles')
83    >>> [True for d in dirs if d in os.listdir('.')]
84    [True, True, True]
85
86And initialization files (__init__.py, configure.zcml) ::
[1017]87
[1005]88    >>> files = ('__init__.py', 'configure.zcml')
89    >>> [True for d in files if d in os.listdir('.')]
90    [True, True]
91   
92
93*browser* directory
94-------------------
95
96Browser directory contains:
97
[2573]98- 'templates' resource directory
99- interfaces.py module with IThemeSpecific marker interface
100- configure.zcml, with registered theme marker interface::
101
[1005]102    >>> ls('browser')
103    __init__.py
104    configure.zcml
105    interfaces.py
106    templates
107
108    >>> cat('browser/interfaces.py')
109    from plone.theme.interfaces import IDefaultPloneLayer
110    <BLANKLINE>
111    class IThemeSpecific(IDefaultPloneLayer):
112    ...
113
114    >>> cat('browser/configure.zcml')
115    <configure
116    ...
117        <interface
118            interface=".interfaces.IThemeSpecific"
119            type="zope.publisher.interfaces.browser.IBrowserSkinType"
120            name="Custom Theme"
121            />
122    ...
123
124As we see, default theme name is 'Custom Theme', but on theme
125creation you can point out your own name. Check this ...
126
[2573]127First create configuration file with different skin name::
128
[1005]129    >>> conf_data = """
130    ... [pastescript]
131    ... skinname=My Theme Name
132    ... """
133    >>> file('theme_config.conf','w').write(conf_data)
134
[2573]135Create the same theme with your own skin name and check this::
136
[3048]137    >>> paster('create -t qplone3_theme quintagroup.theme.example --overwrite --config=theme_config.conf')
[1005]138    paster create ...
139    >>> cd(package_dir)
[2570]140    >>> cat('quintagroup/theme/example/browser/configure.zcml')
[1005]141    <configure
142    ...
143        <interface
144            interface=".interfaces.IThemeSpecific"
145            type="zope.publisher.interfaces.browser.IBrowserSkinType"
146            name="My Theme Name"
147            />
148    ...
149
150
151*skins* directory
[2573]152-----------------
[1005]153
154It contains only README.txt file and NO SKIN LAYERS YET.
155This is a job for localcommand ;)
156
[2573]157But check whether I am right ...::
158
[2570]159    >>> cd('quintagroup/theme/example')
[1005]160    >>> ls('skins')
161    README.txt
162
163
[2573]164*profiles* directory
165--------------------
166
167There is 'default' and uninstall profiles inside::
168
[1005]169    >>> 'default' in os.listdir('profiles')
170    True
171    >>> 'uninstall' in os.listdir('profiles')
172    True
173
174There are the following items in default profile:
175
[2573]176- import_steps.xml - for any reason.
177- skins.xml - for registering skins directory::
178
[1005]179    >>> cd('profiles/default')
180    >>> 'import_steps.xml' in os.listdir('.')
181    True
182    >>> 'skins.xml' in os.listdir('.')
183    True
184
185*skins.xml* profile makes your theme default on installation
186and uses layers list from 'Plone Default' for our theme,
[2573]187without any new layers (yet)::
[1005]188
189    >>> cat('skins.xml')
190    <?xml version="1.0"?>
[2570]191    ...
[1005]192    <object name="portal_skins" ...
[3048]193            default_skin="My Theme Name" request_varname="plone_skin">
[1005]194    ...
195    <skin-path name="My Theme Name" based-on="Plone Default">
196      <!-- -*- extra layer stuff goes here -*- -->
197    <BLANKLINE>
198    </skin-path>
199    ...
200
201*import_steps.xml* - call _setupVarious_ function from
[2573]202_setuphandlers.py_ module for additional installation steps::
[1005]203
204    >>> cat('import_steps.xml')
205    <?xml version="1.0"?>
206    ...
[2570]207    <import-step id="quintagroup.theme.example.various"
[1005]208    ...
[2570]209                 handler="quintagroup.theme.example.setuphandlers.setupVarious"
[1005]210    ...
211    </import-step>
212    ...
213
[2573]214Look at setuphandlers.py module::
215
[1005]216    >>> cd('../..')
217    >>> cat('setuphandlers.py')
218        def setupVarious(context):
219    ...
220
221
222Extending theme
223===============
224
225One of the best features, which ZopeSkel package brings, is *localcommand*.
226
[1010]227This part shows how you can extend a theme (generated with qplone3_theme
[1005]228ZopeSkel template) with additional useful stuff:
229
[2573]230- skin layers
231- views
232- viewlets
233- portlets
234- css
235- javascripts
236- objects in zexp files
[1005]237
238So, in qplone3_theme generated package you can use *addcontent* ZopeSkel
239local command.
240
241IMPORTANT TO NOTE: localcommand (addcontent in our case) should be
242called in any subdirectory of the generated theme package. And it won't
[2573]243work outside this package::
[1005]244
245    >>> paster('addcontent -a')
246    paster addcontent -a
247      ...
[1360]248        css_dtml_skin:   A DTML file in skin layer with CSS registration
[1005]249        css_resource:    A Plone 3 CSS resource template
250      ...
251        import_zexps:    A template for importing zexp-objects into portal on installation
252        js_resource:     A Plone 3 JS resource template
253      N portlet:         A Plone 3 portlet
254      ...
255        skin_layer:      A Plone 3 Skin Layer
256      ...
257      N view:            A browser view skeleton
258        viewlet_hidden:  A Plone 3 Hidden Viewlet template
259        viewlet_order:   A Plone 3 Order Viewlet template
260      ...
261
262
263We can see a list of extention subtemplates, which can be used for our theme.
264'N' character tells us that these subtemplates are registered for other (archetype)
265template, but it does not matter - they can correctly extend our theme.
266
267
268Adding SKIN LAYER
269=================
270
[2573]271For that case use *skin_layer* subtemplate with *addcontent* local command::
[1005]272
[3048]273    >>> paster('addcontent skin_layer')
274    paster addcontent skin_layer ...
[1005]275    Recursing into profiles
276    ...
277
278This command adds NEW 'skin_layer' (default name) directory to _skins_ directory,
[2573]279with only CONTENT.txt file inside::
[1005]280
281    >>> 'skin_layer' in os.listdir('skins')
282    True
283    >>> ls('skins/skin_layer')
284    CONTENT.txt
285
[2573]286*skins.xml* profile is also updated::
[1005]287
288    >>> cat('profiles/default/skins.xml')
289    <?xml version="1.0"?>
[2570]290    ...
[1005]291    <object name="portal_skins" allow_any="False" cookie_persistence="False"
[3048]292       default_skin="My Theme Name" request_varname="plone_skin">
[1005]293    ...
294     <object name="skin_layer"
295        meta_type="Filesystem Directory View"
[2570]296        directory="quintagroup.theme.example:skins/skin_layer"/>
297    ...
[1005]298     <skin-path name="My Theme Name" based-on="Plone Default">
299    ...
300      <layer name="skin_layer"
301         insert-after="custom"/>
302    <BLANKLINE>
303     </skin-path>
304    ...
305
306We can see, that:
307
[2573]308- skin_layer directory was registered as Filesystem Directory View
309- skin_layer Filesystem Directory View was added to our theme layers list
[1005]310
[2573]311
[1005]312Adding PORTLET
[2573]313==============
[1005]314
[2573]315Only initialization files are available in portlets directory before adding new portlet::
[1005]316
317    >>> ls('portlets')
318    __init__.py
319    configure.zcml
320
[2573]321Add portlet with *portlet* subtemplate::
[1005]322
[3048]323    >>> paster('addcontent portlet')
324    paster addcontent portlet ...
[2570]325    ...
[1005]326    Recursing into portlets
327    ...
328
329After executing this local command ...
330
[2573]331configure.zcml file in the theme root directory - includes portlets registry::
[1005]332
333    >>> cat('configure.zcml')
334    <configure
335    ...
336    <include package=".portlets" />
337    ...
338
[2573]339exampleportlet.pt template and exampleportlet.py script added to portlets directory::
340 
341  >>> files = ('exampleportlet.pt', 'exampleportlet.py')
[1005]342    >>> [True for d in files if d in os.listdir('portlets')]
343    [True, True]
344
[2573]345And portlets/configure.zcml - register new portlet::
346
[1005]347    >>> cat('portlets/configure.zcml')
348    <configure
349    ...
350         <plone:portlet
[2570]351             name="quintagroup.theme.example.portlets.ExamplePortlet"
[1005]352             interface=".exampleportlet.IExamplePortlet"
353             assignment=".exampleportlet.Assignment"
354             view_permission="zope2.View"
355             edit_permission="cmf.ManagePortal"
356             renderer=".exampleportlet.Renderer"
357             addview=".exampleportlet.AddForm"
358             editview=".exampleportlet.EditForm"
359             />
360    ...
361
[2573]362Finally, new portlet type is registered in portlets.xml profile::
[1005]363
364    >>> cat('profiles/default/portlets.xml')
365    <?xml version="1.0"?>
366    ...
367       <portlet
[2570]368         addview="quintagroup.theme.example.portlets.ExamplePortlet"
[1005]369         title="Example portlet"
370         description=""
[2570]371         i18n:attributes="title; description"
372         />
[1005]373    ...
374
375Thanks to ZopeSkel developers for this subtempalte ;)
376
377
378Adding CSS resource
379===================
380
[2573]381Use *css_resource* subtemplate::
[1005]382
[3048]383    >>> paster("addcontent css_resource")
384    paster addcontent css_resource ...
[1005]385    Recursing into browser
386    ...
387    Recursing into profiles
388    ...
389
390This template adds (if does not exist yet) _stylesheets_ directory in _browser_
[2573]391directory::
[1005]392
393    >>> 'stylesheets' in os.listdir('browser')
394    True
395
396In _stylesheets_ resource directory empty main.css stylesheet
[2573]397resource added::
[1005]398
399    >>> 'main.css' in os.listdir('browser/stylesheets')
400    True
401    >>> cat('browser/stylesheets/main.css')
402    <BLANKLINE>
403
404
[2573]405New resource directory was registered in configure.zcml::
[1005]406
407    >>> cat('browser/configure.zcml')
408    <configure
409    ...
410        <browser:resourceDirectory
[2570]411            name="quintagroup.theme.example.stylesheets"
[1005]412            directory="stylesheets"
413            layer=".interfaces.IThemeSpecific"
414            />
415    ...
416   
417
418And cssregistry.xml profile was added into profiles/default directory with
[2573]419registered main.css stylesheet::
[1005]420
421    >>> 'cssregistry.xml' in os.listdir('profiles/default')
422    True
423    >>> cat('profiles/default/cssregistry.xml')
424    <?xml version="1.0"?>
425    <object name="portal_css">
426    <BLANKLINE>
427     <stylesheet title=""
[2570]428        id="++resource++quintagroup.theme.example.stylesheets/main.css"
[1005]429        media="screen" rel="stylesheet" rendering="inline"
430        cacheable="True" compression="safe" cookable="True"
431        enabled="1" expression=""/>
432    ...
433
434
435
[1360]436Adding CSS resource as dtml-file into skins layer
437=================================================
438
439This template actually absolutely same to the previouse one, but layer_name
440variable added to point in which skin layer css dtml-file should be added to.
441And, of course, css resource added into pointing *skins/<layer_name>/<css_reseource_name>.dtml* file.
442
443This subtemplate has several benefits before registering css as resource layer:
444
[2573]445- in dtml file you can use power of dtml language
446- this resource can be overriden by customer if he needs that
447
[1360]448IMPORTANT:
449For add css resource in registered skin layer - you should use this subtemplate
450in conjunction with *skin_layer* one.
451
452
[2573]453Use *css_dtml_skin* subtemplate::
[1360]454
[3048]455    >>> paster("addcontent css_dtml_skin")
456    paster addcontent css_dtml_skin ...
[1360]457    Recursing into profiles
458    ...
459    Recursing into skins
460    ...
461
[2573]462This template adds main.css.dtml file into skins/skin_layer folder::
[1360]463
464    >>> 'main.css.dtml' in os.listdir('skins/skin_layer')
465    True
466
[2573]467The main.css.dtml file already prepared to use as dtml-document::
468
[1360]469    >>> cat('skins/skin_layer/main.css.dtml')
470    /*
471    ...
472    /* <dtml-with base_properties> (do not remove this :) */
473    ...
474    /* </dtml-with> */
475    <BLANKLINE>
476 
477
478And cssregistry.xml profile was added into profiles/default directory with
[2573]479registered main.css stylesheet::
[1360]480
481    >>> 'cssregistry.xml' in os.listdir('profiles/default')
482    True
483    >>> cat('profiles/default/cssregistry.xml')
484    <?xml version="1.0"?>
485    <object name="portal_css">
486    <BLANKLINE>
487     <stylesheet title=""
[2570]488        id="++resource++quintagroup.theme.example.stylesheets/main.css"
[1360]489        media="screen" rel="stylesheet" rendering="inline"
490        cacheable="True" compression="safe" cookable="True"
491        enabled="1" expression=""/>
492    ...
493
494
[1005]495Adding JAVASCRIPT resource
496--------------------------
497
[2573]498Use *js_resource* subtemplate::
[1005]499
[3048]500    >>> paster('addcontent js_resource')
501    paster addcontent js_resource ...
[1005]502    Recursing into browser
503    ...
504    Recursing into profiles
505    ...
506
507This template adds (if does not exist yet) _scripts_ directory in _browser_
[2573]508directory::
[1005]509
510    >>> 'scripts' in os.listdir('browser')
511    True
512
513
[2573]514Empty foo.js javascript file was added to _scripts_ directory::
[1005]515
516    >>> 'foo.js' in os.listdir('browser/scripts')
517    True
518    >>> cat('browser/scripts/foo.js')
519    <BLANKLINE>
520
521
[2573]522New resource directory was registered in configure.zcml, if has not been registered yet::
[1005]523
524    >>> cat('browser/configure.zcml')
525    <configure
526    ...
527        <browser:resourceDirectory
[2570]528            name="quintagroup.theme.example.scripts"
[1005]529            directory="scripts"
530            layer=".interfaces.IThemeSpecific"
531            />
532    ...
533   
534
535cssregistry.xml profile was added into profiles/default directory (if does not exist yet),
[2573]536and register new foo.js javascript resource::
[1005]537
538    >>> 'jsregistry.xml' in os.listdir('profiles/default')
539    True
540    >>> cat('profiles/default/jsregistry.xml')
541    <?xml version="1.0"?>
542    <object name="portal_javascripts">
543    ...
544     <javascript
[2570]545        id="++resource++quintagroup.theme.example.scripts/foo.js"
[1005]546        inline="False" cacheable="True" compression="safe"
547        cookable="True" enabled="1"
548        expression=""
549        />
550    ...
551
552
553
554Test viewlets subtemplates
555==========================
556
557There are 2 types of viewlet subtemplates:
558
[2573]559- viewlet_order
560- viewlet_hidden
561
[1005]562The first one is used for adding new viewlets and setting
563viewlets order for the ViewletManager, the second one only hides
564viewlet in pointed ViewletManager.
565
566Ordered NEW viewlet
567-------------------
568
[2573]569Use *viewlet_order* subtemplate::
[1005]570
[3048]571    >>> paster('addcontent viewlet_order')
572    paster addcontent viewlet_order ...
[1005]573    Recursing into browser
574    ...
575    Recursing into templates
576    ...
577    Recursing into profiles
578    ...
579
580This template adds (if not exist ;)) _viewlets.py_ module in browser directory.
581With added Example ViewletBase class, which is bound to templates/example_viewlet.pt
[2573]582template::
[1005]583
584    >>> 'viewlets.py' in os.listdir('browser')
585    True
586   
587    >>> cat('browser/viewlets.py')
588    from Products.CMFCore.utils import getToolByName
589    from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
590    from plone.app.layout.viewlets import common
591    ...
592    class Example(common.ViewletBase):
593        render = ViewPageTemplateFile('templates/example_viewlet.pt')
594    <BLANKLINE>
595
[2573]596Check template file in templates directory::
[1005]597
598    >>> 'example_viewlet.pt' in os.listdir('browser/templates')
599    True
600    >>> cat('browser/templates/example_viewlet.pt')
601    <BLANKLINE>
602
[2573]603New viewlet is registered in configure.zcml::
[1005]604
605    >>> cat('browser/configure.zcml')
606    <configure
607    ...
608       <browser:viewlet
[2570]609            name="quintagroup.theme.example.example"
[1005]610            manager="plone.app.layout.viewlets.interfaces.IPortalHeader"
611            class=".viewlets.Example"
612            layer=".interfaces.IThemeSpecific"
613            permission="zope2.View"
614            />
615    ...
616   
617
618viewlets.xml profile is added to profiles/default directory with new viewlet
[2573]619registration, ordered for specified viewlet manager::
[1005]620
621    >>> 'viewlets.xml' in os.listdir('profiles/default')
622    True
623    >>> cat('profiles/default/viewlets.xml')
624    <?xml version="1.0"?>
625    <object>
626    ...
627     <order manager="plone.portalheader"
628             based-on="Plone Default"
629             skinname="My Theme Name" >
630    ...
[2570]631        <viewlet name="quintagroup.theme.example.example" insert-after="*" />
[1005]632    <BLANKLINE>
633      </order>
634    <BLANKLINE>
635    </object>
636
637
638
639Hide EXISTING viewlet
640---------------------
641
[2573]642For that case you can use *viewlet_hidden* subtemplate::
[1005]643
[3048]644    >>> paster('addcontent viewlet_hidden')
645    paster addcontent viewlet_hidden ...
[1005]646    Recursing into profiles
647    ...
648
649As we see from upper log - there is stuff for adding/updating profiles only.
650   
651
652There is viewlet.xml profile in profiles/default directory
[2573]653which hides viewlet for specified viewlet manager::
[1005]654
655    >>> 'viewlets.xml' in os.listdir('profiles/default')
656    True
657    >>> cat('profiles/default/viewlets.xml')
658    <?xml version="1.0"?>
659    <object>
660    ...
661      <hidden manager="plone.portalheader" skinname="My Theme Name">
662    ...
663        <viewlet name="example" />
664    <BLANKLINE>
665      </hidden>
666    ...
667    </object>
668
669
670Adding ZEXPs importing
671======================
672
673Imagine situation, when you develop a theme, which uses some
674extra portal objects (documents with text for some potlets)
675Then customer of your theme can edit these objects according
676to his need.
677
678For this situation *import_zexps* subtemplate exists.
679
680*import_zexps* subtemplate extends your theme with
681mechanism for importing list of zexp formated files
[2573]682into portal root on theme instllation::
[1005]683
[3048]684    >>> paster('addcontent import_zexps')
685    paster addcontent import_zexps ...
[1005]686    ...
687    Recursing into import
688    ...
689    Recursing into profiles
690    ...
[1363]691    Inserting from profiles.zcml_insert ...
692    ...
[1005]693    Inserting from setuphandlers.py_insert into ...
694    ...
695
[2573]696As we see from the upper log
697
698- 'import' directory was added into root of the theme
699- profiles stuff was updated
700- profiles.zcml file is updated
701- some stuff into setuphandlers.py module was inserted
[1005]702   
7031. There was empty 'import' directory added, where you
[2573]704   will put zexp objects for install into portal root.::
[1005]705
706    >>> ls('import')
707    CONTENT.txt
708
709
[1363]7102. import_steps.xml was added in profiles/import_zexps directory,
[2573]711   which contains additional *quintagroup.theme.example.import_zexps* step::
[1005]712
[1363]713    >>> 'import_zexps' in os.listdir('profiles')
[1005]714    True
[1363]715    >>> 'import_steps.xml' in os.listdir('profiles/import_zexps')
716    True
[1005]717
[1363]718    >>> cat('profiles/import_zexps/import_steps.xml')
[1005]719    <?xml version="1.0"?>
720    ...
[2570]721      <import-step id="quintagroup.theme.example.import_zexps"
[1005]722                   version="..."
[2570]723                   handler="quintagroup.theme.example.setuphandlers.importZEXPs"
[1005]724                   title="My Theme Name: Import zexps objects">
725        Import zexp objects into portal on My Theme Name theme installation
726      </import-step>
727    <BLANKLINE>
[1363]728    ...
[1005]729
[1363]7303. profiles.zcml configuration updated with new genericsetup profile for zexps
[2573]731   importing::
[1005]732
[1363]733    >>> cat('profiles.zcml')
734    <configure
735    ...
736      <genericsetup:registerProfile
737        name="import_zexps"
738        title="My Theme Name: Import ZEXPs"
739        directory="profiles/import_zexps"
740        description='Extension profile for importing objects of the "My Theme Name" Plone theme.'
741        provides="Products.GenericSetup.interfaces.EXTENSION"
742        />
743    <BLANKLINE>
744    ...
745   
[2573]7464. Check setuphandlers.py module - there must be importZEXPs function defined::
[1363]747
[1005]748    >>> cat('setuphandlers.py')
749    def setupVarious(context):
750    ...
751    def importZEXPs(context):
752    ...
753
754Then simply prepare zexp objects and copy them to *import* directory.
755
756
757RELEASE NOTES !
758===============
759
760Before releasing theme - I suggest to clean up setup.py script:
761
[2573]762- remove *theme_vars* argument (its value is useful only for theme development)
[1005]763
[2573]764- remove *entry_points* argument (same reason). It's useless in plone for now.
[1005]765
[2573]766- And remove *paster_plugins* argument too (it has sence in conjunction with entry_points during theme developing)
[1005]767
768Steps mentioned above prevent possible problems with
769theme distribution/deployment.
[2571]770
771Notes:
772------
773
[2573]774* quintagroup.themetemplate v0.25 compatible with ZopeSkel >= 2.15
[2571]775
Note: See TracBrowser for help on using the repository browser.