X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=doc%2Fcustomizing.txt;h=ddcd33ea6edf65df6d13d654fcdf924c41e65f1e;hb=1d2361aba006cdd3fd144032a19de1d01467770e;hp=e5170c32c13e25001b8a4490a8ee0546b08a2c01;hpb=697470498ceb8616fc9d85afde7395d5a5451c17;p=roundup.git diff --git a/doc/customizing.txt b/doc/customizing.txt index e5170c3..ddcd33e 100644 --- a/doc/customizing.txt +++ b/doc/customizing.txt @@ -2,7 +2,7 @@ Customising Roundup =================== -:Version: $Revision: 1.119 $ +:Version: $Revision: 1.133 $ .. 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 @@ -193,6 +203,11 @@ The configuration variables available are: 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.:: @@ -280,6 +295,10 @@ tracker is attempted.:: 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 # @@ -298,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()) @@ -543,6 +585,9 @@ interface for detectors. __ design.html +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: @@ -552,28 +597,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 @@ -768,7 +847,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`` @@ -826,18 +904,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 @@ -861,6 +939,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 ---------------------------------- @@ -1509,6 +1594,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 @@ -1607,14 +1695,34 @@ local only on Date properties - return this date as a new property python:context.creation.local(10) will render the date with a +10 hour offset. -pretty only on Interval properties - render the interval in a pretty +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 ~~~~~~~~~~~~~~~~~~~~ @@ -1750,6 +1858,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 @@ -2173,18 +2283,25 @@ 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 @@ -2197,7 +2314,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 @@ -2239,7 +2356,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 @@ -2270,12 +2387,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 @@ -2328,7 +2444,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 @@ -2370,7 +2486,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 @@ -2490,7 +2606,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 @@ -2518,7 +2634,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 @@ -2576,7 +2692,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; @@ -2617,237 +2733,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 ----------------------------------- - -Sometimes tracker admins want to control the states that users may move -issues to. You can do this by following these steps: +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. make "status" a required variable. This is achieved by adding the - following to the top of the form in the ``issue.item.html`` - template:: +1. Add a new class to your tracker ``dbinit.py``:: - + # storage for time logging + timelog = Class(db, "timelog", period=Interval()) - this will force users to select a status. + Note that we automatically get the date of the time log entry + creation through the standard property "creation". -2. add a Multilink property to the status class:: +2. Link to the new class from your issue class (again, in + ``dbinit.py``):: - stat = Class(db, "status", ... , transitions=Multilink('status'), - ...) + issue = IssueClass(db, "issue", + assignedto=Link("user"), topic=Multilink("keyword"), + priority=Link("priority"), status=Link("status"), + times=Multilink("timelog")) - and then edit the statuses already created, either: + the "times" property is the new link to the "timelog" class. - a. through the web using the class list -> status class editor, or - b. using the roundup-admin "set" command. +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:: -3. add an auditor module ``checktransition.py`` in your tracker's - ``detectors`` directory, for example:: + + 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:: - 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) + 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. in the ``issue.item.html`` template, change the status editing bit - from:: +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:: - Status - status + 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 - to:: + 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. - Status - - - - +5. Display the time log for an issue:: - which displays only the allowed status to transition to. + + + + + + + + +
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). -Displaying only message summaries in the issue display ------------------------------------------------------- +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. -Alter the issue.item template section for messages to:: - - - - - - - - - -
Messages
authordatesummary - - remove -
+Tracking different types of issues +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Restricting the list of users that are assignable to a task ------------------------------------------------------------ +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. -1. In your tracker's "dbinit.py", create a new Role, say "Developer":: +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. - db.security.addRole(name='Developer', description='A 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:: -2. Just after that, create a new Permission, say "Fixer", specific to - "issue":: + support = IssueClass(db, "support", + assignedto=Link("user"), topic=Multilink("keyword"), + status=Link("status"), deadline=Date(), + affects=Multilink("system")) - p = db.security.addPermission(name='Fixer', klass='issue', - description='User is allowed to be assigned to fix issues') +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. -3. Then assign the new Permission to your "Developer" Role:: +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". - db.security.addPermissionToRole('Developer', p) +5. Create a new sidebar box for the new support class. Duplicate the + existing issues one, changing the "issue" class name to "support". -4. In the issue item edit page ("html/issue.item.html" in your tracker - directory), use the new Permission in restricting the "assignedto" - list:: +6. Re-start your tracker and start using the new "support" class. - -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):: +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:: - 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 + p = db.security.getPermission('View', 'support') + db.security.addPermissionToRole('SysAdmin', p) + p = db.security.getPermission('Edit', 'support') + db.security.addPermissionToRole('SysAdmin', p) - def init(db): - db.issue.audit('set', assignedtoMustBeFixer) - db.issue.audit('create', assignedtoMustBeFixer) +You would then (as an "admin" user) edit the details of the appropriate +users, and add "SysAdmin" to their Roles list. -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. +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. -Setting up a "wizard" (or "druid") for controlled adding of issues ------------------------------------------------------------------- +Using External User Databases +----------------------------- -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 exsample:: - - - - 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). - - -Using an external password validation source --------------------------------------------- +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 @@ -2896,205 +2941,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): - 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 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 @@ -3230,7 +3078,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 @@ -3266,56 +3114,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 @@ -3447,7 +3419,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 @@ -3464,7 +3436,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 @@ -3484,7 +3456,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 @@ -3507,7 +3479,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. @@ -3589,7 +3561,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: @@ -3613,26 +3585,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 @@ -3701,8 +3715,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 @@ -3724,7 +3758,7 @@ 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``: @@ -3770,6 +3804,154 @@ To edit the status of all items in the item index view, edit 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`_