X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=doc%2Fcustomizing.txt;h=abc945c807334dc8e712d4c2d306f6684603909d;hb=0e6265c6115212ad957c3e2d7a0a3b4581c9274a;hp=827c3f76525051acb7742c6e198956326edeaf11;hpb=bf396e0654d7b5886e0734860f3966a30f3278cc;p=roundup.git diff --git a/doc/customizing.txt b/doc/customizing.txt index 827c3f7..abc945c 100644 --- a/doc/customizing.txt +++ b/doc/customizing.txt @@ -2,7 +2,7 @@ Customising Roundup =================== -:Version: $Revision: 1.111 $ +:Version: $Revision: 1.134 $ .. This document borrows from the ZopeBook section on ZPT. The original is at: http://www.zope.org/Documentation/Books/ZopeBook/current/ZPT.stx @@ -146,6 +146,16 @@ The configuration variables available are: "Foo Bar EMAIL_FROM_TAG" +**ERROR_MESSAGES_TO** - ``'user'``, ``'dispatcher'`` or ``'both'`` + Sends error messages to the dispatcher, user, or both. It will use the + ``DISPATCHER_EMAIL`` address if set, otherwise it'll use the + ``ADMIN_EMAIL`` address. + +**DISPATCHER_EMAIL** - ``''`` + The email address that Roundup will issue all error messages to. This is + also useful if you want to switch your 'new message' notification to + a central user. + **MESSAGES_TO_AUTHOR** - ``'new'``, ``'yes'`` or``'no'`` Send nosy messages to the author of the message? If 'new' is used, then the author will only be sent the message when the @@ -187,6 +197,17 @@ The configuration variables available are: wish to make them xhtml, then you'll need to change this var to 'xhtml' too so all auto-generated HTML is compliant. +**EMAIL_CHARSET** - ``utf-8`` (or ``iso-8859-1`` for Eudora users) + Character set to encode email headers with. We use utf-8 by default, as + it's the most flexible. Some mail readers (eg. Eudora) can't cope with + that, so you might need to specify a more limited character set (eg. + 'iso-8859-1'. + +**DEFAULT_TIMEZONE** - ``0`` + Numeric hour timezone offest to be used when displaying local times. + The default timezone is used when users do not choose their own in + their settings. + The default config.py is given below - as you can see, the MAIL_DOMAIN must be edited before any interaction with the tracker is attempted.:: @@ -267,6 +288,17 @@ tracker is attempted.:: # too so all auto-generated HTML is compliant. HTML_VERSION = 'html4' # either 'html4' or 'xhtml' + # Character set to encode email headers with. We use utf-8 by default, as + # it's the most flexible. Some mail readers (eg. Eudora) can't cope with + # that, so you might need to specify a more limited character set (eg. + # 'iso-8859-1'. + EMAIL_CHARSET = 'utf-8' + #EMAIL_CHARSET = 'iso-8859-1' # use this instead for Eudora users + + # You may specify a different default timezone, for use when users do not + # choose their own in their settings. + DEFAULT_TIMEZONE = 0 # specify as numeric hour offest + # # SECURITY DEFINITIONS # @@ -285,7 +317,30 @@ Note: if you modify the schema, you'll most likely need to edit the A tracker schema defines what data is stored in the tracker's database. Schemas are defined using Python code in the ``dbinit.py`` module of your -tracker. The "classic" schema looks like this (see below for the meaning +tracker. + +The ``dbinit.py`` module +------------------------ + +The ``dbinit.py`` module contains two functions: + +**open** + This function defines what your tracker looks like on the inside, the + **schema** of the tracker. It defines the **Classes** and **properties** + on each class. It also defines the **security** for those Classes. The + next few sections describe how schemas work and what you can do with + them. +**init** + This function is responsible for setting up the initial state of your + tracker. It's called exactly once - but the ``roundup-admin initialise`` + command. See the start of the section on `database content`_ for more + info about how this works. + + +The "classic" schema +-------------------- + +The "classic" schema looks like this (see below for the meaning of ``'setkey'``):: pri = Class(db, "priority", name=String(), order=String()) @@ -530,6 +585,48 @@ interface for detectors. __ design.html + +Detector API +------------ + +Auditors are called with the arguments:: + + audit(db, cl, itemid, newdata) + +where ``db`` is the database, ``cl`` is an instance of Class or +IssueClass within the database, and ``newdata`` is a dictionary mapping +property names to values. + +For a ``create()`` operation, the ``itemid`` argument is None and +newdata contains all of the initial property values with which the item +is about to be created. + +For a ``set()`` operation, newdata contains only the names and values of +properties that are about to be changed. + +For a ``retire()`` or ``restore()`` operation, newdata is None. + +Reactors are called with the arguments:: + + react(db, cl, itemid, olddata) + +where ``db`` is the database, ``cl`` is an instance of Class or +IssueClass within the database, and ``olddata`` is a dictionary mapping +property names to values. + +For a ``create()`` operation, the ``itemid`` argument is the id of the +newly-created item and ``olddata`` is None. + +For a ``set()`` operation, ``olddata`` contains the names and previous +values of properties that were changed. + +For a ``retire()`` or ``restore()`` operation, ``itemid`` is the id of +the retired or restored item and ``olddata`` is None. + + +Additional Detectors Ready For Use +---------------------------------- + Sample additional detectors that have been found useful will appear in the ``'detectors'`` directory of the Roundup distribution. If you want to use one, copy it to the ``'detectors'`` of your tracker instance: @@ -539,28 +636,62 @@ to use one, copy it to the ``'detectors'`` of your tracker instance: created. The address is hard-coded into the detector, so edit it before you use it (look for the text 'team@team.host') or you'll get email errors! +**creator_resolution.py** + Catch attempts to set the status to "resolved" - if the assignedto + user isn't the creator, then set the status to "confirm-done". Note that + "classic" Roundup doesn't have that status, so you'll have to add it. If + you don't want to though, it'll just use "in-progress" instead. +**email_auditor.py** + If a file added to an issue is of type message/rfc822, we tack on the + extension .eml. + The reason for this is that Microsoft Internet Explorer will not open + things with a .eml attachment, as they deem it 'unsafe'. Worse yet, + they'll just give you an incomprehensible error message. For more + information, see the detector code - it has a length explanation. + + +Auditor or Reactor? +------------------- - The detector code:: +Generally speaking, the following rules should be observed: - from roundup import roundupdb +**Auditors** + Are used for `vetoing creation of or changes to items`_. They might + also make automatic changes to item properties. +**Reactors** + Detect changes in the database and react accordingly. They should avoid + making changes to the database where possible, as this could create + detector loops. - def newissuecopy(db, cl, nodeid, oldvalues): - ''' Copy a message about new issues to a team address. - ''' - # so use all the messages in the create - change_note = cl.generateCreateNote(nodeid) - # send a copy to the nosy list - for msgid in cl.get(nodeid, 'messages'): - try: - # note: last arg must be a list - cl.send_message(nodeid, msgid, change_note, - ['team@team.host']) - except roundupdb.MessageSendError, message: - raise roundupdb.DetectorError, message +Vetoing creation of or changes to items +--------------------------------------- - def init(db): - db.issue.react('create', newissuecopy) +Auditors may raise the ``Reject`` exception to prevent the creation of +or changes to items in the database. The mail gateway, for example, will +not attach files or messages to issues when the creation of those files or +messages are prevented through the ``Reject`` exception. It'll also not create +users if that creation is ``Reject``'ed too. + +To use, simply add at the top of your auditor:: + + from roundup.exceptions import Reject + +And then when your rejection criteria have been detected, simply:: + + raise Reject + + +Generating email from Roundup +----------------------------- + +The module ``roundup.mailer`` contains most of the nuts-n-bolts required +to generate email messages from Roundup. + +In addition, the ``IssueClass`` methods ``nosymessage()`` and +``send_message()`` are used to generate nosy messages, and may generate +messages which only consist of a change note (ie. the message id parameter +is not required). Database Content @@ -600,6 +731,9 @@ A set of Permissions is built into the security module by default: - Edit (everything) - View (everything) +Every Class you define in your tracker's schema also gets an Edit and View +Permission of its own. + The default interfaces define: - Web Registration @@ -630,13 +764,6 @@ settings appear in the ``open()`` function of the tracker ``dbinit.py`` # # SECURITY SETTINGS # - # new permissions for this schema - for cl in ('user', ): - db.security.addPermission(name="Edit", klass=cl, - description="User is allowed to edit "+cl) - db.security.addPermission(name="View", klass=cl, - description="User is allowed to access "+cl) - # and give the regular users access to the web and email interface p = db.security.getPermission('Web Access') db.security.addPermissionToRole('User', p) @@ -684,7 +811,13 @@ Adding a new Permission When adding a new Permission, you will need to: -1. add it to your tracker's dbinit so it is created +1. add it to your tracker's dbinit so it is created, using + ``security.addPermission``, for example:: + + self.security.addPermission(name="View", klass='frozzle', + description="User is allowed to access frozzles") + + will set up a new "View" permission on the Class "frozzle". 2. enable it for the Roles that should have it (verify with "``roundup-admin security``") 3. add it to the relevant HTML interface templates @@ -753,7 +886,6 @@ Web Interface .. contents:: :local: - :depth: 1 The web interface is provided by the ``roundup.cgi.client`` module and is used by ``roundup.cgi``, ``roundup-server`` and ``ZRoundup`` @@ -811,18 +943,18 @@ identifier is examined. Typical URL paths look like: 1. ``/tracker/issue`` 2. ``/tracker/issue1`` -3. ``/tracker/_file/style.css`` +3. ``/tracker/@file/style.css`` 4. ``/cgi-bin/roundup.cgi/tracker/file1`` 5. ``/cgi-bin/roundup.cgi/tracker/file1/kitten.png`` where the "tracker identifier" is "tracker" in the above cases. That means -we're looking at "issue", "issue1", "_file/style.css", "file1" and +we're looking at "issue", "issue1", "@file/style.css", "file1" and "file1/kitten.png" in the cases above. The path is generally only one entry long - longer paths are handled differently. a. if there is no path, then we are in the "home" context. -b. if the path starts with "_file" (as in example 3, - "/tracker/_file/style.css"), then the additional path entry, +b. if the path starts with "@file" (as in example 3, + "/tracker/@file/style.css"), then the additional path entry, "style.css" specifies the filename of a static file we're to serve up from the tracker "html" directory. Raises a SendStaticFile exception. c. if there is something in the path (as in example 1, "issue"), it @@ -846,6 +978,13 @@ defaults to: - full item designator supplied: "item" +Serving static content +---------------------- + +See the previous section `determining web context`_ where it describes +``@file`` paths. + + Performing actions in web requests ---------------------------------- @@ -892,15 +1031,15 @@ of: - Also handle the ":queryname" variable and save off the query to the user's query list. -Each of the actions is implemented by a corresponding ``*actionAction*`` -(where "action" is the name of the action) method on the -``roundup.cgi.Client`` class, which also happens to be available in your -tracker instance as ``interfaces.Client``. So if you need to define new -actions, you may add them there (see `defining new web actions`_). +Each of the actions is implemented by a corresponding ``*XxxAction*`` (where +"Xxx" is the name of the action) class in the ``roundup.cgi.actions`` module. +These classes are registered with ``roundup.cgi.client.Client`` which also +happens to be available in your tracker instance as ``interfaces.Client``. So +if you need to define new actions, you may add them there (see `defining new +web actions`_). -Each action also has a corresponding ``*actionPermission*`` (where -"action" is the name of the action) method which determines whether the -action is permissible given the current user. The base permission checks +Each action class also has a ``*permission*`` method which determines whether +the action is permissible given the current user. The base permission checks are: **login** @@ -1494,6 +1633,9 @@ hasPermission specific to the "user" class - determine whether the user has a Permission is_edit_ok is the user allowed to Edit the current item? is_view_ok is the user allowed to View the current item? +is_retired is the item retired? +download_url generates a url-quoted link for download of FileClass + item contents (ie. file/) =============== ======================================================== Note that if you have a property of the same name as one of the above @@ -1587,15 +1729,39 @@ now only on Date properties - return the current date as a new reldate only on Date properties - render the interval between the date and now local only on Date properties - return this date as a new property - with some timezone offset -pretty only on Interval properties - render the interval in a pretty + with some timezone offset, for example:: + + python:context.creation.local(10) + + will render the date with a +10 hour offset. +pretty Date properties - render the date as "dd Mon YYYY" (eg. "19 + Mar 2004"). Takes an optional format argument, for example:: + + python:context.activity.pretty('%Y-%m-%d') + + Will format as "2004-03-19" instead. + + Interval properties - render the interval in a pretty format (eg. "yesterday") menu only on Link and Multilink properties - render a form select list for this property reverse only on Multilink properties - produce a list of the linked items in reverse order +isset returns True if the property has been set to a value =========== ================================================================ +All of the above functions perform checks for permissions required to +display or edit the data they are manipulating. The simplest case is +editing an issue title. Including the expression:: + + context/title/field + +Will present the user with an edit field, if they have edit permission. If +not, then they will be presented with a static display if they have view +permission. If they don't even have view permission, then an error message +is raised, preventing the display of the page, indicating that they don't +have permission to view the information. + The request variable ~~~~~~~~~~~~~~~~~~~~ @@ -1731,6 +1897,8 @@ as described below. Method Description =============== ======================================================== Batch return a batch object using the supplied list +url_quote quote some text as safe for a URL (ie. space, %, ...) +html_quote quote some text as safe in HTML (ie. <, >, ...) =============== ======================================================== You may add additional utility methods by writing them in your tracker @@ -2073,12 +2241,12 @@ templating through the "journal" method of the item*:: Defining new web actions ------------------------ -You may define new actions to be triggered by the ``@action`` form -variable. These are added to the tracker ``interfaces.py`` as methods on -the ``Client`` class. +You may define new actions to be triggered by the ``@action`` form variable. +These are added to the tracker ``interfaces.py`` as ``Action`` classes that get +called by the the ``Client`` class. -Adding action methods takes three steps; first you `define the new -action method`_, then you `register the action method`_ with the cgi +Adding action classes takes three steps; first you `define the new +action class`_, then you `register the action class`_ with the cgi interface so it may be triggered by the ``@action`` form variable. Finally you `use the new action`_ in your HTML form. @@ -2086,41 +2254,41 @@ See "`setting up a "wizard" (or "druid") for controlled adding of issues`_" for an example. -Define the new action method +Define the new action class ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The action methods have the following interface:: +The action classes have the following interface:: - def myActionMethod(self): - ''' Perform some action. No return value is required. - ''' + class MyAction(Action): + def handle(self): + ''' Perform some action. No return value is required. + ''' -The *self* argument is an instance of your tracker ``instance.Client`` -class - thus it's mostly implemented by ``roundup.cgi.Client``. See the +The *self.client* attribute is an instance of your tracker ``instance.Client`` +class - thus it's mostly implemented by ``roundup.cgi.client.Client``. See the docstring of that class for details of what it can do. The method will typically check the ``self.form`` variable's contents. It may then: -- add information to ``self.ok_message`` or ``self.error_message`` -- change the ``self.template`` variable to alter what the user will see +- add information to ``self.client.ok_message`` or ``self.client.error_message`` +- change the ``self.client.template`` variable to alter what the user will see next - raise Unauthorised, SendStaticFile, SendFile, NotFound or Redirect - exceptions + exceptions (import them from roundup.cgi.exceptions) -Register the action method +Register the action class ~~~~~~~~~~~~~~~~~~~~~~~~~~ -The method is now written, but isn't available to the user until you add -it to the `instance.Client`` class ``actions`` variable, like so:: +The class is now written, but isn't available to the user until you add it to +the ``instance.Client`` class ``actions`` variable, like so:: - actions = client.Class.actions + ( - ('myaction', 'myActionMethod'), + actions = client.Client.actions + ( + ('myaction', myActionClass), ) -This maps the action name "myaction" to the action method we defined. - +This maps the action name "myaction" to the action class we defined. Use the new action ~~~~~~~~~~~~~~~~~~ @@ -2131,24 +2299,48 @@ In your HTML form, add a hidden form element like so:: where "myaction" is the name you registered in the previous step. +Actions may return content to the user +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Actions generally perform some database manipulation and then pass control +on to the rendering of a template in the current context (see `Determining +web context`_ for how that works.) Some actions will want to generate the +actual content returned to the user. Action methods may return their own +content string to be displayed to the user, overriding the templating step. +In this situation, we assume that the content is HTML by default. You may +override the content type indicated to the user by calling ``setHeader``:: + + self.client.setHeader('Content-Type', 'text/csv') + +This example indicates that the value sent back to the user is actually +comma-separated value content (eg. something to be loaded into a +spreadsheet or database). + Examples ======== .. contents:: :local: - :depth: 1 + :depth: 2 + + +Changing what's stored in the database +-------------------------------------- + +The following examples illustrate ways to change the information stored in +the database. Adding a new field to the classic schema ----------------------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This example shows how to add a new constrained property (i.e. a selection of distinct values) to your tracker. Introduction -~~~~~~~~~~~~ +:::::::::::: To make the classic schema of roundup useful as a TODO tracking system for a group of systems administrators, it needed an extra data field per @@ -2161,7 +2353,7 @@ best). Adding a field to the database -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +:::::::::::::::::::::::::::::: This is the easiest part of the change. The category would just be a plain string, nothing fancy. To change what is in the database you need @@ -2203,7 +2395,7 @@ is fiddling around so you can actually use the new category. Populating the new category class -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +::::::::::::::::::::::::::::::::: If you haven't initialised the database with the roundup-admin "initialise" command, then you can add the following to the tracker @@ -2234,12 +2426,11 @@ If the database has already been initalised, then you need to use the roundup> exit... There are unsaved changes. Commit them (y/N)? y -TODO: explain why order=1 in each case. Also, does key get set to "name" -automatically when added via roundup-admin? +TODO: explain why order=1 in each case. Setting up security on the new objects -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +:::::::::::::::::::::::::::::::::::::: By default only the admin user can look at and change objects. This doesn't suit us, as we want any user to be able to create new categories @@ -2292,7 +2483,7 @@ interface stuff. Changing the web left hand frame -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +:::::::::::::::::::::::::::::::: We need to give the users the ability to create new categories, and the place to put the link to this functionality is in the left hand function @@ -2334,7 +2525,7 @@ would see the "Categories" stuff. Setting up a page to edit categories -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +:::::::::::::::::::::::::::::::::::: We defined code in the previous section which let users with the appropriate permissions see a link to a page which would let them edit @@ -2454,7 +2645,7 @@ maybe a few extra to get the formatting correct). Adding the category to the issue -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +:::::::::::::::::::::::::::::::: We now have the ability to create issues to our heart's content, but that is pointless unless we can assign categories to issues. Just like @@ -2482,7 +2673,7 @@ which contains the list of currently known categories. Searching on categories -~~~~~~~~~~~~~~~~~~~~~~~ +::::::::::::::::::::::: We can add categories, and create issues with categories. The next obvious thing that we would like to be able to do, would be to search @@ -2540,7 +2731,7 @@ The category search code above would expand to the following:: Adding category to the default view -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +::::::::::::::::::::::::::::::::::: We can now add categories, add issues with categories, and search for issues based on categories. This is everything that we need to do; @@ -2581,236 +2772,166 @@ The option that we are interested in is the ``:columns=`` one which tells roundup which fields of the issue to display. Simply add "category" to that list and it all should work. +Adding a time log to your issues +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Adding in state transition control ----------------------------------- +We want to log the dates and amount of time spent working on issues, and +be able to give a summary of the total time spent on a particular issue. -Sometimes tracker admins want to control the states that users may move -issues to. You can do this by following these steps: +1. Add a new class to your tracker ``dbinit.py``:: -1. make "status" a required variable. This is achieved by adding the - following to the top of the form in the ``issue.item.html`` - template:: + # storage for time logging + timelog = Class(db, "timelog", period=Interval()) - + Note that we automatically get the date of the time log entry + creation through the standard property "creation". - this will force users to select a status. +2. Link to the new class from your issue class (again, in + ``dbinit.py``):: -2. add a Multilink property to the status class:: + issue = IssueClass(db, "issue", + assignedto=Link("user"), topic=Multilink("keyword"), + priority=Link("priority"), status=Link("status"), + times=Multilink("timelog")) - stat = Class(db, "status", ... , transitions=Multilink('status'), - ...) + the "times" property is the new link to the "timelog" class. - and then edit the statuses already created, either: +3. We'll need to let people add in times to the issue, so in the web + interface we'll have a new entry field. This is a special field + because unlike the other fields in the issue.item template, it + affects a different item (a timelog item) and not the template's + item, an issue. We have a special syntax for form fields that affect + items other than the template default item (see the cgi + documentation on `special form variables`_). In particular, we add a + field to capture a new timelog item's perdiod:: - a. through the web using the class list -> status class editor, or - b. using the roundup-admin "set" command. + + Time Log + +
(enter as '3y 1m 4d 2:40:02' or parts thereof) + + + + and another hidden field that links that new timelog item (new + because it's marked as having id "-1") to the issue item. It looks + like this:: -3. add an auditor module ``checktransition.py`` in your tracker's - ``detectors`` directory, for example:: + - def checktransition(db, cl, nodeid, newvalues): - ''' Check that the desired transition is valid for the "status" - property. - ''' - if not newvalues.has_key('status'): - return - current = cl.get(nodeid, 'status') - new = newvalues['status'] - if new == current: - return - ok = db.status.get(current, 'transitions') - if new not in ok: - raise ValueError, 'Status not allowed to move from "%s" to "%s"'%( - db.status.get(current, 'name'), db.status.get(new, 'name')) + On submission, the "-1" timelog item will be created and assigned a + real item id. The "times" property of the issue will have the new id + added to it. - def init(db): - db.issue.audit('set', checktransition) +4. We want to display a total of the time log times that have been + accumulated for an issue. To do this, we'll need to actually write + some Python code, since it's beyond the scope of PageTemplates to + perform such calculations. We do this by adding a method to the + TemplatingUtils class in our tracker ``interfaces.py`` module:: -4. in the ``issue.item.html`` template, change the status editing bit - from:: + class TemplatingUtils: + ''' Methods implemented on this class will be available to HTML + templates through the 'utils' variable. + ''' + def totalTimeSpent(self, times): + ''' Call me with a list of timelog items (which have an + Interval "period" property) + ''' + total = Interval('0d') + for time in times: + total += time.period._value + return total - Status - status + Replace the ``pass`` line if one appears in your TemplatingUtils + class. As indicated in the docstrings, we will be able to access the + ``totalTimeSpent`` method via the ``utils`` variable in our templates. - to:: +5. Display the time log for an issue:: - Status - - - - + + + + + + + + +
Time Log + +
DatePeriodLogged By
- which displays only the allowed status to transition to. + I put this just above the Messages log in my issue display. Note our + use of the ``totalTimeSpent`` method which will total up the times + for the issue and return a new Interval. That will be automatically + displayed in the template as text like "+ 1y 2:40" (1 year, 2 hours + and 40 minutes). +8. If you're using a persistent web server - roundup-server or + mod_python for example - then you'll need to restart that to pick up + the code changes. When that's done, you'll be able to use the new + time logging interface. -Displaying only message summaries in the issue display ------------------------------------------------------- -Alter the issue.item template section for messages to:: +Tracking different types of issues +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - - - - - - - - -
Messages
authordatesummary - - remove -
+Sometimes you will want to track different types of issues - developer, +customer support, systems, sales leads, etc. A single Roundup tracker is +able to support multiple types of issues. This example demonstrates adding +a customer support issue class to a tracker. -Restricting the list of users that are assignable to a task ------------------------------------------------------------ +1. Figure out what information you're going to want to capture. OK, so + this is obvious, but sometimes it's better to actually sit down for a + while and think about the schema you're going to implement. -1. In your tracker's "dbinit.py", create a new Role, say "Developer":: +2. Add the new issue class to your tracker's ``dbinit.py`` - in this + example, we're adding a "system support" class. Just after the "issue" + class definition in the "open" function, add:: - db.security.addRole(name='Developer', description='A developer') + support = IssueClass(db, "support", + assignedto=Link("user"), topic=Multilink("keyword"), + status=Link("status"), deadline=Date(), + affects=Multilink("system")) -2. Just after that, create a new Permission, say "Fixer", specific to - "issue":: +3. Copy the existing "issue.*" (item, search and index) templates in the + tracker's "html" to "support.*". Edit them so they use the properties + defined in the "support" class. Be sure to check for hidden form + variables like "required" to make sure they have the correct set of + required properties. - p = db.security.addPermission(name='Fixer', klass='issue', - description='User is allowed to be assigned to fix issues') +4. Edit the modules in the "detectors", adding lines to their "init" + functions where appropriate. Look for "audit" and "react" registrations + on the "issue" class, and duplicate them for "support". -3. Then assign the new Permission to your "Developer" Role:: +5. Create a new sidebar box for the new support class. Duplicate the + existing issues one, changing the "issue" class name to "support". - db.security.addPermissionToRole('Developer', p) +6. Re-start your tracker and start using the new "support" class. -4. In the issue item edit page ("html/issue.item.html" in your tracker - directory), use the new Permission in restricting the "assignedto" - list:: - - -For extra security, you may wish to setup an auditor to enforce the -Permission requirement (install this as "assignedtoFixer.py" in your -tracker "detectors" directory):: - - def assignedtoMustBeFixer(db, cl, nodeid, newvalues): - ''' Ensure the assignedto value in newvalues is a used with the - Fixer Permission - ''' - if not newvalues.has_key('assignedto'): - # don't care - return - - # get the userid - userid = newvalues['assignedto'] - if not db.security.hasPermission('Fixer', userid, cl.classname): - raise ValueError, 'You do not have permission to edit %s'%cl.classname - - def init(db): - db.issue.audit('set', assignedtoMustBeFixer) - db.issue.audit('create', assignedtoMustBeFixer) - -So now, if an edit action attempts to set "assignedto" to a user that -doesn't have the "Fixer" Permission, the error will be raised. - - -Setting up a "wizard" (or "druid") for controlled adding of issues ------------------------------------------------------------------- - -1. Set up the page templates you wish to use for data input. My wizard - is going to be a two-step process: first figuring out what category - of issue the user is submitting, and then getting details specific to - that category. The first page includes a table of help, explaining - what the category names mean, and then the core of the form:: +Optionally, you might want to restrict the users able to access this new +class to just the users with a new "SysAdmin" Role. To do this, we add +some security declarations:: -
- - - - Category: - - - + p = db.security.getPermission('View', 'support') + db.security.addPermissionToRole('SysAdmin', p) + p = db.security.getPermission('Edit', 'support') + db.security.addPermissionToRole('SysAdmin', p) - The next page has the usual issue entry information, with the - addition of the following form fragments:: +You would then (as an "admin" user) edit the details of the appropriate +users, and add "SysAdmin" to their Roles list. -
+Alternatively, you might want to change the Edit/View permissions granted +for the "issue" class so that it's only available to users with the "System" +or "Developer" Role, and then the new class you're adding is available to +all with the "User" Role. - - - - . - . - . -
- - Note that later in the form, I test the value of "cat" include form - elements that are appropriate. For example:: - - - - Operating System - - - - Web Browser - - - - - ... the above section will only be displayed if the category is one - of 6, 10, 13, 14, 15, 16 or 17. - -3. Determine what actions need to be taken between the pages - these are - usually to validate user choices and determine what page is next. Now - encode those actions in methods on the ``interfaces.Client`` class - and insert hooks to those actions in the "actions" attribute on that - class, like so:: - - actions = client.Client.actions + ( - ('page1_submit', 'page1SubmitAction'), - ) - - def page1SubmitAction(self): - ''' Verify that the user has selected a category, and then move - on to page 2. - ''' - category = self.form['category'].value - if category == '-1': - self.error_message.append('You must select a category of report') - return - # everything's ok, move on to the next page - self.template = 'add_page2' - -4. Use the usual "new" action as the ``@action`` on the final page, and - you're done (the standard context/submit method can do this for you). +Using External User Databases +----------------------------- Using an external password validation source --------------------------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We have a centrally-managed password changing system for our users. This results in a UN*X passwd-style file that we use for verification of @@ -2821,27 +2942,36 @@ would be:: admin:aamrgyQfDFSHw -Each user of Roundup must still have their information stored in the -Roundup database - we just use the passwd file to check their password. -To do this, we add the following code to our ``Client`` class in the -tracker home ``interfaces.py`` module:: +Each user of Roundup must still have their information stored in the Roundup +database - we just use the passwd file to check their password. To do this, we +need to override the standard ``verifyPassword`` method defined in +``roundup.cgi.actions.LoginAction`` and register the new class with our +``Client`` class in the tracker home ``interfaces.py`` module:: - def verifyPassword(self, userid, password): - # get the user's username - username = self.db.user.get(userid, 'username') + from roundup.cgi.actions import LoginAction - # the passwords are stored in the "passwd.txt" file in the - # tracker home - file = os.path.join(self.db.config.TRACKER_HOME, 'passwd.txt') + class ExternalPasswordLoginAction(LoginAction): + def verifyPassword(self, userid, password): + # get the user's username + username = self.db.user.get(userid, 'username') - # see if we can find a match - for ent in [line.strip().split(':') for line in - open(file).readlines()]: - if ent[0] == username: - return crypt.crypt(password, ent[1][:2]) == ent[1] + # the passwords are stored in the "passwd.txt" file in the + # tracker home + file = os.path.join(self.db.config.TRACKER_HOME, 'passwd.txt') - # user doesn't exist in the file - return 0 + # see if we can find a match + for ent in [line.strip().split(':') for line in + open(file).readlines()]: + if ent[0] == username: + return crypt.crypt(password, ent[1][:2]) == ent[1] + + # user doesn't exist in the file + return 0 + + class Client(client.Client): + actions = client.Client.actions + ( + ('login', ExternalPasswordLoginAction) + ) What this does is look through the file, line by line, looking for a name that matches. @@ -2850,206 +2980,8 @@ We also remove the redundant password fields from the ``user.item`` template. -Adding a "vacation" flag to users for stopping nosy messages ------------------------------------------------------------- - -When users go on vacation and set up vacation email bouncing, you'll -start to see a lot of messages come back through Roundup "Fred is on -vacation". Not very useful, and relatively easy to stop. - -1. add a "vacation" flag to your users:: - - user = Class(db, "user", - username=String(), password=Password(), - address=String(), realname=String(), - phone=String(), organisation=String(), - alternate_addresses=String(), - roles=String(), queries=Multilink("query"), - vacation=Boolean()) - -2. So that users may edit the vacation flags, add something like the - following to your ``user.item`` template:: - - - On Vacation - vacation - - -3. edit your detector ``nosyreactor.py`` so that the ``nosyreaction()`` - consists of:: - - def nosyreaction(db, cl, nodeid, oldvalues): - # send a copy of all new messages to the nosy list - for msgid in determineNewMessages(cl, nodeid, oldvalues): - try: - users = db.user - messages = db.msg - - # figure the recipient ids - sendto = [] - r = {} - recipients = messages.get(msgid, 'recipients') - for recipid in messages.get(msgid, 'recipients'): - r[recipid] = 1 - - # figure the author's id, and indicate they've received - # the message - authid = messages.get(msgid, 'author') - - # possibly send the message to the author, as long as - # they aren't anonymous - if (db.config.MESSAGES_TO_AUTHOR == 'yes' and - users.get(authid, 'username') != 'anonymous'): - sendto.append(authid) - r[authid] = 1 - - # now figure the nosy people who weren't recipients - nosy = cl.get(nodeid, 'nosy') - for nosyid in nosy: - # Don't send nosy mail to the anonymous user (that - # user shouldn't appear in the nosy list, but just - # in case they do...) - if users.get(nosyid, 'username') == 'anonymous': - continue - # make sure they haven't seen the message already - if not r.has_key(nosyid): - # send it to them - sendto.append(nosyid) - recipients.append(nosyid) - - # generate a change note - if oldvalues: - note = cl.generateChangeNote(nodeid, oldvalues) - else: - note = cl.generateCreateNote(nodeid) - - # we have new recipients - if sendto: - # filter out the people on vacation - sendto = [i for i in sendto - if not users.get(i, 'vacation', 0)] - - # map userids to addresses - sendto = [users.get(i, 'address') for i in sendto] - - # update the message's recipients list - messages.set(msgid, recipients=recipients) - - # send the message - cl.send_message(nodeid, msgid, note, sendto) - except roundupdb.MessageSendError, message: - raise roundupdb.DetectorError, message - - Note that this is the standard nosy reaction code, with the small - addition of:: - - # filter out the people on vacation - sendto = [i for i in sendto if not users.get(i, 'vacation', 0)] - - which filters out the users that have the vacation flag set to true. - - -Adding a time log to your issues --------------------------------- - -We want to log the dates and amount of time spent working on issues, and -be able to give a summary of the total time spent on a particular issue. - -1. Add a new class to your tracker ``dbinit.py``:: - - # storage for time logging - timelog = Class(db, "timelog", period=Interval()) - - Note that we automatically get the date of the time log entry - creation through the standard property "creation". - -2. Link to the new class from your issue class (again, in - ``dbinit.py``):: - - issue = IssueClass(db, "issue", - assignedto=Link("user"), topic=Multilink("keyword"), - priority=Link("priority"), status=Link("status"), - times=Multilink("timelog")) - - the "times" property is the new link to the "timelog" class. - -3. We'll need to let people add in times to the issue, so in the web - interface we'll have a new entry field. This is a special field - because unlike the other fields in the issue.item template, it - affects a different item (a timelog item) and not the template's - item, an issue. We have a special syntax for form fields that affect - items other than the template default item (see the cgi - documentation on `special form variables`_). In particular, we add a - field to capture a new timelog item's perdiod:: - - - Time Log - -
(enter as '3y 1m 4d 2:40:02' or parts thereof) - - - - and another hidden field that links that new timelog item (new - because it's marked as having id "-1") to the issue item. It looks - like this:: - - - - On submission, the "-1" timelog item will be created and assigned a - real item id. The "times" property of the issue will have the new id - added to it. - -4. We want to display a total of the time log times that have been - accumulated for an issue. To do this, we'll need to actually write - some Python code, since it's beyond the scope of PageTemplates to - perform such calculations. We do this by adding a method to the - TemplatingUtils class in our tracker ``interfaces.py`` module:: - - class TemplatingUtils: - ''' Methods implemented on this class will be available to HTML - templates through the 'utils' variable. - ''' - def totalTimeSpent(self, times): - ''' Call me with a list of timelog items (which have an - Interval "period" property) - ''' - total = Interval('0d') - for time in times: - total += time.period._value - return total - - Replace the ``pass`` line if one appears in your TemplatingUtils - class. As indicated in the docstrings, we will be able to access the - ``totalTimeSpent`` method via the ``utils`` variable in our templates. - -5. Display the time log for an issue:: - - - - - - - - - -
Time Log - -
DatePeriodLogged By
- - I put this just above the Messages log in my issue display. Note our - use of the ``totalTimeSpent`` method which will total up the times - for the issue and return a new Interval. That will be automatically - displayed in the template as text like "+ 1y 2:40" (1 year, 2 hours - and 40 minutes). - -8. If you're using a persistent web server - roundup-server or - mod_python for example - then you'll need to restart that to pick up - the code changes. When that's done, you'll be able to use the new - time logging interface. - Using a UN*X passwd file as the user database ---------------------------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ On some systems the primary store of users is the UN*X passwd file. It holds information on users such as their username, real name, password @@ -3185,7 +3117,7 @@ And that's it! Using an LDAP database for user information -------------------------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A script that reads users from an LDAP store using http://python-ldap.sf.net/ and then compares the list to the users in the @@ -3196,9 +3128,10 @@ for more information about doing this. To authenticate off the LDAP store (rather than using the passwords in the roundup user database) you'd use the same python-ldap module inside an -extension to the cgi interface. You'd do this by adding a method called -"verifyPassword" to the Client class in your tracker's interfaces.py -module. The method is implemented by default as:: +extension to the cgi interface. You'd do this by overriding the method called +"verifyPassword" on the LoginAction class in your tracker's interfaces.py +module (see `using an external password validation source`_). The method is +implemented by default as:: def verifyPassword(self, userid, password): ''' Verify the password that the user has supplied @@ -3220,56 +3153,180 @@ So you could reimplement this as something like:: # now verify the password supplied against the LDAP store -Enabling display of either message summaries or the entire messages -------------------------------------------------------------------- +Changes to Tracker Behaviour +---------------------------- -This is pretty simple - all we need to do is copy the code from the -example `displaying only message summaries in the issue display`_ into -our template alongside the summary display, and then introduce a switch -that shows either one or the other. We'll use a new form variable, -``@whole_messages`` to achieve this:: +Stop "nosy" messages going to people on vacation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - - - - - - - - - +When users go on vacation and set up vacation email bouncing, you'll +start to see a lot of messages come back through Roundup "Fred is on +vacation". Not very useful, and relatively easy to stop. + +1. add a "vacation" flag to your users:: + + user = Class(db, "user", + username=String(), password=Password(), + address=String(), realname=String(), + phone=String(), organisation=String(), + alternate_addresses=String(), + roles=String(), queries=Multilink("query"), + vacation=Boolean()) + +2. So that users may edit the vacation flags, add something like the + following to your ``user.item`` template:: + + + + + + +3. edit your detector ``nosyreactor.py`` so that the ``nosyreaction()`` + consists of:: + + def nosyreaction(db, cl, nodeid, oldvalues): + users = db.user + messages = db.msg + # send a copy of all new messages to the nosy list + for msgid in determineNewMessages(cl, nodeid, oldvalues): + try: + # figure the recipient ids + sendto = [] + seen_message = {} + recipients = messages.get(msgid, 'recipients') + for recipid in messages.get(msgid, 'recipients'): + seen_message[recipid] = 1 + + # figure the author's id, and indicate they've received + # the message + authid = messages.get(msgid, 'author') + + # possibly send the message to the author, as long as + # they aren't anonymous + if (db.config.MESSAGES_TO_AUTHOR == 'yes' and + users.get(authid, 'username') != 'anonymous'): + sendto.append(authid) + seen_message[authid] = 1 + + # now figure the nosy people who weren't recipients + nosy = cl.get(nodeid, 'nosy') + for nosyid in nosy: + # Don't send nosy mail to the anonymous user (that + # user shouldn't appear in the nosy list, but just + # in case they do...) + if users.get(nosyid, 'username') == 'anonymous': + continue + # make sure they haven't seen the message already + if not seen_message.has_key(nosyid): + # send it to them + sendto.append(nosyid) + recipients.append(nosyid) + + # generate a change note + if oldvalues: + note = cl.generateChangeNote(nodeid, oldvalues) + else: + note = cl.generateCreateNote(nodeid) + + # we have new recipients + if sendto: + # filter out the people on vacation + sendto = [i for i in sendto + if not users.get(i, 'vacation', 0)] + + # map userids to addresses + sendto = [users.get(i, 'address') for i in sendto] + + # update the message's recipients list + messages.set(msgid, recipients=recipients) + + # send the message + cl.send_message(nodeid, msgid, note, sendto) + except roundupdb.MessageSendError, message: + raise roundupdb.DetectorError, message + + Note that this is the standard nosy reaction code, with the small + addition of:: + + # filter out the people on vacation + sendto = [i for i in sendto if not users.get(i, 'vacation', 0)] + + which filters out the users that have the vacation flag set to true. + +Adding in state transition control +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes tracker admins want to control the states that users may move +issues to. You can do this by following these steps: + +1. make "status" a required variable. This is achieved by adding the + following to the top of the form in the ``issue.item.html`` + template:: + + + + this will force users to select a status. + +2. add a Multilink property to the status class:: + + stat = Class(db, "status", ... , transitions=Multilink('status'), + ...) + + and then edit the statuses already created, either: + + a. through the web using the class list -> status class editor, or + b. using the roundup-admin "set" command. + +3. add an auditor module ``checktransition.py`` in your tracker's + ``detectors`` directory, for example:: + + def checktransition(db, cl, nodeid, newvalues): + ''' Check that the desired transition is valid for the "status" + property. + ''' + if not newvalues.has_key('status'): + return + current = cl.get(nodeid, 'status') + new = newvalues['status'] + if new == current: + return + ok = db.status.get(current, 'transitions') + if new not in ok: + raise ValueError, 'Status not allowed to move from "%s" to "%s"'%( + db.status.get(current, 'name'), db.status.get(new, 'name')) + + def init(db): + db.issue.audit('set', checktransition) + +4. in the ``issue.item.html`` template, change the status editing bit + from:: + + + + + to:: + + - - - - - - - - - - - - - - - -
Messages - show entire messages -
authordatesummary
On Vacationvacation
StatusstatusStatus - remove + +
Messages - show only summaries -
authordate - (remove) -
+ which displays only the allowed status to transition to. Blocking issues that depend on other issues -------------------------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We needed the ability to mark certain issues as "blockers" - that is, they can't be resolved until another issue (the blocker) they rely on is @@ -3401,7 +3458,7 @@ history at the bottom of the issue page - look for a "link" event to another issue's "blockers" property. Add users to the nosy list based on the topic ---------------------------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We need the ability to automatically add users to the nosy list based on the occurence of a topic. Every user should be allowed to edit his @@ -3418,7 +3475,7 @@ this list of topics, and addition of an auditor which updates the nosy list when a topic is set. Adding the nosy topic list -~~~~~~~~~~~~~~~~~~~~~~~~~~ +:::::::::::::::::::::::::: The change in the database to make is that for any user there should be a list of topics for which he wants to be put on the nosy list. Adding @@ -3438,7 +3495,7 @@ the updated definition of user will be:: nosy_keywords=Multilink('keyword')) Changing the user view to allow changing the nosy topic list -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: We want any user to be able to change the list of topics for which he will by default be added to the nosy list. We choose to add this @@ -3461,7 +3518,7 @@ E-mail addresses' in the classic template):: Addition of an auditor to update the nosy list -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +:::::::::::::::::::::::::::::::::::::::::::::: The more difficult part is the addition of the logic to actually at the users to the nosy list when it is required. @@ -3543,7 +3600,7 @@ and these two function are the only ones needed in the file. TODO: update this example to use the find() Class method. Caveats -~~~~~~~ +::::::: A few problems with the design here can be noted: @@ -3567,26 +3624,68 @@ Scalability selected these topics a nosy topics. This will eliminate the loop over all users. +Changes to Security and Permissions +----------------------------------- -Adding action links to the index page -------------------------------------- +Restricting the list of users that are assignable to a task +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Add a column to the item.index.html template. +1. In your tracker's "dbinit.py", create a new Role, say "Developer":: -Resolving the issue:: + db.security.addRole(name='Developer', description='A developer') - resolve +2. Just after that, create a new Permission, say "Fixer", specific to + "issue":: -"Take" the issue:: + p = db.security.addPermission(name='Fixer', klass='issue', + description='User is allowed to be assigned to fix issues') - take +3. Then assign the new Permission to your "Developer" Role:: -... and so on + db.security.addPermissionToRole('Developer', p) + +4. In the issue item edit page ("html/issue.item.html" in your tracker + directory), use the new Permission in restricting the "assignedto" + list:: + + + +For extra security, you may wish to setup an auditor to enforce the +Permission requirement (install this as "assignedtoFixer.py" in your +tracker "detectors" directory):: + + def assignedtoMustBeFixer(db, cl, nodeid, newvalues): + ''' Ensure the assignedto value in newvalues is a used with the + Fixer Permission + ''' + if not newvalues.has_key('assignedto'): + # don't care + return + + # get the userid + userid = newvalues['assignedto'] + if not db.security.hasPermission('Fixer', userid, cl.classname): + raise ValueError, 'You do not have permission to edit %s'%cl.classname + + def init(db): + db.issue.audit('set', assignedtoMustBeFixer) + db.issue.audit('create', assignedtoMustBeFixer) + +So now, if an edit action attempts to set "assignedto" to a user that +doesn't have the "Fixer" Permission, the error will be raised. Users may only edit their issues --------------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Users registering themselves are granted Provisional access - meaning they have access to edit the issues they submit, but not others. We create a new @@ -3655,8 +3754,28 @@ template as follows:: line). +Changes to the Web User Interface +--------------------------------- + +Adding action links to the index page +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Add a column to the item.index.html template. + +Resolving the issue:: + + resolve + +"Take" the issue:: + + take + +... and so on + Colouring the rows in the issue index according to priority ------------------------------------------------------------ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A simple ``tal:attributes`` statement will do the bulk of the work here. In the ``issue.index.html`` template, add to the ```` that displays the @@ -3677,6 +3796,201 @@ different priorities, like:: and so on, with far less offensive colours :) +Editing multiple items in an index view +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To edit the status of all items in the item index view, edit the +``issue.item.html``: + +1. add a form around the listing table, so at the top it reads:: + +
+ + + and at the bottom of that table:: + +
+
`` from the list table, not the + navigation table or the subsequent form table. + +2. in the display for the issue property, change:: + +   + + to:: + +   + + this will result in an edit field for the status property. + +3. after the ``tal:block`` which lists the actual index items (marked by + ``tal:repeat="i batch"``) add a new table row:: + + + + + + + + + + which gives us a submit button, indicates that we are performing an edit + on any changed statuses and the final block will make sure that the + current index view parameters (filtering, columns, etc) will be used in + rendering the next page (the results of the editing). + + +Displaying only message summaries in the issue display +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Alter the issue.item template section for messages to:: + + + + + + + + + + +
Messages
authordatesummary + + remove +
+ + +Enabling display of either message summaries or the entire messages +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This is pretty simple - all we need to do is copy the code from the +example `displaying only message summaries in the issue display`_ into +our template alongside the summary display, and then introduce a switch +that shows either one or the other. We'll use a new form variable, +``@whole_messages`` to achieve this:: + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Messages + show entire messages +
authordatesummary + remove +
Messages + show only summaries +
authordate + (remove) +
+ +Setting up a "wizard" (or "druid") for controlled adding of issues +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +1. Set up the page templates you wish to use for data input. My wizard + is going to be a two-step process: first figuring out what category + of issue the user is submitting, and then getting details specific to + that category. The first page includes a table of help, explaining + what the category names mean, and then the core of the form:: + +
+ + + + Category: + + + + + The next page has the usual issue entry information, with the + addition of the following form fragments:: + +
+ + + + + . + . + . +
+ + Note that later in the form, I test the value of "cat" include form + elements that are appropriate. For example:: + + + + Operating System + + + + Web Browser + + + + + ... the above section will only be displayed if the category is one + of 6, 10, 13, 14, 15, 16 or 17. + +3. Determine what actions need to be taken between the pages - these are + usually to validate user choices and determine what page is next. Now encode + those actions in a new ``Action`` class and insert hooks to those actions in + the "actions" attribute on on the ``interfaces.Client`` class, like so (see + `defining new web actions`_):: + + class Page1SubmitAction(Action): + def handle(self): + ''' Verify that the user has selected a category, and then move + on to page 2. + ''' + category = self.form['category'].value + if category == '-1': + self.error_message.append('You must select a category of report') + return + # everything's ok, move on to the next page + self.template = 'add_page2' + + actions = client.Client.actions + ( + ('page1_submit', Page1SubmitAction), + ) + +4. Use the usual "new" action as the ``@action`` on the final page, and + you're done (the standard context/submit method can do this for you). + + + ------------------- Back to `Table of Contents`_