comparison doc/customizing.txt @ 7352:f3c9ba5db30b

split reference out from customizing and fix all links. also reformat a couple of lists. Still to do verify that references to customizing.txt from the rest of the docs should still point there.
author John Rouillard <rouilj@ieee.org>
date Tue, 16 May 2023 00:56:00 -0400
parents bee24b37a90f
children 2b1cbe079ff5
comparison
equal deleted inserted replaced
7351:1f8e41b0e97f 7352:f3c9ba5db30b
25 Before you get too far, it's probably worth having a quick read of the Roundup 25 Before you get too far, it's probably worth having a quick read of the Roundup
26 `design documentation`_. 26 `design documentation`_.
27 27
28 Customisation of Roundup can take one of six forms: 28 Customisation of Roundup can take one of six forms:
29 29
30 1. `tracker configuration`_ changes 30 1. `tracker configuration <reference.html#tracker-configuration>`_ changes
31 2. database, or `tracker schema`_ changes 31 2. database, or `tracker schema <reference.html#tracker-schema>`_ changes
32 3. "definition" class `database content`_ changes 32 3. "definition" class `database content <reference.html#database-content>`_ changes
33 4. behavioural changes through detectors_, extensions_ and interfaces.py_ 33 4. behavioural changes through `detectors <reference.html#detectors>`_,
34 5. `security / access controls`_ 34 `extensions <reference.html#extensions>`_ and
35 6. change the `web interface`_ 35 `interfaces.py <reference.html#interfaces-py>`_
36 5. `security / access controls <reference.html#security-access-controls>`_
37 6. change the `web interface <reference.html#web-interface>`_
36 38
37 The third case is special because it takes two distinctly different forms 39 The third case is special because it takes two distinctly different forms
38 depending upon whether the tracker has been initialised or not. The other two 40 depending upon whether the tracker has been initialised or not. The other two
39 may be done at any time, before or after tracker initialisation. Yes, this 41 may be done at any time, before or after tracker initialisation. Yes, this
40 includes adding or removing properties from classes. 42 includes adding or removing properties from classes.
41 43
42 44 .. _CustomExamples:
43 Trackers in a Nutshell 45
44 ====================== 46 Examples
45 47 ========
46 Trackers have the following structure: 48
47 49 .. contents::
48 .. index:: 50 :local:
49 single: tracker; structure db directory 51 :depth: 2
50 single: tracker; structure detectors directory 52
51 single: tracker; structure extensions directory 53
52 single: tracker; structure html directory 54 Changing what's stored in the database
53 single: tracker; structure html directory 55 --------------------------------------
54 single: tracker; structure lib directory 56
55 57 The following examples illustrate ways to change the information stored in
56 =================== ======================================================== 58 the database.
57 Tracker File Description 59
58 =================== ======================================================== 60
59 config.ini Holds the basic `tracker configuration`_ 61 Adding a new field to the classic schema
60 schema.py Holds the `tracker schema`_ 62 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
61 initial_data.py Holds any data to be entered into the database when the 63
62 tracker is initialised (optional) 64 This example shows how to add a simple field (a due date) to the default
63 interfaces.py Allows `modifying the core of Roundup`_ (optional) 65 classic schema. It does not add any additional behaviour, such as enforcing
64 db/ Holds the tracker's database 66 the due date, or causing automatic actions to fire if the due date passes.
65 db/files/ Holds the tracker's upload files and messages 67
66 db/backend_name Names the database back-end for the tracker (obsolete). 68 You add new fields by editing the ``schema.py`` file in you tracker's home.
67 Use the ``backend`` setting in the ``[rdbms]`` 69 Schema changes are automatically applied to the database on the next
68 section of ``config.ini`` instead. 70 tracker access (note that roundup-server would need to be restarted as it
69 detectors/ `Auditors and reactors`_ for this tracker 71 caches the schema).
70 extensions/ Additional `actions`_ and `templating utilities`_ 72
71 html/ Web interface templates, images and style sheets 73 .. index:: schema; example changes
72 lib/ optional common imports for detectors and extensions 74
73 =================== ======================================================== 75 1. Modify the ``schema.py``::
74 76
75 77 issue = IssueClass(db, "issue",
76 .. index:: config.ini 78 assignedto=Link("user"), keyword=Multilink("keyword"),
77 .. index:: configuration; see config.ini 79 priority=Link("priority"), status=Link("status"),
78 80 due_date=Date())
79 Tracker Configuration 81
80 ===================== 82 2. Add an edit field to the ``issue.item.html`` template::
81 83
82 The ``config.ini`` located in your tracker home contains the basic 84 <tr>
83 configuration for the web and e-mail components of Roundup's interfaces. 85 <th>Due Date</th>
84 86 <td tal:content="structure context/due_date/field" />
85 Changes to the data captured by your tracker is controlled by the `tracker 87 </tr>
86 schema`_. Some configuration is also performed using permissions - see the 88
87 `security / access controls`_ section. For example, to allow users to 89 If you want to show only the date part of due_date then do this instead::
88 automatically register through the email interface, you must grant the 90
89 "Anonymous" Role the "Email Access" Permission. 91 <tr>
90 92 <th>Due Date</th>
91 .. index:: 93 <td tal:content="structure python:context.due_date.field(format='%Y-%m-%d')" />
92 single: config.ini; sections 94 </tr>
93 see: configuration; config.ini 95
94 96 3. Add the property to the ``issue.index.html`` page::
95 The following is taken from the `Python Library Reference`__ (July 18, 2018) 97
96 section "ConfigParser -- Configuration file parser": 98 (in the heading row)
97 99 <th tal:condition="request/show/due_date">Due Date</th>
98 The configuration file consists of sections, led by a [section] header 100 (in the data row)
99 and followed by name: value entries, with continuations in the style 101 <td tal:condition="request/show/due_date"
100 of RFC 822 (see section 3.1.1, “LONG HEADER FIELDS”); name=value is 102 tal:content="i/due_date" />
101 also accepted. Note that leading whitespace is removed from 103
102 values. The optional values can contain format strings which refer to 104 If you want format control of the display of the due date you can
103 other values in the same section, or values in a special DEFAULT 105 enter the following in the data row to show only the actual due date::
104 section. Additional defaults can be provided on initialization and 106
105 retrieval. Lines beginning with '#' or ';' are ignored and may be 107 <td tal:condition="request/show/due_date"
106 used to provide comments. 108 tal:content="python:i.due_date.pretty('%Y-%m-%d')">&nbsp;</td>
107 109
108 For example:: 110 4. Add the property to the ``issue.search.html`` page::
109 111
110 [My Section] 112 <tr tal:define="name string:due_date">
111 foodir = %(dir)s/whatever 113 <th i18n:translate="">Due Date:</th>
112 dir = frob 114 <td metal:use-macro="search_input"></td>
113 115 <td metal:use-macro="column_input"></td>
114 would resolve the "%(dir)s" to the value of "dir" ("frob" in this case) 116 <td metal:use-macro="sort_input"></td>
115 resulting in "foodir" being "frob/whatever". 117 <td metal:use-macro="group_input"></td>
116 118 </tr>
117 __ https://docs.python.org/2/library/configparser.html 119
118 120 5. If you wish for the due date to appear in the standard views listed
119 Example configuration settings are below. This is a partial 121 in the sidebar of the web interface then you'll need to add "due_date"
120 list. Documentation on all the settings is included in the 122 to the columns and columns_showall lists in your ``page.html``::
121 ``config.ini`` file. 123
124 columns string:id,activity,due_date,title,creator,status;
125 columns_showall string:id,activity,due_date,title,creator,assignedto,status;
126
127 Adding a new constrained field to the classic schema
128 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
129
130 This example shows how to add a new constrained property (i.e. a
131 selection of distinct values) to your tracker.
132
133
134 Introduction
135 ::::::::::::
136
137 To make the classic schema of Roundup useful as a TODO tracking system
138 for a group of systems administrators, it needs an extra data field per
139 issue: a category.
140
141 This would let sysadmins quickly list all TODOs in their particular area
142 of interest without having to do complex queries, and without relying on
143 the spelling capabilities of other sysadmins (a losing proposition at
144 best).
145
146
147 Adding a field to the database
148 ::::::::::::::::::::::::::::::
149
150 This is the easiest part of the change. The category would just be a
151 plain string, nothing fancy. To change what is in the database you need
152 to add some lines to the ``schema.py`` file of your tracker instance.
153 Under the comment::
154
155 # add any additional database schema configuration here
156
157 add::
158
159 category = Class(db, "category", name=String())
160 category.setkey("name")
161
162 Here we are setting up a chunk of the database which we are calling
163 "category". It contains a string, which we are refering to as "name" for
164 lack of a more imaginative title. (Since "name" is one of the properties
165 that Roundup looks for on items if you do not set a key for them, it's
166 probably a good idea to stick with it for new classes if at all
167 appropriate.) Then we are setting the key of this chunk of the database
168 to be that "name". This is equivalent to an index for database types.
169 This also means that there can only be one category with a given name.
170
171 Adding the above lines allows us to create categories, but they're not
172 tied to the issues that we are going to be creating. It's just a list of
173 categories off on its own, which isn't much use. We need to link it in
174 with the issues. To do that, find the lines
175 in ``schema.py`` which set up the "issue" class, and then add a link to
176 the category::
177
178 issue = IssueClass(db, "issue", ... ,
179 category=Multilink("category"), ... )
180
181 The ``Multilink()`` means that each issue can have many categories. If
182 you were adding something with a one-to-one relationship to issues (such
183 as the "assignedto" property), use ``Link()`` instead.
184
185 That is all you need to do to change the schema. The rest of the effort
186 is fiddling around so you can actually use the new category.
187
188
189 Populating the new category class
190 :::::::::::::::::::::::::::::::::
191
192 If you haven't initialised the database with the
193 "``roundup-admin initialise``" command, then you
194 can add the following to the tracker ``initial_data.py``
195 under the comment::
196
197 # add any additional database creation steps here - but only if you
198 # haven't initialised the database with the admin "initialise" command
199
200 Add::
201
202 category = db.getclass('category')
203 category.create(name="scipy")
204 category.create(name="chaco")
205 category.create(name="weave")
206
207 .. index:: roundup-admin; create entries in class
208
209 If the database has already been initalised, then you need to use the
210 ``roundup-admin`` tool::
211
212 % roundup-admin -i <tracker home>
213 Roundup <version> ready for input.
214 Type "help" for help.
215 roundup> create category name=scipy
216 1
217 roundup> create category name=chaco
218 2
219 roundup> create category name=weave
220 3
221 roundup> exit...
222 There are unsaved changes. Commit them (y/N)? y
223
224
225 Setting up security on the new objects
226 ::::::::::::::::::::::::::::::::::::::
227
228 By default only the admin user can look at and change objects. This
229 doesn't suit us, as we want any user to be able to create new categories
230 as required, and obviously everyone needs to be able to view the
231 categories of issues for it to be useful.
232
233 We therefore need to change the security of the category objects. This
234 is also done in ``schema.py``.
235
236 There are currently two loops which set up permissions and then assign
237 them to various roles. Simply add the new "category" to both lists::
238
239 # Assign the access and edit permissions for issue, file and message
240 # to regular users now
241 for cl in 'issue', 'file', 'msg', 'category':
242 p = db.security.getPermission('View', cl)
243 db.security.addPermissionToRole('User', 'View', cl)
244 db.security.addPermissionToRole('User', 'Edit', cl)
245 db.security.addPermissionToRole('User', 'Create', cl)
246
247 These lines assign the "View" and "Edit" Permissions to the "User" role,
248 so that normal users can view and edit "category" objects.
249
250 This is all the work that needs to be done for the database. It will
251 store categories, and let users view and edit them. Now on to the
252 interface stuff.
253
254
255 Changing the web left hand frame
256 ::::::::::::::::::::::::::::::::
257
258 We need to give the users the ability to create new categories, and the
259 place to put the link to this functionality is in the left hand function
260 bar, under the "Issues" area. The file that defines how this area looks
261 is ``html/page.html``, which is what we are going to be editing next.
262
263 If you look at this file you can see that it contains a lot of
264 "classblock" sections which are chunks of HTML that will be included or
265 excluded in the output depending on whether the condition in the
266 classblock is met. We are going to add the category code at the end of
267 the classblock for the *issue* class::
268
269 <p class="classblock"
270 tal:condition="python:request.user.hasPermission('View', 'category')">
271 <b>Categories</b><br>
272 <a tal:condition="python:request.user.hasPermission('Edit', 'category')"
273 href="category?@template=item">New Category<br></a>
274 </p>
275
276 The first two lines is the classblock definition, which sets up a
277 condition that only users who have "View" permission for the "category"
278 object will have this section included in their output. Next comes a
279 plain "Categories" header in bold. Everyone who can view categories will
280 get that.
281
282 Next comes the link to the editing area of categories. This link will
283 only appear if the condition - that the user has "Edit" permissions for
284 the "category" objects - is matched. If they do have permission then
285 they will get a link to another page which will let the user add new
286 categories.
287
288 Note that if you have permission to *view* but not to *edit* categories,
289 then all you will see is a "Categories" header with nothing underneath
290 it. This is obviously not very good interface design, but will do for
291 now. I just claim that it is so I can add more links in this section
292 later on. However, to fix the problem you could change the condition in
293 the classblock statement, so that only users with "Edit" permission
294 would see the "Categories" stuff.
295
296
297 Setting up a page to edit categories
298 ::::::::::::::::::::::::::::::::::::
299
300 We defined code in the previous section which let users with the
301 appropriate permissions see a link to a page which would let them edit
302 conditions. Now we have to write that page.
303
304 The link was for the *item* template of the *category* object. This
305 translates into Roundup looking for a file called ``category.item.html``
306 in the ``html`` tracker directory. This is the file that we are going to
307 write now.
308
309 First, we add an info tag in a comment which doesn't affect the outcome
310 of the code at all, but is useful for debugging. If you load a page in a
311 browser and look at the page source, you can see which sections come
312 from which files by looking for these comments::
313
314 <!-- category.item -->
315
316 Next we need to add in the METAL macro stuff so we get the normal page
317 trappings::
318
319 <tal:block metal:use-macro="templates/page/macros/icing">
320 <title metal:fill-slot="head_title">Category editing</title>
321 <td class="page-header-top" metal:fill-slot="body_title">
322 <h2>Category editing</h2>
323 </td>
324 <td class="content" metal:fill-slot="content">
325
326 Next we need to setup up a standard HTML form, which is the whole
327 purpose of this file. We link to some handy javascript which sends the
328 form through only once. This is to stop users hitting the send button
329 multiple times when they are impatient and thus having the form sent
330 multiple times::
331
332 <form method="POST" onSubmit="return submit_once()"
333 enctype="multipart/form-data">
334
335 Next we define some code which sets up the minimum list of fields that
336 we require the user to enter. There will be only one field - "name" - so
337 they better put something in it, otherwise the whole form is pointless::
338
339 <input type="hidden" name="@required" value="name">
340
341 To get everything to line up properly we will put everything in a table,
342 and put a nice big header on it so the user has an idea what is
343 happening::
344
345 <table class="form">
346 <tr><th class="header" colspan="2">Category</th></tr>
347
348 Next, we need the field into which the user is going to enter the new
349 category. The ``context.name.field(size=60)`` bit tells Roundup to
350 generate a normal HTML field of size 60, and the contents of that field
351 will be the "name" variable of the current context (namely "category").
352 The upshot of this is that when the user types something in
353 to the form, a new category will be created with that name::
354
355 <tr>
356 <th>Name</th>
357 <td tal:content="structure python:context.name.field(size=60)">
358 name</td>
359 </tr>
360
361 Then a submit button so that the user can submit the new category::
362
363 <tr>
364 <td>&nbsp;</td>
365 <td colspan="3" tal:content="structure context/submit">
366 submit button will go here
367 </td>
368 </tr>
369
370 The ``context/submit`` bit generates the submit button but also
371 generates the @action and @csrf hidden fields. The @action field is
372 used to tell Roundup how to process the form. The @csrf field provides
373 a unique single use token to defend against CSRF attacks. (More about
374 anti-csrf measures can be found in ``upgrading.txt``.)
375
376 Finally we finish off the tags we used at the start to do the METAL
377 stuff::
378
379 </td>
380 </tal:block>
381
382 So putting it all together, and closing the table and form we get::
383
384 <!-- category.item -->
385 <tal:block metal:use-macro="templates/page/macros/icing">
386 <title metal:fill-slot="head_title">Category editing</title>
387 <td class="page-header-top" metal:fill-slot="body_title">
388 <h2>Category editing</h2>
389 </td>
390 <td class="content" metal:fill-slot="content">
391 <form method="POST" onSubmit="return submit_once()"
392 enctype="multipart/form-data">
393
394 <table class="form">
395 <tr><th class="header" colspan="2">Category</th></tr>
396
397 <tr>
398 <th>Name</th>
399 <td tal:content="structure python:context.name.field(size=60)">
400 name</td>
401 </tr>
402
403 <tr>
404 <td>
405 &nbsp;
406 <input type="hidden" name="@required" value="name">
407 </td>
408 <td colspan="3" tal:content="structure context/submit">
409 submit button will go here
410 </td>
411 </tr>
412 </table>
413 </form>
414 </td>
415 </tal:block>
416
417 This is quite a lot to just ask the user one simple question, but there
418 is a lot of setup for basically one line (the form line) to do its work.
419 To add another field to "category" would involve one more line (well,
420 maybe a few extra to get the formatting correct).
421
422
423 Adding the category to the issue
424 ::::::::::::::::::::::::::::::::
425
426 We now have the ability to create issues to our heart's content, but
427 that is pointless unless we can assign categories to issues. Just like
428 the ``html/category.item.html`` file was used to define how to add a new
429 category, the ``html/issue.item.html`` is used to define how a new issue
430 is created.
431
432 Just like ``category.issue.html``, this file defines a form which has a
433 table to lay things out. It doesn't matter where in the table we add new
434 stuff, it is entirely up to your sense of aesthetics::
435
436 <th>Category</th>
437 <td>
438 <span tal:replace="structure context/category/field" />
439 <span tal:replace="structure python:db.category.classhelp('name',
440 property='category', width='200')" />
441 </td>
442
443 First, we define a nice header so that the user knows what the next
444 section is, then the middle line does what we are most interested in.
445 This ``context/category/field`` gets replaced by a field which contains
446 the category in the current context (the current context being the new
447 issue).
448
449 The classhelp lines generate a link (labelled "list") to a popup window
450 which contains the list of currently known categories.
451
452
453 Searching on categories
454 :::::::::::::::::::::::
455
456 Now we can add categories, and create issues with categories. The next
457 obvious thing that we would like to be able to do, would be to search
458 for issues based on their category, so that, for example, anyone working
459 on the web server could look at all issues in the category "Web".
460
461 If you look for "Search Issues" in the ``html/page.html`` file, you will
462 find that it looks something like
463 ``<a href="issue?@template=search">Search Issues</a>``. This shows us
464 that when you click on "Search Issues" it will be looking for a
465 ``issue.search.html`` file to display. So that is the file that we will
466 change.
467
468 If you look at this file it should begin to seem familiar, although it
469 does use some new macros. You can add the new category search code anywhere you
470 like within that form::
471
472 <tr tal:define="name string:category;
473 db_klass string:category;
474 db_content string:name;">
475 <th>Priority:</th>
476 <td metal:use-macro="search_select"></td>
477 <td metal:use-macro="column_input"></td>
478 <td metal:use-macro="sort_input"></td>
479 <td metal:use-macro="group_input"></td>
480 </tr>
481
482 The definitions in the ``<tr>`` opening tag are used by the macros:
483
484 - ``search_select`` expands to a drop-down box with all categories using
485 ``db_klass`` and ``db_content``.
486 - ``column_input`` expands to a checkbox for selecting what columns
487 should be displayed.
488 - ``sort_input`` expands to a radio button for selecting what property
489 should be sorted on.
490 - ``group_input`` expands to a radio button for selecting what property
491 should be grouped on.
492
493 The category search code above would expand to the following::
494
495 <tr>
496 <th>Category:</th>
497 <td>
498 <select name="category">
499 <option value="">don't care</option>
500 <option value="">------------</option>
501 <option value="1">scipy</option>
502 <option value="2">chaco</option>
503 <option value="3">weave</option>
504 </select>
505 </td>
506 <td><input type="checkbox" name=":columns" value="category"></td>
507 <td><input type="radio" name=":sort0" value="category"></td>
508 <td><input type="radio" name=":group0" value="category"></td>
509 </tr>
510
511 Adding category to the default view
512 :::::::::::::::::::::::::::::::::::
513
514 We can now add categories, add issues with categories, and search for
515 issues based on categories. This is everything that we need to do;
516 however, there is some more icing that we would like. I think the
517 category of an issue is important enough that it should be displayed by
518 default when listing all the issues.
519
520 Unfortunately, this is a bit less obvious than the previous steps. The
521 code defining how the issues look is in ``html/issue.index.html``. This
522 is a large table with a form down at the bottom for redisplaying and so
523 forth.
524
525 Firstly we need to add an appropriate header to the start of the table::
526
527 <th tal:condition="request/show/category">Category</th>
528
529 The *condition* part of this statement is to avoid displaying the
530 Category column if the user has selected not to see it.
531
532 The rest of the table is a loop which will go through every issue that
533 matches the display criteria. The loop variable is "i" - which means
534 that every issue gets assigned to "i" in turn.
535
536 The new part of code to display the category will look like this::
537
538 <td tal:condition="request/show/category"
539 tal:content="i/category"></td>
540
541 The condition is the same as above: only display the condition when the
542 user hasn't asked for it to be hidden. The next part is to set the
543 content of the cell to be the category part of "i" - the current issue.
544
545 Finally we have to edit ``html/page.html`` again. This time, we need to
546 tell it that when the user clicks on "Unassigned Issues" or "All Issues",
547 the category column should be included in the resulting list. If you
548 scroll down the page file, you can see the links with lots of options.
549 The option that we are interested in is the ``:columns=`` one which
550 tells Roundup which fields of the issue to display. Simply add
551 "category" to that list and it all should work.
552
553 Adding a time log to your issues
554 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
555
556 We want to log the dates and amount of time spent working on issues, and
557 be able to give a summary of the total time spent on a particular issue.
558
559 1. Add a new class to your tracker ``schema.py``::
560
561 # storage for time logging
562 timelog = Class(db, "timelog", period=Interval())
563
564 Note that we automatically get the date of the time log entry
565 creation through the standard property "creation".
566
567 You will need to grant "Creation" permission to the users who are
568 allowed to add timelog entries. You may do this with::
569
570 db.security.addPermissionToRole('User', 'Create', 'timelog')
571 db.security.addPermissionToRole('User', 'View', 'timelog')
572
573 If users are also able to *edit* timelog entries, then also include::
574
575 db.security.addPermissionToRole('User', 'Edit', 'timelog')
576
577 .. index:: schema; example changes
578
579 2. Link to the new class from your issue class (again, in
580 ``schema.py``)::
581
582 issue = IssueClass(db, "issue",
583 assignedto=Link("user"), keyword=Multilink("keyword"),
584 priority=Link("priority"), status=Link("status"),
585 times=Multilink("timelog"))
586
587 the "times" property is the new link to the "timelog" class.
588
589 3. We'll need to let people add in times to the issue, so in the web
590 interface we'll have a new entry field. This is a special field
591 because unlike the other fields in the ``issue.item`` template, it
592 affects a different item (a timelog item) and not the template's
593 item (an issue). We have a special syntax for form fields that affect
594 items other than the template default item (see the cgi
595 documentation on `special form variables
596 <reference.html#special-form-variables>`_). In particular, we add a
597 field to capture a new timelog item's period::
598
599 <tr>
600 <th>Time Log</th>
601 <td colspan=3><input type="text" name="timelog-1@period" />
602 (enter as '3y 1m 4d 2:40:02' or parts thereof)
603 </td>
604 </tr>
605
606 and another hidden field that links that new timelog item (new
607 because it's marked as having id "-1") to the issue item. It looks
608 like this::
609
610 <input type="hidden" name="@link@times" value="timelog-1" />
611
612 On submission, the "-1" timelog item will be created and assigned a
613 real item id. The "times" property of the issue will have the new id
614 added to it.
615
616 The full entry will now look like this::
617
618 <tr>
619 <th>Time Log</th>
620 <td colspan=3><input type="text" name="timelog-1@period" />
621 (enter as '3y 1m 4d 2:40:02' or parts thereof)
622 <input type="hidden" name="@link@times" value="timelog-1" />
623 </td>
624 </tr>
625
626 .. _adding-a-time-log-to-your-issues-4:
627
628 4. We want to display a total of the timelog times that have been
629 accumulated for an issue. To do this, we'll need to actually write
630 some Python code, since it's beyond the scope of PageTemplates to
631 perform such calculations. We do this by adding a module ``timespent.py``
632 to the ``extensions`` directory in our tracker. The contents of this
633 file is as follows::
634
635 from roundup import date
636
637 def totalTimeSpent(times):
638 ''' Call me with a list of timelog items (which have an
639 Interval "period" property)
640 '''
641 total = date.Interval('0d')
642 for time in times:
643 total += time.period._value
644 return total
645
646 def init(instance):
647 instance.registerUtil('totalTimeSpent', totalTimeSpent)
648
649 We will now be able to access the ``totalTimeSpent`` function via the
650 ``utils`` variable in our templates, as shown in the next step.
651
652 5. Display the timelog for an issue::
653
654 <table class="otherinfo" tal:condition="context/times">
655 <tr><th colspan="3" class="header">Time Log
656 <tal:block
657 tal:replace="python:utils.totalTimeSpent(context.times)" />
658 </th></tr>
659 <tr><th>Date</th><th>Period</th><th>Logged By</th></tr>
660 <tr tal:repeat="time context/times">
661 <td tal:content="time/creation"></td>
662 <td tal:content="time/period"></td>
663 <td tal:content="time/creator"></td>
664 </tr>
665 </table>
666
667 I put this just above the Messages log in my issue display. Note our
668 use of the ``totalTimeSpent`` method which will total up the times
669 for the issue and return a new Interval. That will be automatically
670 displayed in the template as text like "+ 1y 2:40" (1 year, 2 hours
671 and 40 minutes).
672
673 6. If you're using a persistent web server - ``roundup-server`` or
674 ``mod_wsgi`` for example - then you'll need to restart that to pick up
675 the code changes. When that's done, you'll be able to use the new
676 time logging interface.
677
678 An extension of this modification attaches the timelog entries to any
679 change message entered at the time of the timelog entry:
680
681 A. Add a link to the timelog to the msg class in ``schema.py``:
682
683 msg = FileClass(db, "msg",
684 author=Link("user", do_journal='no'),
685 recipients=Multilink("user", do_journal='no'),
686 date=Date(),
687 summary=String(),
688 files=Multilink("file"),
689 messageid=String(),
690 inreplyto=String(),
691 times=Multilink("timelog"))
692
693 B. Add a new hidden field that links that new timelog item (new
694 because it's marked as having id "-1") to the new message.
695 The link is placed in ``issue.item.html`` in the same section that
696 handles the timelog entry.
697
698 It looks like this after this addition::
699
700 <tr>
701 <th>Time Log</th>
702 <td colspan=3><input type="text" name="timelog-1@period" />
703 (enter as '3y 1m 4d 2:40:02' or parts thereof)
704 <input type="hidden" name="@link@times" value="timelog-1" />
705 <input type="hidden" name="msg-1@link@times" value="timelog-1" />
706 </td>
707 </tr>
122 708
123 .. index:: config.ini; sections main 709 The "times" property of the message will have the new id added to it.
124 710
125 Section **main** 711 C. Add the timelog listing from step 5. to the ``msg.item.html`` template
126 database -- ``db`` 712 so that the timelog entry appears on the message view page. Note that
127 Database directory path. The path may be either absolute or relative 713 the call to totalTimeSpent is not used here since there will only be one
128 to the directory containig this config file. 714 single timelog entry for each message.
129 715
130 templates -- ``html`` 716 I placed it after the Date entry like this::
131 Path to the HTML templates directory. The path may be either absolute 717
132 or relative to the directory containing this config file. 718 <tr>
133 719 <th i18n:translate="">Date:</th>
134 static_files -- default *blank* 720 <td tal:content="context/date"></td>
135 A list of space separated directory paths (or a single directory). 721 </tr>
136 These directories hold additional static files available via Web UI. 722 </table>
137 These directories may contain sitewide images, CSS stylesheets etc. If 723
138 a '-' is included, the list processing ends and the TEMPLATES 724 <table class="otherinfo" tal:condition="context/times">
139 directory is not searched after the specified directories. If this 725 <tr><th colspan="3" class="header">Time Log</th></tr>
140 option is not set, all static files are taken from the TEMPLATES 726 <tr><th>Date</th><th>Period</th><th>Logged By</th></tr>
141 directory. 727 <tr tal:repeat="time context/times">
142 728 <td tal:content="time/creation"></td>
143 admin_email -- ``roundup-admin`` 729 <td tal:content="time/period"></td>
144 Email address that roundup will complain to if it runs into trouble. If 730 <td tal:content="time/creator"></td>
145 the email address doesn't contain an ``@`` part, the MAIL_DOMAIN defined 731 </tr>
146 below is used. 732 </table>
147 733
148 dispatcher_email -- ``roundup-admin`` 734 <table class="messages">
149 The 'dispatcher' is a role that can get notified of new items to the 735
150 database. It is used by the ERROR_MESSAGES_TO config setting. If the 736
151 email address doesn't contain an ``@`` part, the MAIL_DOMAIN defined 737 Tracking different types of issues
152 below is used. 738 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
153 739
154 email_from_tag -- default *blank* 740 Sometimes you will want to track different types of issues - developer,
155 Additional text to include in the "name" part of the From: address used 741 customer support, systems, sales leads, etc. A single Roundup tracker is
156 in nosy messages. If the sending user is "Foo Bar", the From: line 742 able to support multiple types of issues. This example demonstrates adding
157 is usually: ``"Foo Bar" <issue_tracker@tracker.example>`` 743 a system support issue class to a tracker.
158 the EMAIL_FROM_TAG goes inside the "Foo Bar" quotes like so: 744
159 ``"Foo Bar EMAIL_FROM_TAG" <issue_tracker@tracker.example>`` 745 1. Figure out what information you're going to want to capture. OK, so
160 746 this is obvious, but sometimes it's better to actually sit down for a
161 new_web_user_roles -- ``User`` 747 while and think about the schema you're going to implement.
162 Roles that a user gets when they register with Web User Interface. 748
163 This is a comma-separated list of role names (e.g. ``Admin,User``). 749 2. Add the new issue class to your tracker's ``schema.py``. Just after the
164 750 "issue" class definition, add::
165 new_email_user_roles -- ``User`` 751
166 Roles that a user gets when they register with Email Gateway. 752 # list our systems
167 This is a comma-separated string of role names (e.g. ``Admin,User``). 753 system = Class(db, "system", name=String(), order=Number())
168 754 system.setkey("name")
169 error_messages_to -- ``user`` 755
170 Send error message emails to the ``dispatcher``, ``user``, or ``both``? 756 # store issues related to those systems
171 The dispatcher is configured using the DISPATCHER_EMAIL setting. 757 support = IssueClass(db, "support",
172 Allowed values: ``dispatcher``, ``user``, or ``both`` 758 assignedto=Link("user"), keyword=Multilink("keyword"),
173 759 status=Link("status"), deadline=Date(),
174 html_version -- ``html4`` 760 affects=Multilink("system"))
175 HTML version to generate. The templates are ``html4`` by default. 761
176 If you wish to make them xhtml, then you'll need to change this 762 3. Copy the existing ``issue.*`` (item, search and index) templates in the
177 var to ``xhtml`` too so all auto-generated HTML is compliant. 763 tracker's ``html`` to ``support.*``. Edit them so they use the properties
178 Allowed values: ``html4``, ``xhtml`` 764 defined in the ``support`` class. Be sure to check for hidden form
179 765 variables like "required" to make sure they have the correct set of
180 timezone -- ``0`` 766 required properties.
181 Numeric timezone offset used when users do not choose their own 767
182 in their settings. 768 4. Edit the modules in the ``detectors``, adding lines to their ``init``
183 769 functions where appropriate. Look for ``audit`` and ``react`` registrations
184 instant_registration -- ``yes`` 770 on the ``issue`` class, and duplicate them for ``support``.
185 Register new users instantly, or require confirmation via 771
186 email? 772 5. Create a new sidebar box for the new support class. Duplicate the
187 Allowed values: ``yes``, ``no`` 773 existing issues one, changing the ``issue`` class name to ``support``.
188 774
189 email_registration_confirmation -- ``yes`` 775 6. Re-start your tracker and start using the new ``support`` class.
190 Offer registration confirmation by email or only through the web? 776
191 Allowed values: ``yes``, ``no`` 777
192 778 Optionally, you might want to restrict the users able to access this new
193 indexer_stopwords -- default *blank* 779 class to just the users with a new "SysAdmin" Role. To do this, we add
194 Additional stop-words for the full-text indexer specific to 780 some security declarations::
195 your tracker. See the indexer source for the default list of 781
196 stop-words (e.g. ``A,AND,ARE,AS,AT,BE,BUT,BY, ...``). 782 db.security.addPermissionToRole('SysAdmin', 'View', 'support')
197 783 db.security.addPermissionToRole('SysAdmin', 'Create', 'support')
198 umask -- ``02`` 784 db.security.addPermissionToRole('SysAdmin', 'Edit', 'support')
199 Defines the file creation mode mask. 785
200 786 You would then (as an "admin" user) edit the details of the appropriate
201 csv_field_size -- ``131072`` 787 users, and add "SysAdmin" to their Roles list.
202 Maximum size of a csv-field during import. Roundup's export 788
203 format is a csv (comma separated values) variant. The csv 789 Alternatively, you might want to change the Edit/View permissions granted
204 reader has a limit on the size of individual fields 790 for the ``issue`` class so that it's only available to users with the "System"
205 starting with python 2.5. Set this to a higher value if you 791 or "Developer" Role, and then the new class you're adding is available to
206 get the error 'Error: field larger than field limit' during 792 all with the "User" Role.
207 import. 793
208 794
209 .. index:: config.ini; sections tracker 795 .. _external-authentication:
210 796
211 Section **tracker** 797 Using External User Databases
212 name -- ``Roundup issue tracker`` 798 -----------------------------
213 A descriptive name for your Roundup instance. 799
214 800 Using an external password validation source
215 web -- ``http://host.example/demo/`` 801 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
216 The web address that the tracker is viewable at. 802
217 This will be included in information sent to users of the tracker. 803 .. note:: You will need to either have an "admin" user in your external
218 The URL MUST include the cgi-bin part or anything else 804 password source *or* have one of your regular users have
219 that is required to get to the home page of the tracker. 805 the Admin Role assigned. If you need to assign the Role *after*
220 You MUST include a trailing '/' in the URL. 806 making the changes below, you may use the ``roundup-admin``
221 807 program to edit a user's details.
222 email -- ``issue_tracker`` 808
223 Email address that mail to Roundup should go to. 809 We have a centrally-managed password changing system for our users. This
224 810 results in a UN*X passwd-style file that we use for verification of
225 language -- default *blank* 811 users. Entries in the file consist of ``name:password`` where the
226 Default locale name for this tracker. If this option is not set, the 812 password is encrypted using the standard UN*X ``crypt()`` function (see
227 language is determined by the environment variable LANGUAGE, LC_ALL, 813 the ``crypt`` module in your Python distribution). An example entry
228 LC_MESSAGES, or LANG, in that order of preference. 814 would be::
229 815
230 .. index:: config.ini; sections web 816 admin:aamrgyQfDFSHw
231 817
232 Section **web** 818 Each user of Roundup must still have their information stored in the Roundup
233 allow_html_file -- ``no`` 819 database - we just use the passwd file to check their password. To do this, we
234 Setting this option enables Roundup to serve uploaded HTML 820 need to override the standard ``verifyPassword`` method defined in
235 file content *as HTML*. This is a potential security risk 821 ``roundup.cgi.actions.LoginAction`` and register the new class. The
236 and is therefore disabled by default. Set to 'yes' if you 822 following is added as ``externalpassword.py`` in the tracker ``extensions``
237 trust *all* users uploading content to your tracker. 823 directory::
238 824
239 http_auth -- ``yes`` 825 import os, crypt
240 Whether to use HTTP Basic Authentication, if present. 826 from roundup.cgi.actions import LoginAction
241 Roundup will use either the REMOTE_USER or HTTP_AUTHORIZATION 827
242 variables supplied by your web server (in that order). 828 class ExternalPasswordLoginAction(LoginAction):
243 Set this option to 'no' if you do not wish to use HTTP Basic 829 def verifyPassword(self, userid, password):
244 Authentication in your web interface. 830 '''Look through the file, line by line, looking for a
245 831 name that matches.
246 use_browser_language -- ``yes`` 832 '''
247 Whether to use HTTP Accept-Language, if present. 833 # get the user's username
248 Browsers send a language-region preference list. 834 username = self.db.user.get(userid, 'username')
249 It's usually set in the client's browser or in their 835
250 Operating System. 836 # the passwords are stored in the "passwd.txt" file in the
251 Set this option to 'no' if you want to ignore it. 837 # tracker home
252 838 file = os.path.join(self.db.config.TRACKER_HOME, 'passwd.txt')
253 debug -- ``no`` 839
254 Setting this option makes Roundup display error tracebacks 840 # see if we can find a match
255 in the user's browser rather than emailing them to the 841 for ent in [line.strip().split(':') for line in
256 tracker admin."), 842 open(file).readlines()]:
257 843 if ent[0] == username:
258 .. index:: config.ini; sections rdbms 844 return crypt.crypt(password, ent[1][:2]) == ent[1]
259 single: config.ini; database settings 845
260 846 # user doesn't exist in the file
261 Section **rdbms** 847 return 0
262 Settings in this section are used to set the backend and configure 848
263 addition settings needed by RDBMs like SQLite, Postgresql and 849 def init(instance):
264 MySQL backends. 850 instance.registerAction('login', ExternalPasswordLoginAction)
265 851
266 .. index:: 852 You should also remove the redundant password fields from the ``user.item``
267 single: postgres; select backend in config.ini 853 template.
268 single: mysql; select backend in config.ini 854
269 single: sqlite; select backend in config.ini 855
270 single: anydbm; select backend in config.ini 856 Using a UN*X passwd file as the user database
271 see: database; postgres 857 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
272 see: database; mysql 858
273 see: database; sqlite 859 On some systems the primary store of users is the UN*X passwd file. It
274 see: database; anydbm 860 holds information on users such as their username, real name, password
275 861 and primary user group.
276 backend -- set to value by init 862
277 The database backend such as anydbm, sqlite, mysql or postgres. 863 Roundup can use this store as its primary source of user information,
278 864 but it needs additional information too - email address(es), Roundup
279 name -- ``roundup`` 865 Roles, vacation flags, Roundup hyperdb item ids, etc. Also, "retired"
280 Name of the database to use. 866 users must still exist in the user database, unlike some passwd files in
281 867 which the users are removed when they no longer have access to a system.
282 host -- ``localhost`` 868
283 Database server host. 869 To make use of the passwd file, we therefore synchronise between the two
284 870 user stores. We also use the passwd file to validate the user logins, as
285 port -- default *blank* 871 described in the previous example, `using an external password
286 TCP port number of the database server. Postgresql usually resides on 872 validation source`_. We keep the user lists in sync using a fairly
287 port 5432 (if any), for MySQL default port number is 3306. Leave this 873 simple script that runs once a day, or several times an hour if more
288 option empty to use backend default. 874 immediate access is needed. In short, it:
289 875
290 user -- ``roundup`` 876 1. parses the passwd file, finding usernames, passwords and real names,
291 Database user name that Roundup should use. 877 2. compares that list to the current Roundup user list:
292 878
293 password -- ``roundup`` 879 a. entries no longer in the passwd file are *retired*
294 Database user password. 880 b. entries with mismatching real names are *updated*
295 881 c. entries only exist in the passwd file are *created*
296 read_default_file -- ``~/.my.cnf`` 882
297 Name of the MySQL defaults file. Only used in MySQL connections. 883 3. send an email to administrators to let them know what's been done.
298 884
299 read_default_group -- ``roundup`` 885 The retiring and updating are simple operations, requiring only a call
300 Name of the group to use in the MySQL defaults file. Only used in 886 to ``retire()`` or ``set()``. The creation operation requires more
301 MySQL connections. 887 information though - the user's email address and their Roundup Roles.
302 888 We're going to assume that the user's email address is the same as their
303 .. index:: 889 login name, so we just append the domain name to that. The Roles are
304 single: sqlite; lock timeout 890 determined using the passwd group identifier - mapping their UN*X group
305 891 to an appropriate set of Roles.
306 sqlite_timeout -- ``30`` 892
307 Number of seconds to wait when the SQLite database is locked. 893 The script to perform all this, broken up into its main components, is
308 Used only for SQLite. 894 as follows. Firstly, we import the necessary modules and open the
309 895 tracker we're to work on::
310 cache_size -- `100` 896
311 Size of the node cache (in elements) used to keep most recently used 897 import sys, os, smtplib
312 data in memory. 898 from roundup import instance, date
313 899
314 .. index:: config.ini; sections logging 900 # open the tracker
315 see: logging; config.ini, sections logging 901 tracker_home = sys.argv[1]
316 902 tracker = instance.open(tracker_home)
317 Section **logging** 903
318 config -- default *blank* 904 Next we read in the *passwd* file from the tracker home::
319 Path to configuration file for standard Python logging module. If this 905
320 option is set, logging configuration is loaded from specified file; 906 # read in the users from the "passwd.txt" file
321 options 'filename' and 'level' in this section are ignored. The path may 907 file = os.path.join(tracker_home, 'passwd.txt')
322 be either absolute or relative to the directory containig this config file. 908 users = [x.strip().split(':') for x in open(file).readlines()]
323 909
324 filename -- default *blank* 910 Handle special users (those to ignore in the file, and those who don't
325 Log file name for minimal logging facility built into Roundup. If no file 911 appear in the file)::
326 name specified, log messages are written on stderr. If above 'config' 912
327 option is set, this option has no effect. The path may be either absolute 913 # users to not keep ever, pre-load with the users I know aren't
328 or relative to the directory containig this config file. 914 # "real" users
329 915 ignore = ['ekmmon', 'bfast', 'csrmail']
330 level -- ``ERROR`` 916
331 Minimal severity level of messages written to log file. If above 'config' 917 # users to keep - pre-load with the roundup-specific users
332 option is set, this option has no effect. 918 keep = ['comment_pool', 'network_pool', 'admin', 'dev-team',
333 Allowed values: ``DEBUG``, ``INFO``, ``WARNING``, ``ERROR`` 919 'cs_pool', 'anonymous', 'system_pool', 'automated']
334 920
335 .. index:: config.ini; sections mail 921 Now we map the UN*X group numbers to the Roles that users should have::
336 922
337 Section **mail** 923 roles = {
338 Outgoing email options. Used for nosy messages, password reset and 924 '501': 'User,Tech', # tech
339 registration approval requests. 925 '502': 'User', # finance
340 926 '503': 'User,CSR', # customer service reps
341 domain -- ``localhost`` 927 '504': 'User', # sales
342 Domain name used for email addresses. 928 '505': 'User', # marketing
343 929 }
344 host -- default *blank* 930
345 SMTP mail host that Roundup will use to send mail 931 Now we do all the work. Note that the body of the script (where we have
346 932 the tracker database open) is wrapped in a ``try`` / ``finally`` clause,
347 username -- default *blank* 933 so that we always close the database cleanly when we're finished. So, we
348 SMTP login name. Set this if your mail host requires authenticated access. 934 now do all the work::
349 If username is not empty, password (below) MUST be set! 935
350 936 # open the database
351 password -- default *blank* 937 db = tracker.open('admin')
352 SMTP login password. 938 try:
353 Set this if your mail host requires authenticated access. 939 # store away messages to send to the tracker admins
354 940 msg = []
355 port -- default *25* 941
356 SMTP port on mail host. 942 # loop over the users list read in from the passwd file
357 Set this if your mail host runs on a different port. 943 for user,passw,uid,gid,real,home,shell in users:
358 944 if user in ignore:
359 local_hostname -- default *blank* 945 # this user shouldn't appear in our tracker
360 The fully qualified domain name (FQDN) to use during SMTP sessions. If left 946 continue
361 blank, the underlying SMTP library will attempt to detect your FQDN. If your 947 keep.append(user)
362 mail host requires something specific, specify the FQDN to use. 948 try:
363 949 # see if the user exists in the tracker
364 tls -- ``no`` 950 uid = db.user.lookup(user)
365 If your SMTP mail host provides or requires TLS (Transport Layer Security) 951
366 then you may set this option to 'yes'. 952 # yes, they do - now check the real name for correctness
367 Allowed values: ``yes``, ``no`` 953 if real != db.user.get(uid, 'realname'):
368 954 db.user.set(uid, realname=real)
369 tls_keyfile -- default *blank* 955 msg.append('FIX %s - %s'%(user, real))
370 If TLS is used, you may set this option to the name of a PEM formatted 956 except KeyError:
371 file that contains your private key. The path may be either absolute or 957 # nope, the user doesn't exist
372 relative to the directory containig this config file. 958 db.user.create(username=user, realname=real,
373 959 address='%s@ekit-inc.com'%user, roles=roles[gid])
374 tls_certfile -- default *blank* 960 msg.append('ADD %s - %s (%s)'%(user, real, roles[gid]))
375 If TLS is used, you may set this option to the name of a PEM formatted 961
376 certificate chain file. The path may be either absolute or relative 962 # now check that all the users in the tracker are also in our
377 to the directory containig this config file. 963 # "keep" list - retire those who aren't
378 964 for uid in db.user.list():
379 charset -- utf-8 965 user = db.user.get(uid, 'username')
380 Character set to encode email headers with. We use utf-8 by default, as 966 if user not in keep:
381 it's the most flexible. Some mail readers (eg. Eudora) can't cope with 967 db.user.retire(uid)
382 that, so you might need to specify a more limited character set 968 msg.append('RET %s'%user)
383 (eg. iso-8859-1). 969
384 970 # if we did work, then send email to the tracker admins
385 debug -- default *blank* 971 if msg:
386 Setting this option makes Roundup to write all outgoing email messages 972 # create the email
387 to this file *instead* of sending them. This option has the same effect 973 msg = '''Subject: %s user database maintenance
388 as environment variable SENDMAILDEBUG. Environment variable takes 974
389 precedence. The path may be either absolute or relative to the directory 975 %s
390 containig this config file. 976 '''%(db.config.TRACKER_NAME, '\n'.join(msg))
391 977
392 add_authorinfo -- ``yes`` 978 # send the email
393 Add a line with author information at top of all messages send by 979 smtp = smtplib.SMTP(db.config.MAILHOST)
394 Roundup. 980 addr = db.config.ADMIN_EMAIL
395 981 smtp.sendmail(addr, addr, msg)
396 add_authoremail -- ``yes`` 982
397 Add the mail address of the author to the author information at the 983 # now we're done - commit the changes
398 top of all messages. If this is false but add_authorinfo is true, 984 db.commit()
399 only the name of the actor is added which protects the mail address 985 finally:
400 of the actor from being exposed at mail archives, etc. 986 # always close the database cleanly
401 987 db.close()
402 .. index:: config.ini; sections mailgw 988
403 single: mailgw; config 989 And that's it!
404 see: mail gateway; mailgw 990
405 991
406 Section **mailgw** 992 Using an LDAP database for user information
407 Roundup Mail Gateway options 993 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
408 994
409 keep_quoted_text -- ``yes`` 995 A script that reads users from an LDAP store using
410 Keep email citations when accepting messages. Setting this to ``no`` strips 996 https://pypi.org/project/python-ldap/ and then compares the list to the users in the
411 out "quoted" text from the message. Signatures are also stripped. 997 Roundup user database would be pretty easy to write. You'd then have it run
412 Allowed values: ``yes``, ``no`` 998 once an hour / day (or on demand if you can work that into your LDAP store
413 999 workflow). See the example `Using a UN*X passwd file as the user database`_
414 leave_body_unchanged -- ``no`` 1000 for more information about doing this.
415 Preserve the email body as is - that is, keep the citations *and* 1001
416 signatures. 1002 To authenticate off the LDAP store (rather than using the passwords in the
417 Allowed values: ``yes``, ``no`` 1003 Roundup user database) you'd use the same python-ldap module inside an
418 1004 extension to the cgi interface. You'd do this by overriding the method called
419 default_class -- ``issue`` 1005 ``verifyPassword`` on the ``LoginAction`` class in your tracker's
420 Default class to use in the mailgw if one isn't supplied in email subjects. 1006 ``extensions`` directory (see `using an external password validation
421 To disable, leave the value blank. 1007 source`_). The method is implemented by default as::
422 1008
423 language -- default *blank* 1009 def verifyPassword(self, userid, password):
424 Default locale name for the tracker mail gateway. If this option is 1010 ''' Verify the password that the user has supplied
425 not set, mail gateway will use the language of the tracker instance. 1011 '''
426 1012 stored = self.db.user.get(self.userid, 'password')
427 subject_prefix_parsing -- ``strict`` 1013 if password == stored:
428 Controls the parsing of the [prefix] on subject lines in incoming emails. 1014 return 1
429 ``strict`` will return an error to the sender if the [prefix] is not 1015 if not password and not stored:
430 recognised. ``loose`` will attempt to parse the [prefix] but just 1016 return 1
431 pass it through as part of the issue title if not recognised. ``none`` 1017 return 0
432 will always pass any [prefix] through as part of the issue title. 1018
433 1019 So you could reimplement this as something like::
434 subject_suffix_parsing -- ``strict`` 1020
435 Controls the parsing of the [suffix] on subject lines in incoming emails. 1021 def verifyPassword(self, userid, password):
436 ``strict`` will return an error to the sender if the [suffix] is not 1022 ''' Verify the password that the user has supplied
437 recognised. ``loose`` will attempt to parse the [suffix] but just 1023 '''
438 pass it through as part of the issue title if not recognised. ``none`` 1024 # look up some unique LDAP information about the user
439 will always pass any [suffix] through as part of the issue title. 1025 username = self.db.user.get(self.userid, 'username')
440 1026 # now verify the password supplied against the LDAP store
441 subject_suffix_delimiters -- ``[]`` 1027
442 Defines the brackets used for delimiting the commands suffix in a subject 1028
443 line. 1029 Changes to Tracker Behaviour
444 1030 ----------------------------
445 subject_content_match -- ``always`` 1031
446 Controls matching of the incoming email subject line against issue titles 1032 .. index:: single: auditors; how to register (example)
447 in the case where there is no designator [prefix]. ``never`` turns off 1033 single: reactors; how to register (example)
448 matching. ``creation + interval`` or ``activity + interval`` will match 1034
449 an issue for the interval after the issue's creation or last activity. 1035 Preventing SPAM
450 The interval is a standard Roundup interval. 1036 ~~~~~~~~~~~~~~~
451 1037
452 subject_updates_title -- ``yes`` 1038 The following detector code may be installed in your tracker's
453 Update issue title if incoming subject of email is different. 1039 ``detectors`` directory. It will block any messages being created that
454 Setting this to ``no`` will ignore the title part of 1040 have HTML attachments (a very common vector for spam and phishing)
455 the subject of incoming email messages. 1041 and any messages that have more than 2 HTTP URLs in them. Just copy
456 1042 the following into ``detectors/anti_spam.py`` in your tracker::
457 refwd_re -- ``(\s*\W?\s*(fw|fwd|re|aw|sv|ang)\W)+`` 1043
458 Regular expression matching a single reply or forward prefix 1044 from roundup.exceptions import Reject
459 prepended by the mailer. This is explicitly stripped from the 1045
460 subject during parsing. Value is Python Regular Expression 1046 def reject_html(db, cl, nodeid, newvalues):
461 (UTF8-encoded). 1047 if newvalues['type'] == 'text/html':
462 1048 raise Reject('not allowed')
463 origmsg_re -- `` ^[>|\s]*-----\s?Original Message\s?-----$`` 1049
464 Regular expression matching start of an original message if quoted 1050 def reject_manylinks(db, cl, nodeid, newvalues):
465 in the body. Value is Python Regular Expression (UTF8-encoded). 1051 content = newvalues['content']
466 1052 if content.count('http://') > 2:
467 sign_re -- ``^[>|\s]*-- ?$`` 1053 raise Reject('not allowed')
468 Regular expression matching the start of a signature in the message 1054
469 body. Value is Python Regular Expression (UTF8-encoded). 1055 def init(db):
470 1056 db.file.audit('create', reject_html)
471 eol_re -- ``[\r\n]+`` 1057 db.msg.audit('create', reject_manylinks)
472 Regular expression matching end of line. Value is Python Regular 1058
473 Expression (UTF8-encoded). 1059 You may also wish to block image attachments if your tracker does not
474 1060 need that ability::
475 blankline_re -- ``[\r\n]+\s*[\r\n]+`` 1061
476 Regular expression matching a blank line. Value is Python Regular 1062 if newvalues['type'].startswith('image/'):
477 Expression (UTF8-encoded). 1063 raise Reject('not allowed')
478 1064
479 ignore_alternatives -- ``no`` 1065
480 When parsing incoming mails, Roundup uses the first 1066 Stop "nosy" messages going to people on vacation
481 text/plain part it finds. If this part is inside a 1067 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
482 multipart/alternative, and this option is set, all other 1068
483 parts of the multipart/alternative are ignored. The default 1069 When users go on vacation and set up vacation email bouncing, you'll
484 is to keep all parts and attach them to the issue. 1070 start to see a lot of messages come back through Roundup "Fred is on
485 1071 vacation". Not very useful, and relatively easy to stop.
486 .. index:: config.ini; sections php 1072
487 1073 1. add a "vacation" flag to your users::
488 Section **pgp** 1074
489 OpenPGP mail processing options 1075 user = Class(db, "user",
490 1076 username=String(), password=Password(),
491 enable -- ``no`` 1077 address=String(), realname=String(),
492 Enable PGP processing. Requires gpg. 1078 phone=String(), organisation=String(),
493 1079 alternate_addresses=String(),
494 roles -- default *blank* 1080 roles=String(), queries=Multilink("query"),
495 If specified, a comma-separated list of roles to perform PGP 1081 vacation=Boolean())
496 processing on. If not specified, it happens for all users. 1082
497 1083 2. So that users may edit the vacation flags, add something like the
498 homedir -- default *blank* 1084 following to your ``user.item`` template::
499 Location of PGP directory. Defaults to $HOME/.gnupg if not 1085
500 specified. 1086 <tr>
501 1087 <th>On Vacation</th>
502 1088 <td tal:content="structure context/vacation/field">vacation</td>
503 .. index:: config.ini; sections nosy 1089 </tr>
504 1090
505 Section **nosy** 1091 3. edit your detector ``nosyreactor.py`` so that the ``nosyreaction()``
506 Nosy messages sending 1092 consists of::
507 1093
508 messages_to_author -- ``no`` 1094 def nosyreaction(db, cl, nodeid, oldvalues):
509 Send nosy messages to the author of the message. 1095 users = db.user
510 If ``yes`` is used, then messages are sent to the author 1096 messages = db.msg
511 even if not on the nosy list, same for ``new`` (but only for new messages). 1097 # send a copy of all new messages to the nosy list
512 When set to ``nosy``, the nosy list controls sending messages to the author. 1098 for msgid in determineNewMessages(cl, nodeid, oldvalues):
513 Allowed values: ``yes``, ``no``, ``new``, ``nosy`` 1099 try:
514 1100 # figure the recipient ids
515 signature_position -- ``bottom`` 1101 sendto = []
516 Where to place the email signature. 1102 seen_message = {}
517 Allowed values: ``top``, ``bottom``, ``none`` 1103 recipients = messages.get(msgid, 'recipients')
518 1104 for recipid in messages.get(msgid, 'recipients'):
519 add_author -- ``new`` 1105 seen_message[recipid] = 1
520 Does the author of a message get placed on the nosy list automatically? 1106
521 If ``new`` is used, then the author will only be added when a message 1107 # figure the author's id, and indicate they've received
522 creates a new issue. If ``yes``, then the author will be added on 1108 # the message
523 followups too. If ``no``, they're never added to the nosy. 1109 authid = messages.get(msgid, 'author')
524 Allowed values: ``yes``, ``no``, ``new`` 1110
1111 # possibly send the message to the author, as long as
1112 # they aren't anonymous
1113 if (db.config.MESSAGES_TO_AUTHOR == 'yes' and
1114 users.get(authid, 'username') != 'anonymous'):
1115 sendto.append(authid)
1116 seen_message[authid] = 1
1117
1118 # now figure the nosy people who weren't recipients
1119 nosy = cl.get(nodeid, 'nosy')
1120 for nosyid in nosy:
1121 # Don't send nosy mail to the anonymous user (that
1122 # user shouldn't appear in the nosy list, but just
1123 # in case they do...)
1124 if users.get(nosyid, 'username') == 'anonymous':
1125 continue
1126 # make sure they haven't seen the message already
1127 if nosyid not in seen_message:
1128 # send it to them
1129 sendto.append(nosyid)
1130 recipients.append(nosyid)
1131
1132 # generate a change note
1133 if oldvalues:
1134 note = cl.generateChangeNote(nodeid, oldvalues)
1135 else:
1136 note = cl.generateCreateNote(nodeid)
1137
1138 # we have new recipients
1139 if sendto:
1140 # filter out the people on vacation
1141 sendto = [i for i in sendto
1142 if not users.get(i, 'vacation', 0)]
1143
1144 # map userids to addresses
1145 sendto = [users.get(i, 'address') for i in sendto]
1146
1147 # update the message's recipients list
1148 messages.set(msgid, recipients=recipients)
1149
1150 # send the message
1151 cl.send_message(nodeid, msgid, note, sendto)
1152 except roundupdb.MessageSendError as message:
1153 raise roundupdb.DetectorError(message)
1154
1155 Note that this is the standard nosy reaction code, with the small
1156 addition of::
1157
1158 # filter out the people on vacation
1159 sendto = [i for i in sendto if not users.get(i, 'vacation', 0)]
1160
1161 which filters out the users that have the vacation flag set to true.
1162
1163 Adding in state transition control
1164 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1165
1166 Sometimes tracker admins want to control the states to which users may
1167 move issues. You can do this by following these steps:
1168
1169 1. make "status" a required variable. This is achieved by adding the
1170 following to the top of the form in the ``issue.item.html``
1171 template::
1172
1173 <input type="hidden" name="@required" value="status">
1174
1175 This will force users to select a status.
1176
1177 2. add a Multilink property to the status class::
1178
1179 stat = Class(db, "status", ... , transitions=Multilink('status'),
1180 ...)
1181
1182 and then edit the statuses already created, either:
1183
1184 a. through the web using the class list -> status class editor, or
1185 b. using the ``roundup-admin`` "set" command.
1186
1187 3. add an auditor module ``checktransition.py`` in your tracker's
1188 ``detectors`` directory, for example::
1189
1190 def checktransition(db, cl, nodeid, newvalues):
1191 ''' Check that the desired transition is valid for the "status"
1192 property.
1193 '''
1194 if 'status' not in newvalues:
1195 return
1196 current = cl.get(nodeid, 'status')
1197 new = newvalues['status']
1198 if new == current:
1199 return
1200 ok = db.status.get(current, 'transitions')
1201 if new not in ok:
1202 raise ValueError('Status not allowed to move from "%s" to "%s"'%(
1203 db.status.get(current, 'name'), db.status.get(new, 'name')))
1204
1205 def init(db):
1206 db.issue.audit('set', checktransition)
1207
1208 4. in the ``issue.item.html`` template, change the status editing bit
1209 from::
1210
1211 <th>Status</th>
1212 <td tal:content="structure context/status/menu">status</td>
1213
1214 to::
1215
1216 <th>Status</th>
1217 <td>
1218 <select tal:condition="context/id" name="status">
1219 <tal:block tal:define="ok context/status/transitions"
1220 tal:repeat="state db/status/list">
1221 <option tal:condition="python:state.id in ok"
1222 tal:attributes="
1223 value state/id;
1224 selected python:state.id == context.status.id"
1225 tal:content="state/name"></option>
1226 </tal:block>
1227 </select>
1228 <tal:block tal:condition="not:context/id"
1229 tal:replace="structure context/status/menu" />
1230 </td>
1231
1232 which displays only the allowed status to transition to.
1233
1234
1235 Blocking issues that depend on other issues
1236 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1237
1238 We needed the ability to mark certain issues as "blockers" - that is,
1239 they can't be resolved until another issue (the blocker) they rely on is
1240 resolved. To achieve this:
1241
1242 1. Create a new property on the ``issue`` class:
1243 ``blockers=Multilink("issue")``. To do this, edit the definition of
1244 this class in your tracker's ``schema.py`` file. Change this::
1245
1246 issue = IssueClass(db, "issue",
1247 assignedto=Link("user"), keyword=Multilink("keyword"),
1248 priority=Link("priority"), status=Link("status"))
1249
1250 to this, adding the blockers entry::
1251
1252 issue = IssueClass(db, "issue",
1253 blockers=Multilink("issue"),
1254 assignedto=Link("user"), keyword=Multilink("keyword"),
1255 priority=Link("priority"), status=Link("status"))
1256
1257 2. Add the new ``blockers`` property to the ``issue.item.html`` edit
1258 page, using something like::
1259
1260 <th>Waiting On</th>
1261 <td>
1262 <span tal:replace="structure python:context.blockers.field(showid=1,
1263 size=20)" />
1264 <span tal:replace="structure python:db.issue.classhelp('id,title',
1265 property='blockers')" />
1266 <span tal:condition="context/blockers"
1267 tal:repeat="blk context/blockers">
1268 <br>View: <a tal:attributes="href string:issue${blk/id}"
1269 tal:content="blk/id"></a>
1270 </span>
1271 </td>
1272
1273 You'll need to fiddle with your item page layout to find an
1274 appropriate place to put it - I'll leave that fun part up to you.
1275 Just make sure it appears in the first table, possibly somewhere near
1276 the "superseders" field.
1277
1278 3. Create a new detector module (see below) which enforces the rules:
1279
1280 - issues may not be resolved if they have blockers
1281 - when a blocker is resolved, it's removed from issues it blocks
1282
1283 The contents of the detector should be something like this::
1284
1285
1286 def blockresolution(db, cl, nodeid, newvalues):
1287 ''' If the issue has blockers, don't allow it to be resolved.
1288 '''
1289 if nodeid is None:
1290 blockers = []
1291 else:
1292 blockers = cl.get(nodeid, 'blockers')
1293 blockers = newvalues.get('blockers', blockers)
1294
1295 # don't do anything if there's no blockers or the status hasn't
1296 # changed
1297 if not blockers or 'status' not in newvalues:
1298 return
1299
1300 # get the resolved state ID
1301 resolved_id = db.status.lookup('resolved')
1302
1303 # format the info
1304 u = db.config.TRACKER_WEB
1305 s = ', '.join(['<a href="%sissue%s">%s</a>'%(
1306 u,id,id) for id in blockers])
1307 if len(blockers) == 1:
1308 s = 'issue %s is'%s
1309 else:
1310 s = 'issues %s are'%s
1311
1312 # ok, see if we're trying to resolve
1313 if newvalues['status'] == resolved_id:
1314 raise ValueError("This issue can't be resolved until %s resolved."%s)
1315
1316
1317 def resolveblockers(db, cl, nodeid, oldvalues):
1318 ''' When we resolve an issue that's a blocker, remove it from the
1319 blockers list of the issue(s) it blocks.
1320 '''
1321 newstatus = cl.get(nodeid,'status')
1322
1323 # no change?
1324 if oldvalues.get('status', None) == newstatus:
1325 return
1326
1327 resolved_id = db.status.lookup('resolved')
1328
1329 # interesting?
1330 if newstatus != resolved_id:
1331 return
1332
1333 # yes - find all the blocked issues, if any, and remove me from
1334 # their blockers list
1335 issues = cl.find(blockers=nodeid)
1336 for issueid in issues:
1337 blockers = cl.get(issueid, 'blockers')
1338 if nodeid in blockers:
1339 blockers.remove(nodeid)
1340 cl.set(issueid, blockers=blockers)
1341
1342 def init(db):
1343 # might, in an obscure situation, happen in a create
1344 db.issue.audit('create', blockresolution)
1345 db.issue.audit('set', blockresolution)
1346
1347 # can only happen on a set
1348 db.issue.react('set', resolveblockers)
1349
1350 Put the above code in a file called "blockers.py" in your tracker's
1351 "detectors" directory.
1352
1353 4. Finally, and this is an optional step, modify the tracker web page
1354 URLs so they filter out issues with any blockers. You do this by
1355 adding an additional filter on "blockers" for the value "-1". For
1356 example, the existing "Show All" link in the "page" template (in the
1357 tracker's "html" directory) looks like this::
1358
1359 <a href="#"
1360 tal:attributes="href python:request.indexargs_url('issue', {
1361 '@sort': '-activity',
1362 '@group': 'priority',
1363 '@filter': 'status',
1364 '@columns': columns_showall,
1365 '@search_text': '',
1366 'status': status_notresolved,
1367 '@dispname': i18n.gettext('Show All'),
1368 })"
1369 i18n:translate="">Show All</a><br>
1370
1371 modify it to add the "blockers" info to the URL (note, both the
1372 "@filter" *and* "blockers" values must be specified)::
1373
1374 <a href="#"
1375 tal:attributes="href python:request.indexargs_url('issue', {
1376 '@sort': '-activity',
1377 '@group': 'priority',
1378 '@filter': 'status,blockers',
1379 '@columns': columns_showall,
1380 '@search_text': '',
1381 'status': status_notresolved,
1382 'blockers': '-1',
1383 '@dispname': i18n.gettext('Show All'),
1384 })"
1385 i18n:translate="">Show All</a><br>
1386
1387 The above examples are line-wrapped on the trailing & and should
1388 be unwrapped.
1389
1390 That's it. You should now be able to set blockers on your issues. Note
1391 that if you want to know whether an issue has any other issues dependent
1392 on it (i.e. it's in their blockers list) you can look at the journal
1393 history at the bottom of the issue page - look for a "link" event to
1394 another issue's "blockers" property.
1395
1396 Add users to the nosy list based on the keyword
1397 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1398
1399 Let's say we need the ability to automatically add users to the nosy
1400 list based
1401 on the occurance of a keyword. Every user should be allowed to edit their
1402 own list of keywords for which they want to be added to the nosy list.
1403
1404 Below, we'll show that this change can be done with minimal
1405 understanding of the Roundup system, using only copy and paste.
1406
1407 This requires three changes to the tracker: a change in the database to
1408 allow per-user recording of the lists of keywords for which he wants to
1409 be put on the nosy list, a change in the user view allowing them to edit
1410 this list of keywords, and addition of an auditor which updates the nosy
1411 list when a keyword is set.
1412
1413 Adding the nosy keyword list
1414 ::::::::::::::::::::::::::::
1415
1416 The change to make in the database, is that for any user there should be a list
1417 of keywords for which he wants to be put on the nosy list. Adding a
1418 ``Multilink`` of ``keyword`` seems to fullfill this. As such, all that has to
1419 be done is to add a new field to the definition of ``user`` within the file
1420 ``schema.py``. We will call this new field ``nosy_keywords``, and the updated
1421 definition of user will be::
1422
1423 user = Class(db, "user",
1424 username=String(), password=Password(),
1425 address=String(), realname=String(),
1426 phone=String(), organisation=String(),
1427 alternate_addresses=String(),
1428 queries=Multilink('query'), roles=String(),
1429 timezone=String(),
1430 nosy_keywords=Multilink('keyword'))
1431
1432 Changing the user view to allow changing the nosy keyword list
1433 ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
1434
1435 We want any user to be able to change the list of keywords for which
1436 he will by default be added to the nosy list. We choose to add this
1437 to the user view, as is generated by the file ``html/user.item.html``.
1438 We can easily
1439 see that the keyword field in the issue view has very similar editing
1440 requirements as our nosy keywords, both being lists of keywords. As
1441 such, we look for Keywords in ``issue.item.html``, and extract the
1442 associated parts from there. We add this to ``user.item.html`` at the
1443 bottom of the list of viewed items (i.e. just below the 'Alternate
1444 E-mail addresses' in the classic template)::
1445
1446 <tr>
1447 <th>Nosy Keywords</th>
1448 <td>
1449 <span tal:replace="structure context/nosy_keywords/field" />
1450 <span tal:replace="structure python:db.keyword.classhelp(property='nosy_keywords')" />
1451 </td>
1452 </tr>
525 1453
526 add_recipients -- ``new`` 1454
527 Do the recipients (``To:``, ``Cc:``) of a message get placed on the nosy 1455 Addition of an auditor to update the nosy list
528 list? If ``new`` is used, then the recipients will only be added when a 1456 ::::::::::::::::::::::::::::::::::::::::::::::
529 message creates a new issue. If ``yes``, then the recipients will be added 1457
530 on followups too. If ``no``, they're never added to the nosy. 1458 The more difficult part is the logic to add
531 Allowed values: ``yes``, ``no``, ``new`` 1459 the users to the nosy list when required.
532 1460 We choose to perform this action whenever the keywords on an
533 email_sending -- ``single`` 1461 item are set (this includes the creation of items).
534 Controls the email sending from the nosy reactor. If ``multiple`` then 1462 Here we choose to start out with a copy of the
535 a separate email is sent to each recipient. If ``single`` then a single 1463 ``detectors/nosyreaction.py`` detector, which we copy to the file
536 email is sent with each recipient as a CC address. 1464 ``detectors/nosy_keyword_reaction.py``.
537 1465 This looks like a good start as it also adds users
538 max_attachment_size -- ``2147483647`` 1466 to the nosy list. A look through the code reveals that the
539 Attachments larger than the given number of bytes won't be attached 1467 ``nosyreaction`` function actually sends the e-mail.
540 to nosy mails. They will be replaced by a link to the tracker's 1468 We don't need this. Therefore, we can change the ``init`` function to::
541 download page for the file. 1469
542 1470 def init(db):
543 1471 db.issue.audit('create', update_kw_nosy)
544 .. index:: single: roundup-admin; config.ini update 1472 db.issue.audit('set', update_kw_nosy)
545 single: roundup-admin; config.ini create 1473
546 single: config.ini; create 1474 After that, we rename the ``updatenosy`` function to ``update_kw_nosy``.
547 single: config.ini; update 1475 The first two blocks of code in that function relate to setting
548 1476 ``current`` to a combination of the old and new nosy lists. This
549 You may generate a new default config file using the ``roundup-admin 1477 functionality is left in the new auditor. The following block of
550 genconfig`` command. You can generate a new config file merging in 1478 code, which handled adding the assignedto user(s) to the nosy list in
551 existing settings using the ``roundup-admin updateconfig`` command. 1479 ``updatenosy``, should be replaced by a block of code to add the
552 1480 interested users to the nosy list. We choose here to loop over all
553 Configuration variables may be referred to in lower or upper case. In code, 1481 new keywords, than looping over all users,
554 variables not in the "main" section are referred to using their section and 1482 and assign the user to the nosy list when the keyword occurs in the user's
555 name, so "domain" in the section "mail" becomes MAIL_DOMAIN. 1483 ``nosy_keywords``. The next part in ``updatenosy`` -- adding the author
556 1484 and/or recipients of a message to the nosy list -- is obviously not
557 .. index:: pair: configuration; extensions 1485 relevant here and is thus deleted from the new auditor. The last
558 pair: configuration; detectors 1486 part, copying the new nosy list to ``newvalues``, can stay as is.
559 1487 This results in the following function::
560 Extending the configuration file 1488
561 -------------------------------- 1489 def update_kw_nosy(db, cl, nodeid, newvalues):
562 1490 '''Update the nosy list for changes to the keywords
563 You can't add new variables to the config.ini file in the tracker home but 1491 '''
564 you can add two new config.ini files: 1492 # nodeid will be None if this is a new node
565 1493 current = {}
566 - a config.ini in the ``extensions`` directory will be loaded and attached 1494 if nodeid is None:
567 to the config variable as "ext". 1495 ok = ('new', 'yes')
568 - a config.ini in the ``detectors`` directory will be loaded and attached 1496 else:
569 to the config variable as "detectors". 1497 ok = ('yes',)
570 1498 # old node, get the current values from the node if they haven't
571 For example, the following in ``detectors/config.ini``:: 1499 # changed
1500 if 'nosy' not in newvalues:
1501 nosy = cl.get(nodeid, 'nosy')
1502 for value in nosy:
1503 if value not in current:
1504 current[value] = 1
1505
1506 # if the nosy list changed in this transaction, init from the new value
1507 if 'nosy' in newvalues:
1508 nosy = newvalues.get('nosy', [])
1509 for value in nosy:
1510 if not db.hasnode('user', value):
1511 continue
1512 if value not in current:
1513 current[value] = 1
1514
1515 # add users with keyword in nosy_keywords to the nosy list
1516 if 'keyword' in newvalues and newvalues['keyword'] is not None:
1517 keyword_ids = newvalues['keyword']
1518 for keyword in keyword_ids:
1519 # loop over all users,
1520 # and assign user to nosy when keyword in nosy_keywords
1521 for user_id in db.user.list():
1522 nosy_kw = db.user.get(user_id, "nosy_keywords")
1523 found = 0
1524 for kw in nosy_kw:
1525 if kw == keyword:
1526 found = 1
1527 if found:
1528 current[user_id] = 1
1529
1530 # that's it, save off the new nosy list
1531 newvalues['nosy'] = list(current.keys())
1532
1533 These two function are the only ones needed in the file.
1534
1535 TODO: update this example to use the ``find()`` Class method.
1536
1537 Caveats
1538 :::::::
1539
1540 A few problems with the design here can be noted:
1541
1542 Multiple additions
1543 When a user, after automatic selection, is manually removed
1544 from the nosy list, he is added to the nosy list again when the
1545 keyword list of the issue is updated. A better design might be
1546 to only check which keywords are new compared to the old list
1547 of keywords, and only add users when they have indicated
1548 interest on a new keyword.
1549
1550 The code could also be changed to only trigger on the ``create()``
1551 event, rather than also on the ``set()`` event, thus only setting
1552 the nosy list when the issue is created.
1553
1554 Scalability
1555 In the auditor, there is a loop over all users. For a site with
1556 only few users this will pose no serious problem; however, with
1557 many users this will be a serious performance bottleneck.
1558 A way out would be to link from the keywords to the users who
1559 selected these keywords as nosy keywords. This will eliminate the
1560 loop over all users. See the ``rev_multilink`` attribute to make
1561 this easier.
1562
1563 Restricting updates that arrive by email
1564 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1565
1566 Roundup supports multiple update methods:
1567
1568 1. command line
1569 2. plain email
1570 3. pgp signed email
1571 4. web access
1572
1573 in some cases you may need to prevent changes to properties by some of
1574 these methods. For example you can set up issues that are viewable
1575 only by people on the nosy list. So you must prevent unauthenticated
1576 changes to the nosy list.
1577
1578 Since plain email can be easily forged, it does not provide sufficient
1579 authentication in this senario.
1580
1581 To prevent this we can add a detector that audits the source of the
1582 transaction and rejects the update if it changes the nosy list.
1583
1584 Create the detector (auditor) module and add it to the detectors
1585 directory of your tracker::
1586
1587 from roundup import roundupdb, hyperdb
1588
1589 from roundup.mailgw import Unauthorized
1590
1591 def restrict_nosy_changes(db, cl, nodeid, newvalues):
1592 '''Do not permit changes to nosy via email.'''
1593
1594 if 'nosy' not in newvalues:
1595 # the nosy field has not changed so no need to check.
1596 return
1597
1598 if db.tx_Source in ['web', 'rest', 'xmlrpc', 'email-sig-openpgp', 'cli' ]:
1599 # if the source of the transaction is from an authenticated
1600 # source or a privileged process allow the transaction.
1601 # Other possible sources: 'email'
1602 return
1603
1604 # otherwise raise an error
1605 raise Unauthorized( \
1606 'Changes to nosy property not allowed via %s for this issue.'%\
1607 tx_Source)
1608
1609 def init(db):
1610 ''' Install restrict_nosy_changes to run after other auditors.
1611
1612 Allow initial creation email to set nosy.
1613 So don't execute: db.issue.audit('create', requestedbyauditor)
1614
1615 Set priority to 110 to run this auditor after other auditors
1616 that can cause nosy to change.
1617 '''
1618 db.issue.audit('set', restrict_nosy_changes, 110)
1619
1620 This detector (auditor) will prevent updates to the nosy field if it
1621 arrives by email. Since it runs after other auditors (due to the
1622 priority of 110), it will also prevent changes to the nosy field that
1623 are done by other auditors if triggered by an email.
1624
1625 Note that db.tx_Source was not present in roundup versions before
1626 1.4.22, so you must be running a newer version to use this detector.
1627 Read the CHANGES.txt document in the roundup source code for further
1628 details on tx_Source.
1629
1630 Changes to Security and Permissions
1631 -----------------------------------
1632
1633 Restricting the list of users that are assignable to a task
1634 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1635
1636 1. In your tracker's ``schema.py``, create a new Role, say "Developer"::
1637
1638 db.security.addRole(name='Developer', description='A developer')
1639
1640 2. Just after that, create a new Permission, say "Fixer", specific to
1641 "issue"::
1642
1643 p = db.security.addPermission(name='Fixer', klass='issue',
1644 description='User is allowed to be assigned to fix issues')
1645
1646 3. Then assign the new Permission to your "Developer" Role::
1647
1648 db.security.addPermissionToRole('Developer', p)
1649
1650 4. In the issue item edit page (``html/issue.item.html`` in your tracker
1651 directory), use the new Permission in restricting the "assignedto"
1652 list::
1653
1654 <select name="assignedto">
1655 <option value="-1">- no selection -</option>
1656 <tal:block tal:repeat="user db/user/list">
1657 <option tal:condition="python:user.hasPermission(
1658 'Fixer', context._classname)"
1659 tal:attributes="
1660 value user/id;
1661 selected python:user.id == context.assignedto"
1662 tal:content="user/realname"></option>
1663 </tal:block>
1664 </select>
1665
1666 For extra security, you may wish to setup an auditor to enforce the
1667 Permission requirement (install this as ``assignedtoFixer.py`` in your
1668 tracker ``detectors`` directory)::
1669
1670 def assignedtoMustBeFixer(db, cl, nodeid, newvalues):
1671 ''' Ensure the assignedto value in newvalues is used with the
1672 Fixer Permission
1673 '''
1674 if 'assignedto' not in newvalues:
1675 # don't care
1676 return
1677
1678 # get the userid
1679 userid = newvalues['assignedto']
1680 if not db.security.hasPermission('Fixer', userid, cl.classname):
1681 raise ValueError('You do not have permission to edit %s'%cl.classname)
1682
1683 def init(db):
1684 db.issue.audit('set', assignedtoMustBeFixer)
1685 db.issue.audit('create', assignedtoMustBeFixer)
1686
1687 So now, if an edit action attempts to set "assignedto" to a user that
1688 doesn't have the "Fixer" Permission, the error will be raised.
1689
1690
1691 Users may only edit their issues
1692 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1693
1694 In this case, users registering themselves are granted Provisional
1695 access, meaning they
1696 have access to edit the issues they submit, but not others. We create a new
1697 Role called "Provisional User" which is granted to newly-registered users,
1698 and has limited access. One of the Permissions they have is the new "Edit
1699 Own" on issues (regular users have "Edit".)
1700
1701 First up, we create the new Role and Permission structure in
1702 ``schema.py``::
1703
1704 #
1705 # New users not approved by the admin
1706 #
1707 db.security.addRole(name='Provisional User',
1708 description='New user registered via web or email')
1709
1710 # These users need to be able to view and create issues but only edit
1711 # and view their own
1712 db.security.addPermissionToRole('Provisional User', 'Create', 'issue')
1713 def own_issue(db, userid, itemid):
1714 '''Determine whether the userid matches the creator of the issue.'''
1715 return userid == db.issue.get(itemid, 'creator')
1716 p = db.security.addPermission(name='Edit', klass='issue',
1717 check=own_issue, description='Can only edit own issues')
1718 db.security.addPermissionToRole('Provisional User', p)
1719 p = db.security.addPermission(name='View', klass='issue',
1720 check=own_issue, description='Can only view own issues')
1721 db.security.addPermissionToRole('Provisional User', p)
1722 # This allows the interface to get the names of the properties
1723 # in the issue. Used for selecting sorting and grouping
1724 # on the index page.
1725 p = db.security.addPermission(name='Search', klass='issue')
1726 db.security.addPermissionToRole ('Provisional User', p)
1727
1728
1729 # Assign the Permissions for issue-related classes
1730 for cl in 'file', 'msg', 'query', 'keyword':
1731 db.security.addPermissionToRole('Provisional User', 'View', cl)
1732 db.security.addPermissionToRole('Provisional User', 'Edit', cl)
1733 db.security.addPermissionToRole('Provisional User', 'Create', cl)
1734 for cl in 'priority', 'status':
1735 db.security.addPermissionToRole('Provisional User', 'View', cl)
1736
1737 # and give the new users access to the web and email interface
1738 db.security.addPermissionToRole('Provisional User', 'Web Access')
1739 db.security.addPermissionToRole('Provisional User', 'Email Access')
1740
1741 # make sure they can view & edit their own user record
1742 def own_record(db, userid, itemid):
1743 '''Determine whether the userid matches the item being accessed.'''
1744 return userid == itemid
1745 p = db.security.addPermission(name='View', klass='user', check=own_record,
1746 description="User is allowed to view their own user details")
1747 db.security.addPermissionToRole('Provisional User', p)
1748 p = db.security.addPermission(name='Edit', klass='user', check=own_record,
1749 description="User is allowed to edit their own user details")
1750 db.security.addPermissionToRole('Provisional User', p)
1751
1752 Then, in ``config.ini``, we change the Role assigned to newly-registered
1753 users, replacing the existing ``'User'`` values::
572 1754
573 [main] 1755 [main]
574 qa_recipients = email@example.com 1756 ...
575 1757 new_web_user_roles = Provisional User
576 is accessible as:: 1758 new_email_user_roles = Provisional User
577 1759
578 db.config.detectors['QA_RECIPIENTS'] 1760
579 1761 All users may only view and edit issues, files and messages they create
580 Note that the name grouping applied to the main configuration file is 1762 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
581 applied to the extension config files, so if you instead have:: 1763
582 1764 Replace the standard "classic" tracker View and Edit Permission assignments
583 [qa] 1765 for the "issue", "file" and "msg" classes with the following::
584 recipients = email@example.com 1766
585 1767 def checker(klass):
586 then the above ``db.config.detectors['QA_RECIPIENTS']`` will still work. 1768 def check(db, userid, itemid, klass=klass):
587 1769 return db.getclass(klass).get(itemid, 'creator') == userid
588 Unlike values in the tracker's main ``config.ini``, the values defined 1770 return check
589 in these config files are not validated. For example: a setting that 1771 for cl in 'issue', 'file', 'msg':
590 is supposed to be an integer value (e.g. 4) could be the word 1772 p = db.security.addPermission(name='View', klass=cl,
591 "foo". If you are writing Python code that uses these settings, you 1773 check=checker(cl),
592 should expect to handle invalid values. 1774 description='User can view only if creator.')
593 1775 db.security.addPermissionToRole('User', p)
594 Also, incorrect values aren't discovered until the config setting is 1776 p = db.security.addPermission(name='Edit', klass=cl,
595 used. This can be long after the tracker is started and the error may 1777 check=checker(cl),
596 not be seen in the logs. 1778 description='User can edit only if creator.')
597 1779 db.security.addPermissionToRole('User', p)
598 It is possible to validate these settings. Validation involves calling 1780 db.security.addPermissionToRole('User', 'Create', cl)
599 the ``update_options`` method on the configuration option. This can be 1781 # This allows the interface to get the names of the properties
600 done from the ``init()`` function in the Python files implementing 1782 # in the issue. Used for selecting sorting and grouping
601 extensions_ or detectors_. 1783 # on the index page.
602 1784 p = db.security.addPermission(name='Search', klass='issue')
603 As an example, adding the following to an extension:: 1785 db.security.addPermissionToRole ('User', p)
604 1786
605 from roundup.configuration import SecretMandatoryOption 1787
606 1788 Moderating user registration
607 def init(instance): 1789 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
608 instance.config.ext.update_option('RECAPTCHA_SECRET', 1790
609 SecretMandatoryOption,description="Secret securing reCaptcha.") 1791 You could set up new-user moderation in a public tracker by:
610 1792
611 similarly for a detector:: 1793 1. creating a new highly-restricted user role "Pending",
612 1794 2. set the config new_web_user_roles and/or new_email_user_roles to that
613 from roundup.configuration import MailAddressOption 1795 role,
614 1796 3. have an auditor that emails you when new users are created with that
615 def init(db): 1797 role using roundup.mailer
616 try: 1798 4. edit the role to "User" for valid users.
617 db.config.detectors.update_option('QA_RECIPIENTS', 1799
618 MailAddressOption, 1800 Some simple javascript might help in the last step. If you have high volume
619 description="Email used for QA comment followup.") 1801 you could search for all currently-Pending users and do a bulk edit of all
620 except KeyError: 1802 their roles at once (again probably with some simple javascript help).
621 # COMMENT_EMAIL setting is not found, but it's optional 1803
622 # so continue 1804
623 pass 1805 Changes to the Web User Interface
624 1806 ---------------------------------
625 will allow reading the secret from a file or append the tracker domain 1807
626 to an email address if it does not have a domain. 1808 Adding action links to the index page
627 1809 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
628 Running ``roundup-admin -i tracker_home display user1`` will validate 1810
629 the settings for both config.ini`s. Otherwise detector options are not 1811 Add a column to the ``item.index.html`` template.
630 validated until the first request to the web interface (or email 1812
631 gateway). 1813 Resolving the issue::
632 1814
633 There are 4 arguments for ``update_option``: 1815 <a tal:attributes="href
634 1816 string:issue${i/id}?:status=resolved&:action=edit">resolve</a>
635 1. config setting name - string (positional, mandatory) 1817
636 2. option type - Option derived class from configuration.py 1818 "Take" the issue::
637 (positional, mandatory) 1819
638 3. default value - string (optional, named default) 1820 <a tal:attributes="href
639 4. description - string (optional, named description) 1821 string:issue${i/id}?:assignedto=${request/user/id}&:action=edit">take</a>
640 1822
641 The first argument is the config setting name as described at the 1823 ... and so on.
642 beginning of this section. 1824
643 1825 Colouring the rows in the issue index according to priority
644 The second argument is a class in the roundup.configuration module. 1826 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
645 There are a number of these classes: BooleanOption, 1827
646 IntegerNumberOption, RegExpOption.... Please see the configuration 1828 A simple ``tal:attributes`` statement will do the bulk of the work here. In
647 module for all Option validators and their descriptions. You can also 1829 the ``issue.index.html`` template, add this to the ``<tr>`` that
648 define your own custom validator in `interfaces.py`_. 1830 displays the rows of data::
649 1831
650 The third and fourth arguments are strings and are optional. They are 1832 <tr tal:attributes="class string:priority-${i/priority/plain}">
651 printed if there is an error and may help the user correct the problem. 1833
652 1834 and then in your stylesheet (``style.css``) specify the colouring for the
653 .. index:: ! schema 1835 different priorities, as follows::
654 1836
655 Tracker Schema 1837 tr.priority-critical td {
656 ============== 1838 background-color: red;
657
658 .. note::
659 if you modify the schema, you'll most likely need to edit the
660 `web interface`_ HTML template files and `detectors`_ to reflect
661 your changes.
662
663 A tracker schema defines what data is stored in the tracker's database.
664 Schemas are defined using Python code in the ``schema.py`` module of your
665 tracker.
666
667 The ``schema.py`` and ``initial_data.py`` modules
668 -------------------------------------------------
669
670 The schema.py module is used to define what your tracker looks like
671 on the inside, the schema of the tracker. It defines the Classes
672 and properties on each class. It also defines the security for
673 those Classes. The next few sections describe how schemas work
674 and what you can do with them.
675
676 The initial_data.py module sets up the initial state of your
677 tracker. It’s called exactly once - by the ``roundup-admin initialise``
678 command. See the start of the section on `database content`_ for more
679 info about how this works.
680
681 .. index:: schema; classic - description of
682
683 The "classic" schema
684 --------------------
685
686 The "classic" schema looks like this (see section `setkey(property)`_
687 below for the meaning of ``'setkey'`` -- you may also want to look into
688 the sections `setlabelprop(property)`_ and `setorderprop(property)`_ for
689 specifying (default) labelling and ordering of classes.)::
690
691 pri = Class(db, "priority", name=String(), order=String())
692 pri.setkey("name")
693
694 stat = Class(db, "status", name=String(), order=String())
695 stat.setkey("name")
696
697 keyword = Class(db, "keyword", name=String())
698 keyword.setkey("name")
699
700 user = Class(db, "user", username=String(), organisation=String(),
701 password=String(), address=String(), realname=String(),
702 phone=String(), alternate_addresses=String(),
703 queries=Multilink('query'), roles=String(), timezone=String())
704 user.setkey("username")
705
706 msg = FileClass(db, "msg", author=Link("user"), summary=String(),
707 date=Date(), recipients=Multilink("user"),
708 files=Multilink("file"), messageid=String(), inreplyto=String())
709
710 file = FileClass(db, "file", name=String())
711
712 issue = IssueClass(db, "issue", keyword=Multilink("keyword"),
713 status=Link("status"), assignedto=Link("user"),
714 priority=Link("priority"))
715 issue.setkey('title')
716
717 .. index:: schema; allowed changes
718
719 What you can't do to the schema
720 -------------------------------
721
722 You must never:
723
724 **Remove the user class**
725 This class is the only *required* class in Roundup.
726
727 **Remove the "username", "address", "password" or "realname" user properties**
728 Various parts of Roundup require these properties. Don't remove them.
729
730 **Change the type of a property**
731 Property types must *never* be changed - the database simply doesn't take
732 this kind of action into account. Note that you can't just remove a
733 property and re-add it as a new type either. If you wanted to make the
734 assignedto property a Multilink, you'd need to create a new property
735 assignedto_list and remove the old assignedto property.
736
737
738 What you can do to the schema
739 -----------------------------
740
741 Your schema may be changed at any time before or after the tracker has been
742 initialised (or used). You may:
743
744 **Add new properties to classes, or add whole new classes**
745 This is painless and easy to do - there are generally no repercussions
746 from adding new information to a tracker's schema.
747
748 **Remove properties**
749 Removing properties is a little more tricky - you need to make sure that
750 the property is no longer used in the `web interface`_ *or* by the
751 detectors_.
752
753
754
755 Classes and Properties - creating a new information store
756 ---------------------------------------------------------
757
758 In the tracker above, we've defined 7 classes of information:
759
760 priority
761 Defines the possible levels of urgency for issues.
762
763 status
764 Defines the possible states of processing the issue may be in.
765
766 keyword
767 Initially empty, will hold keywords useful for searching issues.
768
769 user
770 Initially holding the "admin" user, will eventually have an entry
771 for all users using Roundup.
772
773 msg
774 Initially empty, will hold all e-mail messages sent to or
775 generated by Roundup.
776
777 file
778 Initially empty, will hold all files attached to issues.
779
780 issue
781 Initially empty, this is where the issue information is stored.
782
783 We define the "priority" and "status" classes to allow two things:
784
785 1. reduction in the amount of information stored on the issue
786 2. more powerful, accurate searching of issues by priority and status
787
788 By only requiring a link on the issue (which is stored as a single
789 number) we reduce the chance that someone mis-types a priority or
790 status - or simply makes a new one up.
791
792 Class names are used to access items of that class in the `REST api`_
793 interface. The classic tracker was created before the REST interface
794 was added. It uses the single form (i.e. issue and user not issues and
795 users) for its classes. Most REST documentation suggests using plural
796 forms. However, to make your API consistent, use singular forms for
797 classes that you add.
798
799 Class and Items
800 ~~~~~~~~~~~~~~~
801
802 A Class defines a particular class (or type) of data that will be stored
803 in the database. A class comprises one or more properties, which gives
804 the information about the class items.
805
806 The actual data entered into the database, using ``class.create()``, are
807 called items. They have a special immutable property called ``'id'``. We
808 sometimes refer to this as the *itemid*.
809
810
811 .. index:: schema; property types
812
813 Properties
814 ~~~~~~~~~~
815
816 A Class is comprised of one or more properties of the following types:
817
818 String
819 properties are for storing arbitrary-length strings.
820 Password
821 properties are for storing encoded arbitrary-length strings.
822 The default encoding is defined on the ``roundup.password.Password``
823 class.
824 Date
825 properties store date-and-time stamps. Their values are Timestamp
826 objects.
827 Interval
828 properties store time periods rather than absolute dates. For
829 example 2 hours.
830 Integer
831 properties store integer values. (Number can store real/float values.)
832 Number
833 properties store numeric values. There is an option to use
834 double-precision floating point numbers.
835 Boolean
836 properties store on/off, yes/no, true/false values.
837 Link
838 properties refers to a single other item selected from a
839 specified class. The class is part of the property; the value is an
840 integer, the id of the chosen item.
841 Multilink
842 properties refer to possibly many items in a specified
843 class. The value is a list of integers.
844
845 Properties can have additional attributes to change the default
846 behaviour:
847
848 .. index:: triple: schema; property attributes; required
849 triple: schema; property attributes; default_value
850 triple: schema; property attributes; quiet
851
852 * All properties support the following attributes:
853
854 - ``required``: see `design documentation`_. Adds the property to
855 the list returned by calling get_required_props for the class.
856 - ``default_value``: see `design documentation`_ Sets the default
857 value if the property is not set.
858 - ``quiet``: see `design documentation`_. Suppresses user visible
859 to changes to this property. The property change is not reported:
860
861 - in the change feedback/confirmation message in the web
862 interface
863 - the property change section of the nosy email
864 - the web history at the bottom of an item's page
865
866 This can be used to store state of the user interface (e.g. the
867 names of elements that are collapsed or hidden from the
868 user). Making properties that are updated as an indirect result of
869 a user's change (e.g. updating a blockers property, counting
870 number of times an issue was reopened or reassigned etc.) should
871 not be displayed to the user as they can be confusing.
872
873 .. index:: triple: schema; property attributes; indexme
874
875 * String properties can have an ``indexme`` attribute that defines if the
876 property should be part of the full text index. The default is 'no' but this
877 can be set to 'yes' to allow a property's contents to be in the full
878 text index.
879
880 .. index:: triple: schema; property attributes; use_double
881
882 * Number properties can have a ``use_double`` attribute that, when set
883 to ``True``, will use double precision floating point in the database.
884 * Link and Multilink properties can have several attributes:
885
886 .. index:: triple: schema; property attributes; do_journal
887
888 - ``do_journal``: By default, every change of a link property is
889 recorded in the item being linked to (or being unlinked). A typical
890 use-case for setting ``do_journal='no'`` would be to turn off
891 journalling of nosy list, message author and message recipient link
892 and unlink events to prevent the journal from clogged with these
893 events.
894
895 .. index:: triple: schema; property attributes; try_id_parsing
896
897 - ``try_id_parsing`` is turned on by default. If entering a number
898 into a Link or Multilink field, Roundup interprets this number as an
899 ID of the item to link to. Sometimes items can have numeric names
900 (like, e.g., product codes). For these Roundup needs to match the
901 numeric name and should never match an ID. In this case you can set
902 ``try_id_parsing='no'``.
903
904 .. index:: triple: schema; property attributes; rev_multilink
905
906 - The ``rev_multilink`` option takes a property name to be inserted
907 into the linked-to class. This property is a Multilink property that
908 links back to the current class. The new Multilink is read-only (it
909 is automatically modified if the Link or Multilink property defining
910 it is modified). The new property can be used in normal searches
911 using the "filter" method of the Class. This means it can be used
912 like other Multilink properties when searching (in an index
913 template) or via the REST and XMLRPC APIs.
914
915 As a example, suppose you want to group multiple issues into a
916 super issue. Each issue can be part of only one super issue. It is
917 inefficient to find all of the issues that are part of the
918 super issue by searching through all issues in the system looking
919 at the part_of link property. To make this more efficient, you
920 can declare an issue's part_of property as::
921
922 issue = IssueClass(db, "issue",
923 ...
924 part_of = Link("issue", rev_multilink="components"),
925 ... )
926
927 This automatically creates the ``components`` multilink on the issue
928 class. The ``components`` multilink is never explicitly declared in
929 the issue class, but it has the same effect as though you had
930 declared the class as::
931
932 issue = IssueClass(db, "issue",
933 ...
934 part_of = Link("issue"),
935 components = Multilink("issue"),
936 ... )
937
938 Then wrote a detector to update the components property on the
939 corresponding issue. Writing this detector can be tricky. There is
940 one other difference, you can not explicitly set/modify the
941 ``components`` multilink.
942
943 The effect of setting ``part_of = 3456`` on issue1234
944 automatically adds "1234" to the ``components`` property on
945 issue3456. You can search the ``components`` multilink just like a
946 regular multilink, but you can't explicitly assign to it.
947 Another difference of reverse multilinks to normal multilinks
948 is that when a linked node is retired, the node vanishes from the
949 multilink, e.g. in the example above, if an issue with ``part_of``
950 set to another issue is retired this issue vanishes from the
951 ``components`` multilink of the other issue.
952
953 You can also link between different classes. So you can modify
954 the issue definition to include::
955
956 issue = IssueClass(db, "issue",
957 ...
958 assigned_to = Link("user", rev_multilink="responsibleFor"),
959 ... )
960
961 This makes it easy to list all issues that the user is responsible
962 for (aka assigned_to).
963
964 .. index:: triple: schema; property attributes; msg_header_property
965
966 - The ``msg_header_property`` is used by the mail gateway when sending
967 out messages. When a link or multilink property of an issue changes,
968 Roundup creates email headers of the form::
969
970 X-Roundup-issue-prop: value
971
972 where ``value`` is the ``name`` property for the linked item(s).
973 For example, if you have a multilink for attached_files in your
974 issue, you will see a header::
975
976 X-Roundup-issue-attached_files: MySpecialFile.doc, HisResume.txt
977
978 when the class for attached files is defined as::
979
980 file = FileClass(db, "file", name=String())
981
982 ``MySpecialFile.doc`` is the name for the file object.
983
984 If you have an ``assigned_to`` property in your issue class that
985 links to the user class and you want to add a header::
986
987 X-Roundup-issue-assigned_to: ...
988
989 so that the mail recipients can filter emails where
990 ``X-Roundup-issue-assigned_to: name`` that contains their
991 username. The user class is defined as::
992
993 user = Class(db, "user",
994 username=String(),
995 password=Password(),
996 address=String(),
997 realname=String(),
998 phone=String(),
999 organisation=String(),
1000 alternate_addresses=String(),
1001 queries=Multilink('query'),
1002 roles=String(), # comma-separated string of Role names
1003 timezone=String())
1004
1005 Because there is no ``name`` parameter for the user class, there
1006 will be no header. However setting::
1007
1008 assigned_to=Link("user", msg_header_property="username")
1009
1010 will make the mail gateway generate an ``X-Roundup-issue-assigned_to``
1011 using the username property of the linked user.
1012
1013 Assume assigned_to for an issue is linked to the user with
1014 username=joe_user, setting::
1015
1016 msg_header_property="username"
1017
1018 for the assigned_to property will generated message headers of the
1019 form::
1020
1021 X-Roundup-issue-assigned_to: joe_user
1022
1023 for emails sent on issues where joe_user has been assigned to the issue.
1024
1025 If this property is set to the empty string "", it will prevent
1026 the header from being generated on outgoing mail.
1027
1028 .. index:: triple: schema; class property; creator
1029 triple: schema; class property; creation
1030 triple: schema; class property; actor
1031 triple: schema; class property; activity
1032
1033 All Classes automatically have a number of properties by default:
1034
1035 *creator*
1036 Link to the user that created the item.
1037 *creation*
1038 Date the item was created.
1039 *actor*
1040 Link to the user that last modified the item.
1041 *activity*
1042 Date the item was last modified.
1043
1044
1045 .. index:: triple: schema; class property; content
1046 triple: schema; class property; type
1047
1048 FileClass
1049 ~~~~~~~~~
1050
1051 FileClasses save their "content" attribute off in a separate file from
1052 the rest of the database. This reduces the number of large entries in
1053 the database, which generally makes databases more efficient, and also
1054 allows us to use command-line tools to operate on the files. They are
1055 stored in the files sub-directory of the ``'db'`` directory in your
1056 tracker. FileClasses also have a "type" attribute to store the MIME
1057 type of the file.
1058
1059 Roundup by default considers the contents of the file immutable. This
1060 is to assist in maintaining an accurate record of correspondence. The
1061 distributed tracker templates do not enforce this. So if you have
1062 access to the Roundup tracker directory, you can edit the files (make
1063 sure to preserve mode, owner and group) to remove information (e.g. if
1064 somebody includes a password or you need to redact proprietary
1065 information). Obviously the journal for the message/file will not
1066 report that the file has changed.
1067
1068 Best practice is to remove offending material and leave a
1069 placeholder. E.G. replace a password with the text::
1070
1071 [password has been deleted 2020-12-02 --myname]
1072
1073 If you need to delete an entire file, replace the file contents with::
1074
1075 [file contents deleted due to spam 2020-10-21 --myname]
1076
1077 rather than deleting the file. If you actually delete the file Roundup
1078 will report an error to the user and email the administrator. If you
1079 empty the file, a user downloading the file using the direct URL
1080 (e.g. ``tracker/msg22``) may be confused and think something is broken
1081 when they receive an empty file. Retiring a file/msg does not prevent
1082 access to the file using the direct URL. Retiring an item only removes
1083 it when requesting a list of all items in the class. If you are
1084 replacing the contents, you probably want to change the content type
1085 of the file. E.G. from ``image/jpeg`` to ``text/plain``. You can do
1086 this easily through the web interface, or using the ``roundup-admin``
1087 command line interface.
1088
1089 You can also change the contents of a file or message using the REST
1090 interface. Note that this will NOT result in an entry in the journal,
1091 so again it allows a silent change. To do this you need to make two
1092 rest requests. An example using curl is::
1093
1094 $ curl -u demo:demo -s
1095 -H "X-requested-with: rest" \
1096 -H "Referer: https://tracker.example.com/demo/" \
1097 -X GET \
1098 https://tracker.example.com/demo/rest/data/file/30/content
1099 {
1100 "data": {
1101 "id": "30",
1102 "type": "<class 'str'>",
1103 "link": "https://tracker.example.com/demo/rest/data/file/30/content",
1104 "data": "hello3",
1105 "@etag": "\"3f2f8063dbce5b6bd43567e6f4f3c671\""
1106 }
1107 } 1839 }
1108 1840
1109 using the etag, overwrite the content with:: 1841 tr.priority-urgent td {
1110 1842 background-color: orange;
1111 $ curl -u demo:demo -s 1843 }
1112 -H "X-requested-with: rest" \ 1844
1113 -H "Referer: https://tracker.example.com/demo/" \ 1845 and so on, with far less offensive colours :)
1114 -H 'If-Match: "3f2f8063dbce5b6bd43567e6f4f3c671"' \ 1846
1115 -X PUT \ 1847 Editing multiple items in an index view
1116 -F "data=@hello" \ 1848 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1117 https://tracker.example.com/demo/rest/data/file/30/content 1849
1118 1850 To edit the status of all items in the item index view, edit the
1119 where ``hello`` is a file on local disk. 1851 ``issue.index.html``:
1120 1852
1121 You can enforce immutability in your tracker by adding an auditor (see 1853 1. add a form around the listing table (separate from the existing
1122 detectors_) for the file/msg class that rejects changes to the content 1854 index-page form), so at the top it reads::
1123 property. The auditor could also add a journal entry so that a change 1855
1124 via the Roundup mechanism is reported. Using a mixin (see: 1856 <form method="POST" tal:attributes="action request/classname">
1125 https://wiki.roundup-tracker.org/MixinClassFileClass) to augment the 1857 <table class="list">
1126 file class allows for other possibilities including signing the file, or 1858
1127 recording a checksum in the database and validating the file contents 1859 and at the bottom of that table::
1128 at the time it gets read. This allows detection of changes done on the 1860
1129 filesystem outside of the Roundup mechanism. 1861 </table>
1130 1862 </form
1131 .. index:: triple: schema; class property; messages 1863
1132 triple: schema; class property; files 1864 making sure you match the ``</table>`` from the list table, not the
1133 triple: schema; class property; nosy 1865 navigation table or the subsequent form table.
1134 triple: schema; class property; superseder 1866
1135 1867 2. in the display for the issue property, change::
1136 IssueClass 1868
1137 ~~~~~~~~~~ 1869 <td tal:condition="request/show/status"
1138 1870 tal:content="python:i.status.plain() or default">&nbsp;</td>
1139 IssueClasses automatically include the "messages", "files", "nosy", and 1871
1140 "superseder" properties. 1872 to::
1141 1873
1142 The messages and files properties list the links to the messages and 1874 <td tal:condition="request/show/status"
1143 files related to the issue. The nosy property is a list of links to 1875 tal:content="structure i/status/field">&nbsp;</td>
1144 users who wish to be informed of changes to the issue - they get "CC'ed" 1876
1145 e-mails when messages are sent to or generated by the issue. The nosy 1877 this will result in an edit field for the status property.
1146 reactor (in the ``'detectors'`` directory) handles this action. The 1878
1147 superseder link indicates an issue which has superseded this one. 1879 3. after the ``tal:block`` which lists the index items (marked by
1148 1880 ``tal:repeat="i batch"``) add a new table row::
1149 They also have the dynamically generated "creation", "activity" and 1881
1150 "creator" properties. 1882 <tr>
1151 1883 <td tal:attributes="colspan python:len(request.columns)">
1152 The value of the "creation" property is the date when an item was 1884 <input name="@csrf" type="hidden"
1153 created, and the value of the "activity" property is the date when any 1885 tal:attributes="value python:utils.anti_csrf_nonce()">
1154 property on the item was last edited (equivalently, these are the dates 1886 <input type="submit" value=" Save Changes ">
1155 on the first and last records in the item's journal). The "creator" 1887 <input type="hidden" name="@action" value="edit">
1156 property holds a link to the user that created the issue. 1888 <tal:block replace="structure request/indexargs_form" />
1157 1889 </td>
1158 .. index: triple: schema; class method; setkey 1890 </tr>
1159 1891
1160 setkey(property) 1892 which gives us a submit button, indicates that we are performing an
1161 ~~~~~~~~~~~~~~~~ 1893 edit on any changed statuses, and provides a defense against cross
1162 1894 site request forgery attacks.
1163 .. index:: roundup-admin; setting assignedto on an issue 1895
1164 1896 The final ``tal:block`` will make sure that the current index view
1165 Select a String property of the class to be the key property. The key 1897 parameters (filtering, columns, etc) will be used in rendering the
1166 property must be unique, and allows references to the items in the class 1898 next page (the results of the editing).
1167 by the content of the key property. That is, we can refer to users by 1899
1168 their username: for example, let's say that there's an issue in Roundup, 1900
1169 issue 23. There's also a user, richard, who happens to be user 2. To 1901 Displaying only message summaries in the issue display
1170 assign an issue to him, we could do either of:: 1902 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1171 1903
1172 roundup-admin set issue23 assignedto=2 1904 Alter the ``issue.item`` template section for messages to::
1173 1905
1174 or:: 1906 <table class="messages" tal:condition="context/messages">
1175 1907 <tr><th colspan="5" class="header">Messages</th></tr>
1176 roundup-admin set issue23 assignedto=richard 1908 <tr tal:repeat="msg context/messages">
1177 1909 <td><a tal:attributes="href string:msg${msg/id}"
1178 Note, the same thing can be done in the web and e-mail interfaces. 1910 tal:content="string:msg${msg/id}"></a></td>
1179 1911 <td tal:content="msg/author">author</td>
1180 .. index: triple: schema; class method; setlabelprop 1912 <td class="date" tal:content="msg/date/pretty">date</td>
1181 1913 <td tal:content="msg/summary">summary</td>
1182 setlabelprop(property) 1914 <td>
1183 ~~~~~~~~~~~~~~~~~~~~~~ 1915 <a tal:attributes="href string:?@remove@messages=${msg/id}&@action=edit">
1184 1916 remove</a>
1185 Select a property of the class to be the label property. The label 1917 </td>
1186 property is used whereever an item should be uniquely identified, e.g., 1918 </tr>
1187 when displaying a link to an item. If setlabelprop is not specified for 1919 </table>
1188 a class, the following values are tried for the label: 1920
1189 1921
1190 * the key of the class (see the `setkey(property)`_ section above) 1922 Enabling display of either message summaries or the entire messages
1191 * the "name" property 1923 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1192 * the "title" property 1924
1193 * the first property from the sorted property name list 1925 This is pretty simple - all we need to do is copy the code from the
1194 1926 example `displaying only message summaries in the issue display`_ into
1195 So in most cases you can get away without specifying setlabelprop 1927 our template alongside the summary display, and then introduce a switch
1196 explicitly. 1928 that shows either the one or the other. We'll use a new form variable,
1197 1929 ``@whole_messages`` to achieve this::
1198 You should make sure that users have View access to this property or 1930
1199 the id property for a class. If the property can not be viewed by a 1931 <table class="messages" tal:condition="context/messages">
1200 user, looping over items in the class (e.g. messages attached to an 1932 <tal:block tal:condition="not:request/form/@whole_messages/value | python:0">
1201 issue) will not work. 1933 <tr><th colspan="3" class="header">Messages</th>
1202 1934 <th colspan="2" class="header">
1203 .. index: triple: schema; class method; setorderprop 1935 <a href="?@whole_messages=yes">show entire messages</a>
1204 1936 </th>
1205 setorderprop(property) 1937 </tr>
1206 ~~~~~~~~~~~~~~~~~~~~~~ 1938 <tr tal:repeat="msg context/messages">
1207 1939 <td><a tal:attributes="href string:msg${msg/id}"
1208 Select a property of the class to be the order property. The order 1940 tal:content="string:msg${msg/id}"></a></td>
1209 property is used whenever using a default sort order for the class, 1941 <td tal:content="msg/author">author</td>
1210 e.g., when grouping or sorting class A by a link to class B in the user 1942 <td class="date" tal:content="msg/date/pretty">date</td>
1211 interface, the order property of class B is used for sorting. If 1943 <td tal:content="msg/summary">summary</td>
1212 setorderprop is not specified for a class, the following values are tried 1944 <td>
1213 for the order property: 1945 <a tal:attributes="href string:?@remove@messages=${msg/id}&@action=edit">remove</a>
1214 1946 </td>
1215 * the property named "order" 1947 </tr>
1216 * the label property (see `setlabelprop(property)`_ above) 1948 </tal:block>
1217 1949
1218 So in most cases you can get away without specifying setorderprop 1950 <tal:block tal:condition="request/form/@whole_messages/value | python:0">
1219 explicitly. 1951 <tr><th colspan="2" class="header">Messages</th>
1220 1952 <th class="header">
1221 .. index: triple: schema; class method; create 1953 <a href="?@whole_messages=">show only summaries</a>
1222 1954 </th>
1223 create(information) 1955 </tr>
1224 ~~~~~~~~~~~~~~~~~~~ 1956 <tal:block tal:repeat="msg context/messages">
1225 1957 <tr>
1226 Create an item in the database. This is generally used to create items 1958 <th tal:content="msg/author">author</th>
1227 in the "definitional" classes like "priority" and "status". 1959 <th class="date" tal:content="msg/date/pretty">date</th>
1228 1960 <th style="text-align: right">
1229 .. index: schema; item ordering 1961 (<a tal:attributes="href string:?@remove@messages=${msg/id}&@action=edit">remove</a>)
1230 1962 </th>
1231 A note about ordering 1963 </tr>
1232 ~~~~~~~~~~~~~~~~~~~~~ 1964 <tr><td colspan="3" tal:content="msg/content"></td></tr>
1233 1965 </tal:block>
1234 When we sort items in the hyperdb, we use one of a number of methods, 1966 </tal:block>
1235 depending on the properties being sorted on: 1967 </table>
1236 1968
1237 1. If it's a String, Integer, Number, Date or Interval property, we 1969
1238 just sort the scalar value of the property. Strings are sorted 1970 Setting up a "wizard" (or "druid") for controlled adding of issues
1239 case-sensitively. 1971 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1240 2. If it's a Link property, we sort by either the linked item's "order" 1972
1241 property (if it has one) or the linked item's "id". 1973 1. Set up the page templates you wish to use for data input. My wizard
1242 3. Mulitlinks sort similar to #2, but we start with the first Multilink 1974 is going to be a two-step process: first figuring out what category
1243 list item, and if they're the same, we sort by the second item, and 1975 of issue the user is submitting, and then getting details specific to
1244 so on. 1976 that category. The first page includes a table of help, explaining
1245 1977 what the category names mean, and then the core of the form::
1246 Note that if an "order" property is defined on a Class that is used for 1978
1247 sorting, all items of that Class *must* have a value against the "order" 1979 <form method="POST" onSubmit="return submit_once()"
1248 property, or sorting will result in random ordering. 1980 enctype="multipart/form-data">
1249 1981 <input name="@csrf" type="hidden"
1250 1982 tal:attributes="value python:utils.anti_csrf_nonce()">
1251 Examples of adding to your schema 1983 <input type="hidden" name="@template" value="add_page1">
1252 --------------------------------- 1984 <input type="hidden" name="@action" value="page1_submit">
1253 1985
1254 Some examples are in the :ref:`CustomExamples` section below. 1986 <strong>Category:</strong>
1255 1987 <tal:block tal:replace="structure context/category/menu" />
1256 Also you can start with `Roundup wiki CategorySchema`_ to see a list 1988 <input type="submit" value="Continue">
1257 of additional examples of how schemas can be customised to add new 1989 </form>
1258 functionality. 1990
1259 1991 The next page has the usual issue entry information, with the
1260 .. _Roundup wiki CategorySchema: 1992 addition of the following form fragments::
1261 https://wiki.roundup-tracker.org/CategorySchema 1993
1262 1994 <form method="POST" onSubmit="return submit_once()"
1263 .. index:: !detectors 1995 enctype="multipart/form-data"
1264 .. _detectors: 1996 tal:condition="context/is_edit_ok"
1265 .. _Auditors and reactors: 1997 tal:define="cat request/form/category/value">
1266 1998
1267 Detectors - adding behaviour to your tracker 1999 <input name="@csrf" type="hidden"
1268 ============================================ 2000 tal:attributes="value python:utils.anti_csrf_nonce()">
1269 2001 <input type="hidden" name="@template" value="add_page2">
1270 Detectors are initialised every time you open your tracker database, so 2002 <input type="hidden" name="@required" value="title">
1271 you're free to add and remove them any time, even after the database is 2003 <input type="hidden" name="category" tal:attributes="value cat">
1272 initialised via the ``roundup-admin initialise`` command. 2004 .
1273 2005 .
1274 The detectors in your tracker fire *before* (**auditors**) and *after* 2006 .
1275 (**reactors**) changes to the contents of your database. They are Python 2007 </form>
1276 modules that sit in your tracker's ``detectors`` directory. You will 2008
1277 have some installed by default - have a look. You can write new 2009 Note that later in the form, I use the value of "cat" to decide which
1278 detectors or modify the existing ones. The existing detectors installed 2010 form elements should be displayed. For example::
1279 for you are: 2011
1280 2012 <tal:block tal:condition="python:cat in '6 10 13 14 15 16 17'.split()">
1281 .. index:: detectors; installed 2013 <tr>
1282 2014 <th>Operating System</th>
1283 **nosyreaction.py** 2015 <td tal:content="structure context/os/field"></td>
1284 This provides the automatic nosy list maintenance and email sending. 2016 </tr>
1285 The nosy reactor (``nosyreaction``) fires when new messages are added 2017 <tr>
1286 to issues. The nosy auditor (``updatenosy``) fires when issues are 2018 <th>Web Browser</th>
1287 changed, and figures out what changes need to be made to the nosy list 2019 <td tal:content="structure context/browser/field"></td>
1288 (such as adding new authors, etc.) 2020 </tr>
1289 **statusauditor.py** 2021 </tal:block>
1290 This provides the ``chatty`` auditor which changes the issue status 2022
1291 from ``unread`` or ``closed`` to ``chatting`` if new messages appear. 2023 ... the above section will only be displayed if the category is one
1292 It also provides the ``presetunread`` auditor which pre-sets the 2024 of 6, 10, 13, 14, 15, 16 or 17.
1293 status to ``unread`` on new items if the status isn't explicitly 2025
1294 defined. 2026 3. Determine what actions need to be taken between the pages - these are
1295 **messagesummary.py** 2027 usually to validate user choices and determine what page is next. Now encode
1296 Generates the ``summary`` property for new messages based on the message 2028 those actions in a new ``Action`` class (see
1297 content. 2029 `defining new web actions <reference.html#defining-new-web-actions>`_)::
1298 **userauditor.py** 2030
1299 Verifies the content of some of the user fields (email addresses and 2031 from roundup.cgi.actions import Action
1300 roles lists). 2032
1301 2033 class Page1SubmitAction(Action):
1302 If you don't want this default behaviour, you're completely free to change 2034 def handle(self):
1303 or remove these detectors. 2035 ''' Verify that the user has selected a category, and then move
1304 2036 on to page 2.
1305 See the detectors section in the `design document`__ for details of the 2037 '''
1306 interface for detectors. 2038 category = self.form['category'].value
1307 2039 if category == '-1':
1308 __ design.html 2040 self.client.add_error_message('You must select a category of report')
1309 2041 return
1310 2042 # everything's ok, move on to the next page
1311 .. index:: detectors; writing api 2043 self.client.template = 'add_page2'
1312 2044
1313 Detector API 2045 def init(instance):
1314 ------------ 2046 instance.registerAction('page1_submit', Page1SubmitAction)
1315 2047
1316 .. index:: pair: detectors; auditors 2048 4. Use the usual "new" action as the ``@action`` on the final page, and
1317 single: auditors; function signature 2049 you're done (the standard context/submit method can do this for you).
1318 single: auditors; defining 2050
1319 single: auditors; arguments 2051
1320 2052 Silent Submit
1321 Auditors are called with the arguments:: 2053 ~~~~~~~~~~~~~
1322 2054
1323 audit(db, cl, itemid, newdata) 2055 When working on an issue, most of the time the people on the nosy list
1324 2056 need to be notified of changes. There are cases where a user wants to
1325 where ``db`` is the database, ``cl`` is an instance of Class or 2057 add a comment to an issue and not bother other users on the nosy
1326 IssueClass within the database, and ``newdata`` is a dictionary mapping 2058 list.
1327 property names to values. 2059 This feature is called Silent Submit because it allows the user to
1328 2060 silently modify an issue and not tell anyone.
1329 For a ``create()`` operation, the ``itemid`` argument is None and 2061
1330 newdata contains all of the initial property values with which the item 2062 There are several parts to this change. The main activity part
1331 is about to be created. 2063 involves editing the stock detectors/nosyreaction.py file in your
1332 2064 tracker. Insert the following lines near the top of the nosyreaction
1333 For a ``set()`` operation, newdata contains only the names and values of 2065 function::
1334 properties that are about to be changed. 2066
1335 2067 # Did user click button to do a silent change?
1336 For a ``retire()`` or ``restore()`` operation, newdata is None. 2068 try:
1337 2069 if db.web['submit'] == "silent_change":
1338 .. index:: pair: detectors; reactor 2070 return
1339 single: reactors; function signature 2071 except (AttributeError, KeyError) as err:
1340 single: reactors; defining 2072 # The web attribute or submit key don't exist.
1341 single: reactors; arguments 2073 # That's fine. We were probably triggered by an email
1342 2074 # or cli based change.
1343 Reactors are called with the arguments:: 2075 pass
1344 2076
1345 react(db, cl, itemid, olddata) 2077 This checks the submit button to see if it is the silent type. If there
1346 2078 are exceptions trying to make that determination they are ignored and
1347 where ``db`` is the database, ``cl`` is an instance of Class or 2079 processing continues. You may wonder how db.web gets set. This is done
1348 IssueClass within the database, and ``olddata`` is a dictionary mapping 2080 by creating an extension. Add the file extensions/edit.py with
1349 property names to values. 2081 this content::
1350 2082
1351 For a ``create()`` operation, the ``itemid`` argument is the id of the 2083 from roundup.cgi.actions import EditItemAction
1352 newly-created item and ``olddata`` is None. 2084
1353 2085 class Edit2Action(EditItemAction):
1354 For a ``set()`` operation, ``olddata`` contains the names and previous 2086 def handle(self):
1355 values of properties that were changed. 2087 self.db.web = {} # create the dict
1356 2088 # populate the dict by getting the value of the submit_button
1357 For a ``retire()`` or ``restore()`` operation, ``itemid`` is the id of 2089 # element from the form.
1358 the retired or restored item and ``olddata`` is None. 2090 self.db.web['submit'] = self.form['submit_button'].value
1359 2091
1360 .. index:: detectors; additional 2092 # call the core EditItemAction to process the edit.
1361 2093 EditItemAction.handle(self)
1362 Additional Detectors Ready For Use 2094
1363 ---------------------------------- 2095 def init(instance):
1364 2096 '''Override the default edit action with this new version'''
1365 Sample additional detectors that have been found useful will appear in 2097 instance.registerAction('edit', Edit2Action)
1366 the ``'detectors'`` directory of the Roundup distribution. If you want 2098
1367 to use one, copy it to the ``'detectors'`` of your tracker instance: 2099 This code is a wrapper for the Roundup EditItemAction. It checks the
1368 2100 form's submit button to save the value element. The rest of the changes
1369 **irker.py** 2101 needed for the Silent Submit feature involves editing
1370 This detector sends notification on IRC through an irker daemon 2102 html/issue.item.html to add the silent submit button. In
1371 (http://www.catb.org/esr/irker/) when issues are created or messages 2103 the stock issue.item.html the submit button is on a line that contains
1372 are added. In order to use it you need to install irker, start the 2104 "submit button". Replace that line with something like the following::
1373 irkerd daemon, and add an ``[irker]`` section in ``detectors/config.ini`` 2105
1374 that contains a comma-separated list of channels where the messages should 2106 <input type="submit" name="submit_button"
1375 be sent, e.g. ``channels = irc://chat.freenode.net/channelname``. 2107 tal:condition="context/is_edit_ok"
1376 **newissuecopy.py** 2108 value="Submit Changes">&nbsp;
1377 This detector sends an email to a team address whenever a new issue is 2109 <button type="submit" name="submit_button"
1378 created. The address is hard-coded into the detector, so edit it 2110 tal:condition="context/is_edit_ok"
1379 before you use it (look for the text 'team@team.host') or you'll get 2111 title="Click this to submit but not send nosy email."
1380 email errors! 2112 value="silent_change" i18n:translate="">
1381 **creator_resolution.py** 2113 Silent Change</button>
1382 Catch attempts to set the status to "resolved" - if the assignedto 2114
1383 user isn't the creator, then set the status to "confirm-done". Note that 2115 Note the difference in the value attribute for the two submit buttons.
1384 "classic" Roundup doesn't have that status, so you'll have to add it. If 2116 The value "silent_change" in the button specification must match the
1385 you don't want to though, it'll just use "in-progress" instead. 2117 string in the nosy reaction function.
1386 **email_auditor.py** 2118
1387 If a file added to an issue is of type message/rfc822, we tack on the 2119 Changing How the Core Code Works
1388 extension .eml. 2120 ================================
1389 The reason for this is that Microsoft Internet Explorer will not open
1390 things with a .eml attachment, as they deem it 'unsafe'. Worse yet,
1391 they'll just give you an incomprehensible error message. For more
1392 information, see the detector code - it has a lengthy explanation.
1393
1394
1395 .. index:: auditors; rules for use
1396 single: reactors; rules for use
1397
1398 Auditor or Reactor?
1399 -------------------
1400
1401 Generally speaking, the following rules should be observed:
1402
1403 **Auditors**
1404 Are used for `vetoing creation of or changes to items`_. They might
1405 also make automatic changes to item properties.
1406 **Reactors**
1407 Detect changes in the database and react accordingly. They should avoid
1408 making changes to the database where possible, as this could create
1409 detector loops.
1410
1411
1412 Vetoing creation of or changes to items
1413 ---------------------------------------
1414
1415 Auditors may raise the ``Reject`` exception to prevent the creation of
1416 or changes to items in the database. The mail gateway, for example, will
1417 not attach files or messages to issues when the creation of those files or
1418 messages are prevented through the ``Reject`` exception. It'll also not create
1419 users if that creation is ``Reject``'ed too.
1420
1421 To use, simply add at the top of your auditor::
1422
1423 from roundup.exceptions import Reject
1424
1425 And then when your rejection criteria have been detected, simply::
1426
1427 raise Reject('Description of error')
1428
1429 Error messages raised with ``Reject`` automatically have any HTML content
1430 escaped before being displayed to the user. To display an error message to the
1431 user without performing any HTML escaping the ``RejectRaw`` should be used. All
1432 security implications should be carefully considering before using
1433 ``RejectRaw``.
1434
1435
1436 Generating email from Roundup
1437 -----------------------------
1438
1439 The module ``roundup.mailer`` contains most of the nuts-n-bolts required
1440 to generate email messages from Roundup.
1441
1442 In addition, the ``IssueClass`` methods ``nosymessage()`` and
1443 ``send_message()`` are used to generate nosy messages, and may generate
1444 messages which only consist of a change note (ie. the message id parameter
1445 is not required - this is referred to as a "System Message" because it
1446 comes from "the system" and not a user).
1447
1448
1449 .. index:: extensions
1450 .. index:: extensions; add python functions to tracker
1451 .. _extensions:
1452 .. _actions:
1453
1454 Extensions - adding capabilities to your tracker
1455 ================================================
1456
1457 While detectors_ add new behavior by reacting to changes in tracked
1458 objects, `extensions` add new actions and utilities to Roundup, which
1459 are mostly used to enhance web interface.
1460
1461 You can create an extension by creating Python file in your tracker
1462 ``extensions`` directory. All files from this dir are loaded when
1463 tracker instance is created, at which point it calls ``init(instance)``
1464 from each file supplying itself as a first argument.
1465
1466 Note that at this point web interface is not loaded, but extensions still
1467 can register actions for in tracker instance. This may be fixed in
1468 Roundup 1.6 by introducing ``init_web(client)`` callback or a more
1469 flexible extension point mechanism.
1470
1471 * ``instance.registerUtil`` is used for adding `templating utilities`_
1472 (see `adding a time log to your issues`_ for an example)
1473
1474 * ``instance.registerAction`` is used to add more actions to instance
1475 and to web interface. See `Defining new web actions`_ for details.
1476 Generic action can be added by inheriting from ``action.Action``
1477 instead of ``cgi.action.Action``.
1478
1479 .. _interfaces.py:
1480 .. _modifying the core of Roundup:
1481
1482 interfaces.py - hooking into the core of Roundup
1483 ================================================
1484
1485 There is a magic trick for hooking into the core of Roundup. Using
1486 this you can:
1487
1488 * modify class data structures
1489 * monkey patch core code to add new functionality
1490 * modify the email gateway
1491 * add new rest endpoints
1492
1493 but with great power comes great responsibility.
1494
1495 Interfaces.py has been around since the earliest releases of Roundup
1496 and used to be the main way to get a lot of customization done. In
1497 modern Roundup, the extensions_ mechanism is used, but there are
1498 places where interfaces.py is still useful. Note that the tracker
1499 directories are not on the Python system path when interfaces.py is
1500 evaluated. You need to add library directories explictly by
1501 modifying sys.path.
1502 2121
1503 Example: Changing Cache-Control headers 2122 Example: Changing Cache-Control headers
1504 --------------------------------------- 2123 ---------------------------------------
1505 2124
1506 The Client class in cgi/client.py has a lookup table that is used to 2125 The Client class in cgi/client.py has a lookup table that is used to
1742 -------------- 2361 --------------
1743 2362
1744 See the `rest interface documentation`_ for instructions on how to add 2363 See the `rest interface documentation`_ for instructions on how to add
1745 new rest endpoints or `change the rate limiting method`_ using interfaces.py. 2364 new rest endpoints or `change the rate limiting method`_ using interfaces.py.
1746 2365
1747 Database Content 2366 Examples on the Wiki
1748 ================ 2367 ====================
1749 2368
1750 .. note:: 2369 Even more examples of customization have been contributed by
1751 If you modify the content of definitional classes, you'll most 2370 users. They can be found on the `wiki
1752 likely need to edit the tracker `detectors`_ to reflect your changes. 2371 <https://wiki.roundup-tracker.org>`_.
1753
1754 Customisation of the special "definitional" classes (eg. status,
1755 priority, resolution, ...) may be done either before or after the
1756 tracker is initialised. The actual method of doing so is completely
1757 different in each case though, so be careful to use the right one.
1758
1759 **Changing content before tracker initialisation**
1760 Edit the initial_data.py module in your tracker to alter the items
1761 created using the ``create( ... )`` methods.
1762
1763 **Changing content after tracker initialisation**
1764 As the "admin" user, click on the "class list" link in the web
1765 interface to bring up a list of all database classes. Click on the
1766 name of the class you wish to change the content of.
1767
1768 You may also use the ``roundup-admin`` interface's create, set and
1769 retire methods to add, alter or remove items from the classes in
1770 question.
1771
1772 See "`adding a new field to the classic schema`_" for an example that
1773 requires database content changes.
1774
1775
1776 Security / Access Controls
1777 ==========================
1778
1779 A set of Permissions is built into the security module by default:
1780
1781 - Create (everything)
1782 - Edit (everything)
1783 - Search (everything) (used if View does not permit access)
1784 - View (everything)
1785 - Register (User class only)
1786
1787 These are assigned to the "Admin" Role by default, and allow a user to do
1788 anything. Every Class you define in your `tracker schema`_ also gets an
1789 Create, Edit and View Permission of its own. The web and email interfaces
1790 also define:
1791
1792 *Email Access*
1793 If defined, the user may use the email interface. Used by default to deny
1794 Anonymous users access to the email interface. When granted to the
1795 Anonymous user, they will be automatically registered by the email
1796 interface (see also the ``new_email_user_roles`` configuration option).
1797 *Web Access*
1798 If defined, the user may use the web interface. All users are able to see
1799 the login form, regardless of this setting (thus enabling logging in).
1800 *Web Roles*
1801 Controls user access to editing the "roles" property of the "user" class.
1802 TODO: deprecate in favour of a property-based control.
1803 *Rest Access* and *Xmlrpc Access*
1804 These control access to the Rest and Xmlrpc endpoints. The Admin and User
1805 roles have these by default in the classic tracker. See the
1806 `directions in the rest interface documentation`_ and the
1807 `xmlrpc interface documentation`_.
1808
1809 These are hooked into the default Roles:
1810
1811 - Admin (Create, Edit, Search, View and everything; Web Roles)
1812 - User (Web Access; Email Access)
1813 - Anonymous (Web Access)
1814
1815 And finally, the "admin" user gets the "Admin" Role, and the "anonymous"
1816 user gets "Anonymous" assigned when the tracker is installed.
1817
1818 For the "User" Role, the "classic" tracker defines:
1819
1820 - Create, Edit and View issue, file, msg, query, keyword
1821 - View priority, status
1822 - View user
1823 - Edit their own user record
1824
1825 And the "Anonymous" Role is defined as:
1826
1827 - Web interface access
1828 - Register user (for registration)
1829 - View issue, file, msg, query, keyword, priority, status
1830
1831 Put together, these settings appear in the tracker's ``schema.py`` file::
1832
1833 #
1834 # TRACKER SECURITY SETTINGS
1835 #
1836 # See the configuration and customisation document for information
1837 # about security setup.
1838
1839 #
1840 # REGULAR USERS
1841 #
1842 # Give the regular users access to the web and email interface
1843 db.security.addPermissionToRole('User', 'Web Access')
1844 db.security.addPermissionToRole('User', 'Email Access')
1845 db.security.addPermissionToRole('User', 'Rest Access')
1846 db.security.addPermissionToRole('User', 'Xmlrpc Access')
1847
1848 # Assign the access and edit Permissions for issue, file and message
1849 # to regular users now
1850 for cl in 'issue', 'file', 'msg', 'keyword':
1851 db.security.addPermissionToRole('User', 'View', cl)
1852 db.security.addPermissionToRole('User', 'Edit', cl)
1853 db.security.addPermissionToRole('User', 'Create', cl)
1854 for cl in 'priority', 'status':
1855 db.security.addPermissionToRole('User', 'View', cl)
1856
1857 # May users view other user information? Comment these lines out
1858 # if you don't want them to
1859 p = db.security.addPermission(name='View', klass='user',
1860 properties=('id', 'organisation', 'phone', 'realname', 'timezone',
1861 'username'))
1862 db.security.addPermissionToRole('User', p)
1863
1864 # Users should be able to edit their own details -- this permission is
1865 # limited to only the situation where the Viewed or Edited item is their own.
1866 def own_record(db, userid, itemid, **ctx):
1867 '''Determine whether the userid matches the item being accessed.'''
1868 return userid == itemid
1869 p = db.security.addPermission(name='View', klass='user', check=own_record,
1870 description="User is allowed to view their own user details")
1871 db.security.addPermissionToRole('User', p)
1872 p = db.security.addPermission(name='Edit', klass='user', check=own_record,
1873 properties=('username', 'password', 'address', 'realname', 'phone',
1874 'organisation', 'alternate_addresses', 'queries', 'timezone'),
1875 description="User is allowed to edit their own user details")
1876 db.security.addPermissionToRole('User', p)
1877
1878 # Users should be able to edit and view their own queries. They should also
1879 # be able to view any marked as not private. They should not be able to
1880 # edit others' queries, even if they're not private
1881 def view_query(db, userid, itemid):
1882 private_for = db.query.get(itemid, 'private_for')
1883 if not private_for: return True
1884 return userid == private_for
1885 def edit_query(db, userid, itemid):
1886 return userid == db.query.get(itemid, 'creator')
1887 p = db.security.addPermission(name='View', klass='query', check=view_query,
1888 description="User is allowed to view their own and public queries")
1889 db.security.addPermissionToRole('User', p)
1890 p = db.security.addPermission(name='Search', klass='query')
1891 db.security.addPermissionToRole('User', p)
1892 p = db.security.addPermission(name='Edit', klass='query', check=edit_query,
1893 description="User is allowed to edit their queries")
1894 db.security.addPermissionToRole('User', p)
1895 p = db.security.addPermission(name='Retire', klass='query', check=edit_query,
1896 description="User is allowed to retire their queries")
1897 db.security.addPermissionToRole('User', p)
1898 p = db.security.addPermission(name='Restore', klass='query', check=edit_query,
1899 description="User is allowed to restore their queries")
1900 db.security.addPermissionToRole('User', p)
1901 p = db.security.addPermission(name='Create', klass='query',
1902 description="User is allowed to create queries")
1903 db.security.addPermissionToRole('User', p)
1904
1905 #
1906 # ANONYMOUS USER PERMISSIONS
1907 #
1908 # Let anonymous users access the web interface. Note that almost all
1909 # trackers will need this Permission. The only situation where it's not
1910 # required is in a tracker that uses an HTTP Basic Authenticated front-end.
1911 db.security.addPermissionToRole('Anonymous', 'Web Access')
1912
1913 # Let anonymous users access the email interface (note that this implies
1914 # that they will be registered automatically, hence they will need the
1915 # "Create" user Permission below)
1916 # This is disabled by default to stop spam from auto-registering users on
1917 # public trackers.
1918 #db.security.addPermissionToRole('Anonymous', 'Email Access')
1919
1920 # Assign the appropriate permissions to the anonymous user's Anonymous
1921 # Role. Choices here are:
1922 # - Allow anonymous users to register
1923 db.security.addPermissionToRole('Anonymous', 'Register', 'user')
1924
1925 # Allow anonymous users access to view issues (and the related, linked
1926 # information)
1927 for cl in 'issue', 'file', 'msg', 'keyword', 'priority', 'status':
1928 db.security.addPermissionToRole('Anonymous', 'View', cl)
1929
1930 # Allow the anonymous user to use the "Show Unassigned" search.
1931 # It acts like "Show Open" if this permission is not available.
1932 # If you are running a tracker that does not allow read access for
1933 # anonymous, you should remove this entry as it can be used to perform
1934 # a username guessing attack against a roundup install.
1935 p = db.security.addPermission(name='Search', klass='user')
1936 db.security.addPermissionToRole ('Anonymous', p)
1937
1938 # [OPTIONAL]
1939 # Allow anonymous users access to create or edit "issue" items (and the
1940 # related file and message items)
1941 #for cl in 'issue', 'file', 'msg':
1942 # db.security.addPermissionToRole('Anonymous', 'Create', cl)
1943 # db.security.addPermissionToRole('Anonymous', 'Edit', cl)
1944
1945 .. index::
1946 single: roundup-admin; view class permissions
1947
1948 You can use ``roundup-admin security`` to verify the permissions
1949 defined in the schema. It also verifies that properties specified in
1950 permissions are valid for the class. This helps detect typos that can
1951 cause baffling permission issues.
1952
1953 Automatic Permission Checks
1954 ---------------------------
1955
1956 Permissions are automatically checked when information is rendered
1957 through the web. This includes:
1958
1959 1. View checks for properties when being rendered via the ``plain()`` or
1960 similar methods. If the check fails, the text "[hidden]" will be
1961 displayed.
1962 2. Edit checks for properties when the edit field is being rendered via
1963 the ``field()`` or similar methods. If the check fails, the property
1964 will be rendered via the ``plain()`` method (see point 1. for subsequent
1965 checking performed)
1966 3. View checks are performed in index pages for each item being displayed
1967 such that if the user does not have permission, the row is not rendered.
1968 4. View checks are performed at the top of item pages for the Item being
1969 displayed. If the user does not have permission, the text "You are not
1970 allowed to view this page." will be displayed.
1971 5. View checks are performed at the top of index pages for the Class being
1972 displayed. If the user does not have permission, the text "You are not
1973 allowed to view this page." will be displayed.
1974
1975
1976 New User Roles
1977 --------------
1978
1979 New users are assigned the Roles defined in the config file as:
1980
1981 - NEW_WEB_USER_ROLES
1982 - NEW_EMAIL_USER_ROLES
1983
1984 The `users may only edit their issues`_ example shows customisation of
1985 these parameters.
1986
1987
1988 Changing Access Controls
1989 ------------------------
1990
1991 You may alter the configuration variables to change the Role that new
1992 web or email users get, for example to not give them access to the web
1993 interface if they register through email.
1994
1995 You may use the ``roundup-admin`` "``security``" command to display the
1996 current Role and Permission configuration in your tracker.
1997
1998
1999 Adding a new Permission
2000 ~~~~~~~~~~~~~~~~~~~~~~~
2001
2002 When adding a new Permission, you will need to:
2003
2004 1. add it to your tracker's ``schema.py`` so it is created, using
2005 ``security.addPermission``, for example::
2006
2007 db.security.addPermission(name="View", klass='frozzle',
2008 description="User is allowed to access frozzles")
2009
2010 will set up a new "View" permission on the Class "frozzle".
2011 2. enable it for the Roles that should have it (verify with
2012 "``roundup-admin security``")
2013 3. add it to the relevant HTML interface templates
2014 4. add it to the appropriate xxxPermission methods on in your tracker
2015 interfaces module
2016
2017 The ``addPermission`` method takes a few optional parameters:
2018
2019 **properties**
2020 A sequence of property names that are the only properties to apply the
2021 new Permission to (eg. ``... klass='user', properties=('name',
2022 'email') ...``)
2023 **props_only**
2024 A boolean value (set to false by default) that is a new feature
2025 in Roundup 1.6.
2026 A permission defined using:
2027
2028 ``properties=('list', 'of', 'property', 'names')``
2029
2030 is used to determine access for things other than just those
2031 properties. For example a check for View permission on the issue
2032 class or an issue item can use any View permission for the issue
2033 class even if that permission has a property list. This can be
2034 confusing and surprising as you would think that a permission
2035 including properties would be used only for determining the
2036 access permission for those properties.
2037
2038 ``roundup-admin security`` will report invalid properties for the
2039 class. For example a permission with an invalid summary property is
2040 presented as::
2041
2042 Allowed to see content of object regardless of spam status
2043 (View for "file": ('content', 'summary') only)
2044
2045 **Invalid properties for file: ['summary']
2046
2047 Setting ``props_only=True`` will make the permission valid only for
2048 those properties.
2049
2050 If you use a lot of permissions with property checks, it can be
2051 difficult to change all of them. Calling the function:
2052
2053 db.security.set_props_only_default(True)
2054
2055 at the top of ``schema.py`` will make every permission creation
2056 behave as though props_only was set to True. It is expected that the
2057 default of True will become the default in a future Roundup release.
2058 **check**
2059 A function to be executed which returns boolean determining whether
2060 the Permission is allowed. If it returns True, the permission is
2061 allowed, if it returns False the permission is denied. The function
2062 can have one of two signatures::
2063
2064 check(db, userid, itemid)
2065
2066 or::
2067
2068 check(db, userid, itemid, **ctx)
2069
2070 where ``db`` is a handle on the open database, ``userid`` is
2071 the user attempting access and ``itemid`` is the specific item being
2072 accessed. If the second form is used the ``ctx`` dictionary is
2073 defined with the following values::
2074
2075 ctx['property'] the name of the property being checked or None if
2076 it's a class check.
2077
2078 ctx['classname'] the name of the class that is being checked
2079 (issue, query ....).
2080
2081 ctx['permission'] the name of the permission (e.g. View, Edit...).
2082
2083 The second form is preferred as it makes it easier to implement more
2084 complex permission schemes. An example of the use of ``ctx`` can be
2085 found in the ``upgrading.txt`` or `upgrading.html`_ document.
2086
2087 .. _`upgrading.html`: upgrading.html
2088
2089 Example Scenarios
2090 ~~~~~~~~~~~~~~~~~
2091
2092 See the `examples`_ section for longer examples of customisation.
2093
2094 **anonymous access through the e-mail gateway**
2095 Give the "anonymous" user the "Email Access", ("Edit", "issue") and
2096 ("Create", "msg") Permissions but do not not give them the ("Create",
2097 "user") Permission. This means that when an unknown user sends email
2098 into the tracker, they're automatically logged in as "anonymous".
2099 Since they don't have the ("Create", "user") Permission, they won't
2100 be automatically registered, but since "anonymous" has permission to
2101 use the gateway, they'll still be able to submit issues. Note that
2102 the Sender information - their email address - will not be available
2103 - they're *anonymous*.
2104
2105 **automatic registration of users in the e-mail gateway**
2106 By giving the "anonymous" user the ("Register", "user") Permission, any
2107 unidentified user will automatically be registered with the tracker
2108 (with no password, so they won't be able to log in through
2109 the web until an admin sets their password). By default new Roundup
2110 trackers don't allow this as it opens them up to spam. It may be enabled
2111 by uncommenting the appropriate addPermissionToRole in your tracker's
2112 ``schema.py`` file. The new user is given the Roles list defined in the
2113 "new_email_user_roles" config variable.
2114
2115 **only developers may be assigned issues**
2116 Create a new Permission called "Fixer" for the "issue" class. Create a
2117 new Role "Developer" which has that Permission, and assign that to the
2118 appropriate users. Filter the list of users available in the assignedto
2119 list to include only those users. Enforce the Permission with an
2120 auditor. See the example
2121 `restricting the list of users that are assignable to a task`_.
2122
2123 **only managers may sign off issues as complete**
2124 Create a new Permission called "Closer" for the "issue" class. Create a
2125 new Role "Manager" which has that Permission, and assign that to the
2126 appropriate users. In your web interface, only display the "resolved"
2127 issue state option when the user has the "Closer" Permissions. Enforce
2128 the Permission with an auditor. This is very similar to the previous
2129 example, except that the web interface check would look like::
2130
2131 <option tal:condition="python:request.user.hasPermission('Closer')"
2132 value="resolved">Resolved</option>
2133
2134 **don't give web access to users who register through email**
2135 Create a new Role called "Email User" which has all the Permissions of
2136 the normal "User" Role minus the "Web Access" Permission. This will
2137 allow users to send in emails to the tracker, but not access the web
2138 interface.
2139
2140 **let some users edit the details of all users**
2141 Create a new Role called "User Admin" which has the Permission for
2142 editing users::
2143
2144 db.security.addRole(name='User Admin', description='Managing users')
2145 p = db.security.getPermission('Edit', 'user')
2146 db.security.addPermissionToRole('User Admin', p)
2147
2148 and assign the Role to the users who need the permission.
2149
2150
2151 Web Interface
2152 =============
2153
2154 .. contents::
2155 :local:
2156
2157 The web interface is provided by the ``roundup.cgi.client`` module and
2158 is used by ``roundup.cgi``, ``roundup-server`` and ``ZRoundup``
2159 (``ZRoundup`` is broken, until further notice). In all cases, we
2160 determine which tracker is being accessed (the first part of the URL
2161 path inside the scope of the CGI handler) and pass control on to the
2162 ``roundup.cgi.client.Client`` class - which handles the rest of the
2163 access through its ``main()`` method. This means that you can do pretty
2164 much anything you want as a web interface to your tracker.
2165
2166
2167
2168 Repercussions of changing the tracker schema
2169 ---------------------------------------------
2170
2171 If you choose to change the `tracker schema`_ you will need to ensure
2172 the web interface knows about it:
2173
2174 1. Index, item and search pages for the relevant classes may need to
2175 have properties added or removed,
2176 2. The "page" template may require links to be changed, as might the
2177 "home" page's content arguments.
2178
2179
2180 How requests are processed
2181 --------------------------
2182
2183 The basic processing of a web request proceeds as follows:
2184
2185 1. figure out who we are, defaulting to the "anonymous" user
2186 2. figure out what the request is for - we call this the "context"
2187 3. handle any requested action (item edit, search, ...)
2188 4. render the template requested by the context, resulting in HTML
2189 output
2190
2191 In some situations, exceptions occur:
2192
2193 - HTTP Redirect (generally raised by an action)
2194 - SendFile (generally raised by ``determine_context``)
2195 here we serve up a FileClass "content" property
2196 - SendStaticFile (generally raised by ``determine_context``)
2197 here we serve up a file from the tracker "html" directory
2198 - Unauthorised (generally raised by an action)
2199 here the action is cancelled, the request is rendered and an error
2200 message is displayed indicating that permission was not granted for
2201 the action to take place
2202 - NotFound (raised wherever it needs to be)
2203 this exception percolates up to the CGI interface that called the
2204 client
2205
2206
2207 Roundup URL design
2208 ------------------
2209
2210 Each tracker has several hardcoded URLs. These three are equivalent and
2211 lead to the main tracker page:
2212
2213 1. ``/``
2214 2. ``/index``
2215 3. ``/home``
2216
2217 The following prefix is used to access static resources:
2218
2219 4. ``/@@file/``
2220
2221 Two additional url's are used for the API's.
2222 The `REST api`_ is accessed via:
2223
2224 5. ``/rest/``
2225
2226 .. _`REST api`: rest.html
2227
2228 and the `XMLRPC api`_ is available at:
2229
2230 6. ``/xmlrpc``
2231
2232 .. _`XMLRPC api`: xmlrpc.html
2233
2234 All other URLs depend on the classes configured in Roundup database.
2235 Each class receives two URLs - one for the class itself and another
2236 for specific items of that class. Example for class URL:
2237
2238 7. ``/issue``
2239
2240 This is usually used to show listings of class items. The URL for
2241 for specific object of issue class with id 1 will look like:
2242
2243 8. ``/issue1``
2244
2245 .. _strip_zeros:
2246
2247 Note that a leading string of 0's will be stripped from the id part of
2248 the object designator in the URL. E.G. ``/issue001`` is the same as
2249 ``/issue1``. Similarly for ``/file01`` etc. However you should
2250 generate URL's without the extra zeros.
2251
2252 Determining web context
2253 -----------------------
2254
2255 To determine the "context" of a request (what request is for), we look at
2256 the URL path after the tracker root and at ``@template`` request
2257 parameter. Typical URL paths look like:
2258
2259 1. ``/tracker/issue``
2260 2. ``/tracker/issue1``
2261 3. ``/tracker/@@file/style.css``
2262 4. ``/cgi-bin/roundup.cgi/tracker/file1``
2263 5. ``/cgi-bin/roundup.cgi/tracker/file1/kitten.png``
2264
2265 where tracker root is ``/tracker/`` or ``/cgi-bin/roundup.cgi/tracker/``
2266 We're looking at "issue", "issue1", "@@file/style.css", "file1" and
2267 "file1/kitten.png" in the cases above.
2268
2269 1. with is no path we are in the "home" context. See `the "home"
2270 context`_ below for details. "index" or "home" paths may also be used
2271 to switch into "home" context.
2272 2. for paths starting with "@@file" the additional path entry ("style.css"
2273 in the example above) specifies the static file to be served
2274 from the tracker TEMPLATES directory (or STATIC_FILES, if configured).
2275 This is usually the tracker's "html" directory. Internally this works
2276 by raising SendStaticFile exception.
2277 3. if there is something in the path (as in example 1, "issue"), it
2278 identifies the tracker class to display.
2279 4. if the path is an item designator (as in examples 2 and 4, "issue1"
2280 and "file1"), then we're to display a specific item.
2281 :ref:`Note. <strip_zeros>`
2282 5. if the path starts with an item designator and is longer than one
2283 entry (as in example 5, "file1/kitten.png"), then we're assumed to be
2284 handling an item of a ``FileClass``, and the extra path information
2285 gives the filename that the client is going to label the download
2286 with (i.e. "file1/kitten.png" is nicer to download than "file1").
2287 This raises a ``SendFile`` exception.
2288
2289 Neither 2. or 5. use templates and stop before the template is
2290 determined. For other contexts the template used is specified by the
2291 ``@template`` variable, which defaults to:
2292
2293 - only classname supplied: "index"
2294 - full item designator supplied: "item"
2295
2296
2297 The "home" Context
2298 ------------------
2299
2300 The "home" context is special because it allows you to add templated
2301 pages to your tracker that don't rely on a class or item (ie. an issues
2302 list or specific issue).
2303
2304 Let's say you wish to add frames to control the layout of your tracker's
2305 interface. You'd probably have:
2306
2307 - A top-level frameset page. This page probably wouldn't be templated, so
2308 it could be served as a static file (see `serving static content`_)
2309 - A sidebar frame that is templated. Let's call this page
2310 "home.navigation.html" in your tracker's "html" directory. To load that
2311 page up, you use the URL:
2312
2313 <tracker url>/home?@template=navigation
2314
2315
2316 Serving static content
2317 ----------------------
2318
2319 See the previous section `determining web context`_ where it describes
2320 ``@@file`` paths.
2321
2322 These files are served without any permission checks. Any user on the
2323 internet with the url can download the file.
2324
2325 This is rarely an issue since the html templates are just source code
2326 and much of it can be found in the Roundup repository. Other
2327 decoration (logos, stylesheets) are similarly not security sensitive.
2328 You can use the static_files setting in config.ini to eliminate
2329 access to the templates directory if desired.
2330
2331 If a file resolves to a symbolic link, it is not served.
2332
2333 Performing actions in web requests
2334 ----------------------------------
2335
2336 When a user requests a web page, they may optionally also request for an
2337 action to take place. As described in `how requests are processed`_, the
2338 action is performed before the requested page is generated. Actions are
2339 triggered by using a ``@action`` CGI variable, where the value is one
2340 of:
2341
2342 **login**
2343 Attempt to log a user in.
2344
2345 **logout**
2346 Log the user out - make them "anonymous".
2347
2348 **register**
2349 Attempt to create a new user based on the contents of the form and then
2350 log them in.
2351
2352 **edit**
2353 Perform an edit of an item in the database. There are some `special form
2354 variables`_ you may use. Also you can set the ``__redirect_to`` form
2355 variable to the URL that should be displayed after the edit is succesfully
2356 completed. If you wanted to edit a sequence of issues, users etc. this
2357 could be used to display the next item in the sequence to the user.
2358
2359 **new**
2360 Add a new item to the database. You may use the same `special form
2361 variables`_ as in the "edit" action. Also you can set the
2362 ``__redirect_to`` form variable to the URL that should be displayed after
2363 the new item is created. This is useful if you want to create another
2364 item rather than edit the newly created item.
2365
2366 **retire**
2367 Retire the item in the database.
2368
2369 **editCSV**
2370 Performs an edit of all of a class' items in one go. See also the
2371 *class*.csv templating method which generates the CSV data to be
2372 edited, and the ``'_generic.index'`` template which uses both of these
2373 features.
2374
2375 **search**
2376 Mangle some of the form variables:
2377
2378 - Set the form ":filter" variable based on the values of the filter
2379 variables - if they're set to anything other than "dontcare" then add
2380 them to :filter.
2381
2382 - Also handle the ":queryname" variable and save off the query to the
2383 user's query list.
2384
2385 Each of the actions is implemented by a corresponding ``*XxxAction*`` (where
2386 "Xxx" is the name of the action) class in the ``roundup.cgi.actions`` module.
2387 These classes are registered with ``roundup.cgi.client.Client``. If you need
2388 to define new actions, you may add them there (see `defining new
2389 web actions`_).
2390
2391 Each action class also has a ``*permission*`` method which determines whether
2392 the action is permissible given the current user. The base permission checks
2393 for each action are:
2394
2395 **login**
2396 Determine whether the user has the "Web Access" Permission.
2397 **logout**
2398 No permission checks are made.
2399 **register**
2400 Determine whether the user has the ("Create", "user") Permission.
2401 **edit**
2402 Determine whether the user has permission to edit this item. If we're
2403 editing the "user" class, users are allowed to edit their own details -
2404 unless they try to edit the "roles" property, which requires the
2405 special Permission "Web Roles".
2406 **new**
2407 Determine whether the user has permission to create this item. No
2408 additional property checks are made. Additionally, new user items may
2409 be created if the user has the ("Create", "user") Permission.
2410 **editCSV**
2411 Determine whether the user has permission to edit this class.
2412 **search**
2413 Determine whether the user has permission to view this class.
2414
2415 Protecting users from web application attacks
2416 ---------------------------------------------
2417
2418 There is a class of attacks known as Cross Site Request Forgeries
2419 (CSRF). Malicious code running in the browser can making a
2420 request to Roundup while you are logged into Roundup. The
2421 malicious code piggy backs on your existing Roundup session to
2422 make changes without your knowledge. Roundup 1.6 has support for
2423 defending against this by analyzing the
2424
2425 * Referer,
2426 * Origin, and
2427 * Host or
2428 * X-Forwarded-Host
2429
2430 HTTP headers. It compares the headers to the value of the web setting
2431 in the [tracker] section of the tracker's ``config.ini``.
2432
2433 Also a per form token (also called a nonce) can be enabled for
2434 the tracker using the ``csrf_enforce_token`` option in
2435 config.ini. When enabled, Roundup will validate a hidden form
2436 field called ``@csrf``. If the validation fails (or the token
2437 is used more than once) the request is rejected. The ``@csrf``
2438 input field is added automatically by calling the ``submit``
2439 function/path. It can also be added manually by calling
2440 anti_csrf_nonce() directly. For example::
2441
2442 <input name="@csrf" type="hidden"
2443 tal:attributes="value python:utils.anti_csrf_nonce(lifetime=10)">
2444
2445 By default a nonce lifetime is 2 weeks. However the lifetime (in
2446 minutes) can be set by passing a lifetime argument as shown
2447 above. The example above makes the nonce lifetime 10 minutes.
2448
2449 Search for @csrf in this document for more examples. There are
2450 more examples and information in ``upgrading.txt``.
2451
2452 The token protects you because malicious code supplied by another
2453 site is unable to obtain the token. Thus many attempts they make
2454 to submit a request are rejected.
2455
2456 The protection on the xmlrpc interface is untested, but is based
2457 on a valid header check against the Roundup url and the presence
2458 of the ``X-REQUESTED-WITH`` header. Work to improve this is a
2459 future project after the 1.6 release.
2460
2461 The enforcement levels can be modified in ``config.ini``. Refer to
2462 that file for details.
2463
2464 Special form variables
2465 ----------------------
2466
2467 Item properties and their values are edited with html FORM
2468 variables and their values. You can:
2469
2470 - Change the value of some property of the current item.
2471 - Create a new item of any class, and edit the new item's
2472 properties,
2473 - Attach newly created items to a multilink property of the
2474 current item.
2475 - Remove items from a multilink property of the current item.
2476 - Specify that some properties are required for the edit
2477 operation to be successful.
2478 - Redirect to a different page after creating a new item (new action
2479 only, not edit action). Usually you end up on the page for the
2480 created item.
2481 - Set up user interface locale.
2482
2483 These operations will only take place if the form action (the
2484 ``@action`` variable) is "edit" or "new".
2485
2486 In the following, <bracketed> values are variable, "@" may be
2487 either ":" or "@", and other text "required" is fixed.
2488
2489 Two special form variables are used to specify user language preferences:
2490
2491 ``@language``
2492 value may be locale name or ``none``. If this variable is set to
2493 locale name, web interface language is changed to given value
2494 (provided that appropriate translation is available), the value
2495 is stored in the browser cookie and will be used for all following
2496 requests. If value is ``none`` the cookie is removed and the
2497 language is changed to the tracker default, set up in the tracker
2498 configuration or OS environment.
2499
2500 ``@charset``
2501 value may be character set name or ``none``. Character set name
2502 is stored in the browser cookie and sets output encoding for all
2503 HTML pages generated by Roundup. If value is ``none`` the cookie
2504 is removed and HTML output is reset to Roundup internal encoding
2505 (UTF-8).
2506
2507 Most properties are specified as form variables:
2508
2509 ``<propname>``
2510 property on the current context item
2511
2512 ``<designator>"@"<propname>``
2513 property on the indicated item (for editing related information)
2514
2515 Designators name a specific item of a class.
2516
2517 ``<classname><N>``
2518 Name an existing item of class <classname>.
2519
2520 ``<classname>"-"<N>``
2521 Name the <N>th new item of class <classname>. If the form
2522 submission is successful, a new item of <classname> is
2523 created. Within the submitted form, a particular
2524 designator of this form always refers to the same new
2525 item.
2526
2527 Once we have determined the "propname", we look at it to see
2528 if it's special:
2529
2530 ``@required``
2531 The associated form value is a comma-separated list of
2532 property names that must be specified when the form is
2533 submitted for the edit operation to succeed.
2534
2535 When the <designator> is missing, the properties are
2536 for the current context item. When <designator> is
2537 present, they are for the item specified by
2538 <designator>.
2539
2540 The "@required" specifier must come before any of the
2541 properties it refers to are assigned in the form.
2542
2543 ``@remove@<propname>=id(s)`` or ``@add@<propname>=id(s)``
2544 The "@add@" and "@remove@" edit actions apply only to
2545 Multilink properties. The form value must be a
2546 comma-separate list of keys for the class specified by
2547 the simple form variable. The listed items are added
2548 to (respectively, removed from) the specified
2549 property.
2550
2551 ``@link@<propname>=<designator>``
2552 If the edit action is "@link@", the simple form
2553 variable must specify a Link or Multilink property.
2554 The form value is a comma-separated list of
2555 designators. The item corresponding to each
2556 designator is linked to the property given by simple
2557 form variable.
2558
2559 None of the above (ie. just a simple form value)
2560 The value of the form variable is converted
2561 appropriately, depending on the type of the property.
2562
2563 For a Link('klass') property, the form value is a
2564 single key for 'klass', where the key field is
2565 specified in schema.py.
2566
2567 For a Multilink('klass') property, the form value is a
2568 comma-separated list of keys for 'klass', where the
2569 key field is specified in schema.py.
2570
2571 Note that for simple-form-variables specifiying Link
2572 and Multilink properties, the linked-to class must
2573 have a key field.
2574
2575 For a String() property specifying a filename, the
2576 file named by the form value is uploaded. This means we
2577 try to set additional properties "filename" and "type" (if
2578 they are valid for the class). Otherwise, the property
2579 is set to the form value.
2580
2581 For Date(), Interval(), Boolean(), Integer() and Number()
2582 properties, the form value is converted to the
2583 appropriate value.
2584
2585 Any of the form variables may be prefixed with a classname or
2586 designator.
2587
2588 Setting the form variable: ``__redirect_to=`` to a url when
2589 @action=new redirects the user to the specified url after successfully
2590 creating the new item. This is useful if you want the user to create
2591 another item rather than edit the newly created item. Note that the
2592 url assigned to ``__redirect_to`` must be url encoded/quoted and be
2593 under the tracker's base url. If the base_url uses http, you can set
2594 the url to https.
2595
2596 Two special form values are supported for backwards compatibility:
2597
2598 @note
2599 This is equivalent to::
2600
2601 @link@messages=msg-1
2602 msg-1@content=value
2603
2604 which is equivalent to the html::
2605
2606 <textarea name="msg-1@content"></textarea>
2607 <input type="hidden" name="@link@messages" value="msg-1">
2608
2609 except that in addition, the "author" and "date" properties of
2610 "msg-1" are set to the userid of the submitter, and the current
2611 time, respectively.
2612
2613 @file
2614 This is equivalent to::
2615
2616 @link@files=file-1
2617 file-1@content=value
2618
2619 by adding the HTML::
2620
2621 <input type="file" name="file-1@content">
2622 <input type="hidden" name="@link@files" value="file-1">
2623
2624 The String content value is handled as described above for file
2625 uploads.
2626
2627 If both the "@note" and "@file" form variables are
2628 specified, the action::
2629
2630 msg-1@link@files=file-1
2631
2632 is also performed. This would be expressed in HTML with::
2633
2634 <input type="hidden" name="msg-1@link@files" value="file-1">
2635
2636 We also check that FileClass items have a "content" property with
2637 actual content, otherwise we remove them from all_props before
2638 returning.
2639
2640
2641 Default templates
2642 -----------------
2643
2644 The default templates are html4 compliant. If you wish to change them to be
2645 xhtml compliant, you'll need to change the ``html_version`` configuration
2646 variable in ``config.ini`` to ``'xhtml'`` instead of ``'html4'``.
2647
2648 Most customisation of the web view can be done by modifying the
2649 templates in the tracker ``'html'`` directory. There are several types
2650 of files in there. The *minimal* template includes:
2651
2652 **page.html**
2653 This template usually defines the overall look of your tracker. When
2654 you view an issue, it appears inside this template. When you view an
2655 index, it also appears inside this template. This template defines a
2656 macro called "icing" which is used by almost all other templates as a
2657 coating for their content, using its "content" slot. It also defines
2658 the "head_title" and "body_title" slots to allow setting of the page
2659 title.
2660 **home.html**
2661 the default page displayed when no other page is indicated by the user
2662 **home.classlist.html**
2663 a special version of the default page that lists the classes in the
2664 tracker
2665 **classname.item.html**
2666 displays an item of the *classname* class
2667 **classname.index.html**
2668 displays a list of *classname* items
2669 **classname.search.html**
2670 displays a search page for *classname* items
2671 **_generic.index.html**
2672 used to display a list of items where there is no
2673 ``*classname*.index`` available
2674 **_generic.help.html**
2675 used to display a "class help" page where there is no
2676 ``*classname*.help``
2677 **user.register.html**
2678 a special page just for the user class, that renders the registration
2679 page
2680 **style.css**
2681 a static file that is served up as-is
2682
2683 The *classic* template has a number of additional templates.
2684
2685 Remember that you can create any template extension you want to,
2686 so if you just want to play around with the templating for new issues,
2687 you can copy the current "issue.item" template to "issue.test", and then
2688 access the test template using the "@template" URL argument::
2689
2690 http://your.tracker.example/tracker/issue?@template=test
2691
2692 and it won't affect your users using the "issue.item" template.
2693
2694 You can also put templates into a subdirectory of the template
2695 directory. So if you specify::
2696
2697 http://your.tracker.example/tracker/issue?@template=test/item
2698
2699 you will use the template at: ``test/issue.item.html``. If that
2700 template doesn't exit it will try to use
2701 ``test/_generic.item.html``. If that template doesn't exist
2702 it will return an error.
2703
2704 Implementing Modal Editing Using @template
2705 ------------------------------------------
2706
2707 Many item templates allow you to edit the item. They contain
2708 code that renders edit boxes if the user has edit permissions.
2709 Otherwise the template will just display the item information.
2710
2711 In some cases you want to do a modal edit. The user has to take some
2712 action (click a button or follow a link) to shift from display mode to
2713 edit mode. When the changes are submitted, ending the edit mode,
2714 the user is returned to display mode.
2715
2716 Modal workflows usually slow things down and are not implemented by
2717 default templates. However for some workflows a modal edit is useful.
2718 For example a batch edit mode that allows the user to edit a number of
2719 issues all from one form could be implemented as a modal workflow of:
2720
2721 * search for issues to modify
2722 * switch to edit mode and change values
2723 * exit back to the results of the search
2724
2725 To implement the modal edit, assume you have an issue.edit.html
2726 template that implements an edit form. On the display page (a version
2727 of issue.item.html modified to only display information) add a link
2728 that calls the display url, but adds ``@template=edit`` to the link.
2729
2730 This will now display the edit page. On the edit page you want to add
2731 a hidden text field to your form named ``@template`` with the value:
2732 ``item|edit``. When the form is submitted it is validated. If the
2733 form is correct the user will see the item rendered using the item
2734 template. If there is an error (validation failed) the item will be
2735 rendered using the edit template. The edit template that is rendered
2736 will display all the changes that the user made to the form before it
2737 was submitted. The user can correct the error and resubmit the changes
2738 until the form validates.
2739
2740 If the form failed to validate but the ``@template`` field had the
2741 value ``item`` the user would still see the error, but all of the data
2742 the user entered would be discarded. The user would have to redo all
2743 the edits again.
2744
2745
2746 How the templates work
2747 ----------------------
2748
2749
2750 Templating engines
2751 ~~~~~~~~~~~~~~~~~~
2752
2753 Since version 1.4.20 Roundup supports two templating engines:
2754
2755 * the original `Template Attribute Language`_ (TAL) engine from Zope
2756 * the standalone Chameleon templating engine. Chameleon is intended
2757 as a replacement for the original TAL engine, and supports the
2758 same syntax, but they are not 100% compatible. The major (and most
2759 likely the only) incompatibility is the default expression type being
2760 ``python:`` instead of ``path:``. See also "Incompatibilities and
2761 differences" section of `Chameleon documentation`__.
2762
2763 Version 1.5.0 added experimental support for the `jinja2`_ templating
2764 language. You must install the `jinja2`_ module in order to use it. The
2765 ``jinja2`` template supplied with Roundup has the templates rewritten
2766 to use ``jinja2`` rather than TAL. A number of trackers are running
2767 using ``jinja2`` templating so it is considered less experimental than
2768 Chameleon templating.
2769
2770 .. _jinja2: https://palletsprojects.com/p/jinja/
2771
2772
2773 **NOTE1**: For historical reasons, examples given below assumes path
2774 expression as default expression type. With Chameleon you have to manually
2775 resolve the path expressions. A Chameleon-based, z3c.pt, that is fully
2776 compatible with the old TAL implementation, is planned to be included in a
2777 future release.
2778
2779 **NOTE2**: As of 1.4.20 Chameleon support is highly experimental and **not**
2780 recommended for production use.
2781
2782 .. _Chameleon:
2783 https://pypi.org/project/Chameleon/
2784 .. _z3c.pt:
2785 https://pypi.org/project/z3c.pt/
2786 __ https://chameleon.readthedocs.io/en/latest/reference.html?highlight=differences#incompatibilities-and-differences
2787 .. _TAL:
2788 .. _Template Attribute Language:
2789 https://pagetemplates.readthedocs.io/en/latest/history/TALSpecification14.html
2790
2791
2792 Basic Templating Actions
2793 ~~~~~~~~~~~~~~~~~~~~~~~~
2794
2795 Roundup's templates consist of special attributes on the HTML tags.
2796 These attributes form the **Template Attribute Language**, or TAL.
2797 The basic TAL commands are:
2798
2799 **tal:define="variable expression; variable expression; ..."**
2800 Define a new variable that is local to this tag and its contents. For
2801 example::
2802
2803 <html tal:define="title request/description">
2804 <head><title tal:content="title"></title></head>
2805 </html>
2806
2807 In this example, the variable "title" is defined as the result of the
2808 expression "request/description". The "tal:content" command inside the
2809 <html> tag may then use the "title" variable.
2810
2811 **tal:condition="expression"**
2812 Only keep this tag and its contents if the expression is true. For
2813 example::
2814
2815 <p tal:condition="python:request.user.hasPermission('View', 'issue')">
2816 Display some issue information.
2817 </p>
2818
2819 In the example, the <p> tag and its contents are only displayed if
2820 the user has the "View" permission for issues. We consider the number
2821 zero, a blank string, an empty list, and the built-in variable
2822 nothing to be false values. Nearly every other value is true,
2823 including non-zero numbers, and strings with anything in them (even
2824 spaces!).
2825
2826 **tal:repeat="variable expression"**
2827 Repeat this tag and its contents for each element of the sequence
2828 that the expression returns, defining a new local variable and a
2829 special "repeat" variable for each element. For example::
2830
2831 <tr tal:repeat="u user/list">
2832 <td tal:content="u/id"></td>
2833 <td tal:content="u/username"></td>
2834 <td tal:content="u/realname"></td>
2835 </tr>
2836
2837 The example would iterate over the sequence of users returned by
2838 "user/list" and define the local variable "u" for each entry. Using
2839 the repeat command creates a new variable called "repeat" which you
2840 may access to gather information about the iteration. See the section
2841 below on `the repeat variable`_.
2842
2843 **tal:replace="expression"**
2844 Replace this tag with the result of the expression. For example::
2845
2846 <span tal:replace="request/user/realname" />
2847
2848 The example would replace the <span> tag and its contents with the
2849 user's realname. If the user's realname was "Bruce", then the
2850 resultant output would be "Bruce".
2851
2852 **tal:content="expression"**
2853 Replace the contents of this tag with the result of the expression.
2854 For example::
2855
2856 <span tal:content="request/user/realname">user's name appears here
2857 </span>
2858
2859 The example would replace the contents of the <span> tag with the
2860 user's realname. If the user's realname was "Bruce" then the
2861 resultant output would be "<span>Bruce</span>".
2862
2863 **tal:attributes="attribute expression; attribute expression; ..."**
2864 Set attributes on this tag to the results of expressions. For
2865 example::
2866
2867 <a tal:attributes="href string:user${request/user/id}">My Details</a>
2868
2869 In the example, the "href" attribute of the <a> tag is set to the
2870 value of the "string:user${request/user/id}" expression, which will
2871 be something like "user123".
2872
2873 **tal:omit-tag="expression"**
2874 Remove this tag (but not its contents) if the expression is true. For
2875 example::
2876
2877 <span tal:omit-tag="python:1">Hello, world!</span>
2878
2879 would result in output of::
2880
2881 Hello, world!
2882
2883 Note that the commands on a given tag are evaulated in the order above,
2884 so *define* comes before *condition*, and so on.
2885
2886 Additionally, you may include tags such as <tal:block>, which are
2887 removed from output. Its content is kept, but the tag itself is not (so
2888 don't go using any "tal:attributes" commands on it). This is useful for
2889 making arbitrary blocks of HTML conditional or repeatable (very handy
2890 for repeating multiple table rows, which would otherwise require an
2891 illegal tag placement to effect the repeat).
2892
2893
2894 Templating Expressions
2895 ~~~~~~~~~~~~~~~~~~~~~~
2896
2897 Templating Expressions are covered by `Template Attribute Language
2898 Expression Syntax`_, or TALES. The expressions you may use in the
2899 attribute values may be one of the following forms:
2900
2901 **Path Expressions** - eg. ``item/status/checklist``
2902 These are object attribute / item accesses. Roughly speaking, the
2903 path ``item/status/checklist`` is broken into parts ``item``,
2904 ``status`` and ``checklist``. The ``item`` part is the root of the
2905 expression. We then look for a ``status`` attribute on ``item``, or
2906 failing that, a ``status`` item (as in ``item['status']``). If that
2907 fails, the path expression fails. When we get to the end, the object
2908 we're left with is evaluated to get a string - if it is a method, it
2909 is called; if it is an object, it is stringified. Path expressions
2910 may have an optional ``path:`` prefix, but they are the default
2911 expression type, so it's not necessary.
2912
2913 If an expression evaluates to ``default``, then the expression is
2914 "cancelled" - whatever HTML already exists in the template will
2915 remain (tag content in the case of ``tal:content``, attributes in the
2916 case of ``tal:attributes``).
2917
2918 If an expression evaluates to ``nothing`` then the target of the
2919 expression is removed (tag content in the case of ``tal:content``,
2920 attributes in the case of ``tal:attributes`` and the tag itself in
2921 the case of ``tal:replace``).
2922
2923 If an element in the path may not exist, then you can use the ``|``
2924 operator in the expression to provide an alternative. So, the
2925 expression ``request/form/foo/value | default`` would simply leave
2926 the current HTML in place if the "foo" form variable doesn't exist.
2927
2928 You may use the python function ``path``, as in
2929 ``path("item/status")``, to embed path expressions in Python
2930 expressions.
2931
2932 **String Expressions** - eg. ``string:hello ${user/name}``
2933 These expressions are simple string interpolations - though they can
2934 be just plain strings with no interpolation if you want. The
2935 expression in the ``${ ... }`` is just a path expression as above.
2936
2937 **Python Expressions** - eg. ``python: 1+1``
2938 These expressions give the full power of Python. All the "root level"
2939 variables are available, so ``python:item.status.checklist()`` would
2940 be equivalent to ``item/status/checklist``, assuming that
2941 ``checklist`` is a method.
2942
2943 Modifiers:
2944
2945 **structure** - eg. ``structure python:msg.content.plain(hyperlink=1)``
2946 The result of expressions are normally *escaped* to be safe for HTML
2947 display (all "<", ">" and "&" are turned into special entities). The
2948 ``structure`` expression modifier turns off this escaping - the
2949 result of the expression is now assumed to be HTML, which is passed
2950 to the web browser for rendering.
2951
2952 **not:** - eg. ``not:python:1=1``
2953 This simply inverts the logical true/false value of another
2954 expression.
2955
2956 .. _TALES:
2957 .. _Template Attribute Language Expression Syntax:
2958 https://pagetemplates.readthedocs.io/en/latest/history/TALESSpecification13.html
2959
2960
2961 Template Macros
2962 ~~~~~~~~~~~~~~~
2963
2964 Macros are used in Roundup to save us from repeating the same common
2965 page stuctures over and over. The most common (and probably only) macro
2966 you'll use is the "icing" macro defined in the "page" template.
2967
2968 Macros are generated and used inside your templates using special
2969 attributes similar to the `basic templating actions`_. In this case,
2970 though, the attributes belong to the `Macro Expansion Template
2971 Attribute Language`_, or METAL. The macro commands are:
2972
2973 **metal:define-macro="macro name"**
2974 Define that the tag and its contents are now a macro that may be
2975 inserted into other templates using the *use-macro* command. For
2976 example::
2977
2978 <html metal:define-macro="page">
2979 ...
2980 </html>
2981
2982 defines a macro called "page" using the ``<html>`` tag and its
2983 contents. Once defined, macros are stored on the template they're
2984 defined on in the ``macros`` attribute. You can access them later on
2985 through the ``templates`` variable, eg. the most common
2986 ``templates/page/macros/icing`` to access the "page" macro of the
2987 "page" template.
2988
2989 **metal:use-macro="path expression"**
2990 Use a macro, which is identified by the path expression (see above).
2991 This will replace the current tag with the identified macro contents.
2992 For example::
2993
2994 <tal:block metal:use-macro="templates/page/macros/icing">
2995 ...
2996 </tal:block>
2997
2998 will replace the tag and its contents with the "page" macro of the
2999 "page" template.
3000
3001 **metal:define-slot="slot name"** and **metal:fill-slot="slot name"**
3002 To define *dynamic* parts of the macro, you define "slots" which may
3003 be filled when the macro is used with a *use-macro* command. For
3004 example, the ``templates/page/macros/icing`` macro defines a slot like
3005 so::
3006
3007 <title metal:define-slot="head_title">title goes here</title>
3008
3009 In your *use-macro* command, you may now use a *fill-slot* command
3010 like this::
3011
3012 <title metal:fill-slot="head_title">My Title</title>
3013
3014 where the tag that fills the slot completely replaces the one defined
3015 as the slot in the macro.
3016
3017 Note that you may not mix `METAL`_ and `TAL`_ commands on the same tag, but
3018 TAL commands may be used freely inside METAL-using tags (so your
3019 *fill-slots* tags may have all manner of TAL inside them).
3020
3021 .. _METAL:
3022 .. _Macro Expansion Template Attribute Language:
3023 https://pagetemplates.readthedocs.io/en/latest/history/TALESSpecification13.html
3024
3025 Information available to templates
3026 ----------------------------------
3027
3028 This is implemented by ``roundup.cgi.templating.RoundupPageTemplate``
3029
3030 The following variables are available to templates.
3031
3032 **context**
3033 The current context. This is either None, a `hyperdb class wrapper`_
3034 or a `hyperdb item wrapper`_
3035
3036 **request**
3037 Includes information about the current request, including:
3038
3039 - the current index information (``filterspec``, ``filter``
3040 args, ``properties``, etc) parsed out of the form.
3041 - methods for easy filterspec link generation
3042 - "form"
3043 The current CGI form information as a mapping of form argument name
3044 to value (specifically a cgi.FieldStorage)
3045 - "env" the CGI environment variables
3046 - "base" the base URL for this instance
3047 - "user" a HTMLItem instance for the current user
3048 - "language" as determined by the browser or config
3049 - "classname" the current classname (possibly None)
3050 - "template" the current template (suffix, also possibly None)
3051 **config**
3052 This variable holds all the values defined in the tracker config.ini
3053 file (eg. TRACKER_NAME, etc.)
3054 **db**
3055 The current database, used to access arbitrary database items.
3056 **templates**
3057 Access to all the tracker templates by name. Used mainly in
3058 *use-macro* commands.
3059 **utils**
3060 This variable makes available some utility functions like batching.
3061 **nothing**
3062 This is a special variable - if an expression evaluates to this, then
3063 the tag (in the case of a ``tal:replace``), its contents (in the case
3064 of ``tal:content``) or some attributes (in the case of
3065 ``tal:attributes``) will not appear in the the output. So, for
3066 example::
3067
3068 <span tal:attributes="class nothing">Hello, World!</span>
3069
3070 would result in::
3071
3072 <span>Hello, World!</span>
3073
3074 **default**
3075 Also a special variable - if an expression evaluates to this, then the
3076 existing HTML in the template will not be replaced or removed, it will
3077 remain. So::
3078
3079 <span tal:replace="default">Hello, World!</span>
3080
3081 would result in::
3082
3083 <span>Hello, World!</span>
3084
3085 **true**, **false**
3086 Boolean constants that may be used in `templating expressions`_
3087 instead of ``python:1`` and ``python:0``.
3088 **i18n**
3089 Internationalization service, providing two string translation methods:
3090
3091 **gettext** (*message*)
3092 Return the localized translation of message
3093 **ngettext** (*singular*, *plural*, *number*)
3094 Like ``gettext()``, but consider plural forms. If a translation
3095 is found, apply the plural formula to *number*, and return the
3096 resulting message (some languages have more than two plural forms).
3097 If no translation is found, return singular if *number* is 1;
3098 return plural otherwise.
3099
3100 The context variable
3101 ~~~~~~~~~~~~~~~~~~~~
3102
3103 The *context* variable is one of three things based on the current
3104 context (see `determining web context`_ for how we figure this out):
3105
3106 1. if we're looking at a "home" page, then it's None
3107 2. if we're looking at a specific hyperdb class, it's a
3108 `hyperdb class wrapper`_.
3109 3. if we're looking at a specific hyperdb item, it's a
3110 `hyperdb item wrapper`_.
3111
3112 If the context is not None, we can access the properties of the class or
3113 item. The only real difference between cases 2 and 3 above are:
3114
3115 1. the properties may have a real value behind them, and this will
3116 appear if the property is displayed through ``context/property`` or
3117 ``context/property/field``.
3118 2. the context's "id" property will be a false value in the second case,
3119 but a real, or true value in the third. Thus we can determine whether
3120 we're looking at a real item from the hyperdb by testing
3121 "context/id".
3122
3123 Hyperdb class wrapper
3124 :::::::::::::::::::::
3125
3126 This is implemented by the ``roundup.cgi.templating.HTMLClass``
3127 class.
3128
3129 This wrapper object provides access to a hyperdb class. It is used
3130 primarily in both index view and new item views, but it's also usable
3131 anywhere else that you wish to access information about a class, or the
3132 items of a class, when you don't have a specific item of that class in
3133 mind.
3134
3135 We allow access to properties. There will be no "id" property. The value
3136 accessed through the property will be the current value of the same name
3137 from the CGI form.
3138
3139 There are several methods available on these wrapper objects:
3140
3141 =========== =============================================================
3142 Method Description
3143 =========== =============================================================
3144 properties return a `hyperdb property wrapper`_ for all of this class's
3145 properties that are searchable by the user. You can use
3146 the argument cansearch=False to get all properties.
3147 list lists all of the active (not retired) items in the class.
3148 csv return the items of this class as a chunk of CSV text.
3149 propnames lists the names of the properties of this class.
3150 filter lists of items from this class, filtered and sorted. Two
3151 options are available for sorting:
3152
3153 1. by the current *request* filterspec/filter/sort/group args
3154 2. by the "filterspec", "sort" and "group" keyword args.
3155 "filterspec" is ``{propname: value(s)}``. "sort" and
3156 "group" are an optionally empty list ``[(dir, prop)]``
3157 where dir is '+', '-' or None
3158 and prop is a prop name or None.
3159
3160 The propname in filterspec and prop in a sort/group spec
3161 may be transitive, i.e., it may contain properties of
3162 the form link.link.link.name.
3163
3164 eg. All issues with a priority of "1" with messages added in
3165 the last week, sorted by activity date:
3166 ``issue.filter(filterspec={"priority": "1",
3167 'messages.creation' : '.-1w;'}, sort=[('activity', '+')])``
3168
3169 Note that when searching for Link and Multilink values, the
3170 special value '-1' searches for empty Link or Multilink
3171 values. For both, Links and Multilinks, multiple values
3172 given in a filter call are combined with 'OR' by default.
3173 For Multilinks a postfix expression syntax using negative ID
3174 numbers (as strings) as operators is supported. Each
3175 non-negative number (or '-1') is pushed on an operand stack.
3176 A negative number pops the required number of arguments from
3177 the stack, applies the operator, and pushes the result. The
3178 following operators are supported:
3179
3180 - '-2' stands for 'NOT' and takes one argument
3181 - '-3' stands for 'AND' and takes two arguments
3182 - '-4' stands for 'OR' and takes two arguments
3183
3184 Note that this special handling of ID arguments is applied only
3185 when a negative number smaller than -1 is encountered as an ID
3186 in the filter call. Otherwise the implicit OR default
3187 applies.
3188 Examples of using Multilink expressions would be
3189
3190 - '1', '2', '-4', '3', '4', '-4', '-3'
3191 would search for IDs (1 or 2) and (3 or 4)
3192 - '-1' '-2' would search for all non-empty Multilinks
3193
3194 filter_sql **Only in SQL backends**
3195
3196 Lists the items that match the SQL provided. The SQL is a
3197 complete "select" statement.
3198
3199 The SQL select must include the item id as the first column.
3200
3201 This function **does not** filter out retired items, add
3202 on a where clause "__retired__ <> 1" if you don't want
3203 retired nodes.
3204
3205 classhelp display a link to a javascript popup containing this class'
3206 "help" template.
3207
3208 This generates a link to a popup window which displays the
3209 properties indicated by "properties" of the class named by
3210 "classname". The "properties" should be a comma-separated list
3211 (eg. 'id,name,description'). Properties defaults to all the
3212 properties of a class (excluding id, creator, created and
3213 activity).
3214
3215 You may optionally override the "label" displayed, the "width",
3216 the "height", the number of items per page ("pagesize") and
3217 the field on which the list is sorted ("sort").
3218
3219 With the "filter" arg it is possible to specify a filter for
3220 which items are supposed to be displayed. It has to be of
3221 the format "<field>=<values>;<field>=<values>;...".
3222
3223 The popup window will be resizable and scrollable.
3224
3225 If the "property" arg is given, it's passed through to the
3226 javascript help_window function. This allows updating of a
3227 property in the calling HTML page.
3228
3229 If the "form" arg is given, it's passed through to the
3230 javascript help_window function - it's the name of the form
3231 the "property" belongs to.
3232
3233 submit generate a submit button (and action and @csrf hidden elements)
3234 renderWith render this class with the given template.
3235 history returns 'New node - no history' :)
3236 is_edit_ok is the user allowed to Edit the current class?
3237 is_view_ok is the user allowed to View the current class?
3238 =========== =============================================================
3239
3240 Note that if you have a property of the same name as one of the above
3241 methods, you'll need to access it using a python "item access"
3242 expression. For example::
3243
3244 python:context['list']
3245
3246 will access the "list" property, rather than the list method.
3247
3248
3249 Hyperdb item wrapper
3250 ::::::::::::::::::::
3251
3252 This is implemented by the ``roundup.cgi.templating.HTMLItem``
3253 class.
3254
3255 This wrapper object provides access to a hyperdb item.
3256
3257 We allow access to properties. There will be no "id" property. The value
3258 accessed through the property will be the current value of the same name
3259 from the CGI form.
3260
3261 There are several methods available on these wrapper objects:
3262
3263 =============== ========================================================
3264 Method Description
3265 =============== ========================================================
3266 submit generate a submit button (and action and @csrf hidden elements)
3267 journal return the journal of the current item (**not
3268 implemented**)
3269 history render the journal of the current item as
3270 HTML. By default properties marked as "quiet" (see
3271 `design documentation`_) are not shown unless the
3272 function is called with the showall=True parameter.
3273 Properties that are not Viewable to the user are not
3274 shown.
3275 renderQueryForm specific to the "query" class - render the search form
3276 for the query
3277 hasPermission specific to the "user" class - determine whether the
3278 user has a Permission. The signature is::
3279
3280 hasPermission(self, permission, [classname=],
3281 [property=], [itemid=])
3282
3283 where the classname defaults to the current context.
3284 hasRole specific to the "user" class - determine whether the
3285 user has a Role. The signature is::
3286
3287 hasRole(self, rolename)
3288
3289 is_edit_ok is the user allowed to Edit the current item?
3290 is_view_ok is the user allowed to View the current item?
3291 is_retired is the item retired?
3292 download_url generate a url-quoted link for download of FileClass
3293 item contents (ie. file<id>/<name>)
3294 copy_url generate a url-quoted link for creating a copy
3295 of this item. By default, the copy will acquire
3296 all properties of the current item except for
3297 ``messages`` and ``files``. This can be overridden
3298 by passing ``exclude`` argument which contains a list
3299 (or any iterable) of property names that shall not be
3300 copied. Database-driven properties like ``id`` or
3301 ``activity`` cannot be copied.
3302 =============== ========================================================
3303
3304 Note that if you have a property of the same name as one of the above
3305 methods, you'll need to access it using a python "item access"
3306 expression. For example::
3307
3308 python:context['journal']
3309
3310 will access the "journal" property, rather than the journal method.
3311
3312
3313 Hyperdb property wrapper
3314 ::::::::::::::::::::::::
3315
3316 This is implemented by subclasses of the
3317 ``roundup.cgi.templating.HTMLProperty`` class (``HTMLStringProperty``,
3318 ``HTMLNumberProperty``, and so on).
3319
3320 This wrapper object provides access to a single property of a class. Its
3321 value may be either:
3322
3323 1. if accessed through a `hyperdb item wrapper`_, then it's a value from
3324 the hyperdb
3325 2. if access through a `hyperdb class wrapper`_, then it's a value from
3326 the CGI form
3327
3328
3329 The property wrapper has some useful attributes:
3330
3331 =============== ========================================================
3332 Attribute Description
3333 =============== ========================================================
3334 _name the name of the property
3335 _value the value of the property if any - this is the actual
3336 value retrieved from the hyperdb for this property
3337 =============== ========================================================
3338
3339 There are several methods available on these wrapper objects:
3340
3341 =========== ================================================================
3342 Method Description
3343 =========== ================================================================
3344 plain render a "plain" representation of the property. This method
3345 may take two arguments:
3346
3347 escape
3348 If true, escape the text so it is HTML safe (default: no). The
3349 reason this defaults to off is that text is usually escaped
3350 at a later stage by the TAL commands, unless the "structure"
3351 option is used in the template. The following ``tal:content``
3352 expressions are all equivalent::
3353
3354 "structure python:msg.content.plain(escape=1)"
3355 "python:msg.content.plain()"
3356 "msg/content/plain"
3357 "msg/content"
3358
3359 Usually you'll only want to use the escape option in a
3360 complex expression.
3361
3362 hyperlink
3363 If true, turn URLs, email addresses and hyperdb item
3364 designators in the text into hyperlinks (default: no). Note
3365 that you'll need to use the "structure" TAL option if you
3366 want to use this ``tal:content`` expression::
3367
3368 "structure python:msg.content.plain(hyperlink=1)"
3369
3370 The text is automatically HTML-escaped before the hyperlinking
3371 transformation done in the plain() method.
3372
3373 hyperlinked The same as msg.content.plain(hyperlink=1), but nicer::
3374
3375 "structure msg/content/hyperlinked"
3376
3377 field render an appropriate form edit field for the property - for
3378 most types this is a text entry box, but for Booleans it's a
3379 tri-state yes/no/neither selection. This method may take some
3380 arguments:
3381
3382 size
3383 Sets the width in characters of the edit field
3384
3385 format (Date properties only)
3386 Sets the format of the date in the field - uses the same
3387 format string argument as supplied to the ``pretty`` method
3388 below.
3389
3390 popcal (Date properties only)
3391 Include the Javascript-based popup calendar for date
3392 selection. Defaults to on.
3393
3394 stext only on String properties - render the value of the property
3395 as StructuredText (requires the StructureText module to be
3396 installed separately)
3397 multiline only on String properties - render a multiline form edit
3398 field for the property
3399 email only on String properties - render the value of the property
3400 as an obscured email address
3401 url_quote only on String properties. It quotes any characters in the
3402 string so it is safe to use in a url. E.G. a space is
3403 replaced with %20.
3404 confirm only on Password properties - render a second form edit field
3405 for the property, used for confirmation that the user typed
3406 the password correctly. Generates a field with name
3407 "name:confirm".
3408 now only on Date properties - return the current date as a new
3409 property
3410 reldate only on Date properties - render the interval between the date
3411 and now
3412 local only on Date properties - return this date as a new property
3413 with some timezone offset, for example::
3414
3415 python:context.creation.local(10)
3416
3417 will render the date with a +10 hour offset.
3418 pretty Date properties - render the date as "dd Mon YYYY" (eg. "19
3419 Mar 2004"). Takes an optional format argument, for example::
3420
3421 python:context.activity.pretty('%Y-%m-%d')
3422
3423 Will format as "2004-03-19" instead.
3424
3425 Interval properties - render the interval in a pretty
3426 format (eg. "yesterday"). The format arguments are those used
3427 in the standard ``strftime`` call (see the `Python Library
3428 Reference: time module`__)
3429
3430 Number properties - takes a printf style format argument
3431 (default: '%0.3f') and formats the number accordingly.
3432 If the value can't be converted, '' is returned if the
3433 value is ``None`` otherwise it is converted to a string.
3434 popcal Generate a link to a popup calendar which may be used to
3435 edit the date field, for example::
3436
3437 <span tal:replace="structure context/due/popcal" />
3438
3439 you still need to include the ``field`` for the property, so
3440 typically you'd have::
3441
3442 <span tal:replace="structure context/due/field" />
3443 <span tal:replace="structure context/due/popcal" />
3444
3445 menu only on Link and Multilink properties - render a form select
3446 list for this property. Takes a number of optional arguments
3447
3448 size
3449 is used to limit the length of the list labels
3450 height
3451 is used to set the <select> tag's "size" attribute
3452 showid
3453 includes the item ids in the list labels
3454 additional
3455 lists properties which should be included in the label
3456 sort_on
3457 indicates the property to sort the list on as (direction,
3458 (direction, property) where direction is '+' or '-'. A
3459 single string with the direction prepended may be used.
3460 For example: ('-', 'order'), '+name'.
3461 value
3462 gives a default value to preselect in the menu
3463
3464 The remaining keyword arguments are used as conditions for
3465 filtering the items in the list - they're passed as the
3466 "filterspec" argument to a Class.filter() call. For example::
3467
3468 <span tal:replace="structure context/status/menu" />
3469
3470 <span tal:replace="python:context.status.menu(order='+name",
3471 value='chatting',
3472 filterspec={'status': '1,2,3,4'}" />
3473
3474 sorted only on Multilink properties - produce a list of the linked
3475 items sorted by some property, for example::
3476
3477 python:context.files.sorted('creation')
3478
3479 Will list the files by upload date. While::
3480
3481 python:context.files.sorted('creation', reverse=True)
3482
3483 Will list the files by upload date in reverse order from
3484 the prior example. If the property can be unset, you can
3485 use the ``NoneFirst`` parameter to sort the None/Unset
3486 values at the front or the end of the list. For example::
3487
3488 python:context.files.sorted('creation', NoneFirst=True)
3489
3490 will sort files by creation date with files missing a
3491 creation date at the start of the list. The default for
3492 ``NoneFirst`` is False so these files will sort at the end
3493 by default. (Note creation date is never unset, but you
3494 get the idea.) If you combine NoneFirst with
3495 ``reverse=True`` the meaning of NoneFirst is inverted:
3496 True sorts None/unset at the end and False sorts at the
3497 beginning.
3498 reverse only on Multilink properties - produce a list of the linked
3499 items in reverse order
3500 isset returns True if the property has been set to a value
3501 =========== ================================================================
3502
3503 __ https://docs.python.org/2/library/time.html
3504
3505 All of the above functions perform checks for permissions required to
3506 display or edit the data they are manipulating. The simplest case is
3507 editing an issue title. Including the expression::
3508
3509 context/title/field
3510
3511 Will present the user with an edit field, if they have edit permission. If
3512 not, then they will be presented with a static display if they have view
3513 permission. If they don't even have view permission, then an error message
3514 is raised, preventing the display of the page, indicating that they don't
3515 have permission to view the information.
3516
3517
3518 The request variable
3519 ~~~~~~~~~~~~~~~~~~~~
3520
3521 This is implemented by the ``roundup.cgi.templating.HTMLRequest``
3522 class.
3523
3524 The request variable is packed with information about the current
3525 request.
3526
3527 .. taken from ``roundup.cgi.templating.HTMLRequest`` docstring
3528
3529 =========== ============================================================
3530 Variable Holds
3531 =========== ============================================================
3532 form the CGI form as a cgi.FieldStorage
3533 env the CGI environment variables
3534 base the base URL for this tracker
3535 user a HTMLUser instance for this user
3536 classname the current classname (possibly None)
3537 template the current template (suffix, also possibly None)
3538 form the current CGI form variables in a FieldStorage
3539 =========== ============================================================
3540
3541 **Index page specific variables (indexing arguments)**
3542
3543 =========== ============================================================
3544 Variable Holds
3545 =========== ============================================================
3546 columns dictionary of the columns to display in an index page
3547 show a convenience access to columns - request/show/colname will
3548 be true if the columns should be displayed, false otherwise
3549 sort index sort columns [(direction, column name)]
3550 group index grouping properties [(direction, column name)]
3551 filter properties to filter the index on
3552 filterspec values to filter the index on (property=value, eg
3553 ``priority=1`` or ``messages.author=42``
3554 search_text text to perform a full-text search on for an index
3555 =========== ============================================================
3556
3557 There are several methods available on the request variable:
3558
3559 =============== ========================================================
3560 Method Description
3561 =============== ========================================================
3562 description render a description of the request - handle for the
3563 page title
3564 indexargs_form render the current index args as form elements
3565 indexargs_url render the current index args as a URL
3566 base_javascript render some javascript that is used by other components
3567 of the templating
3568 batch run the current index args through a filter and return a
3569 list of items (see `hyperdb item wrapper`_, and
3570 `batching`_)
3571 =============== ========================================================
3572
3573 The form variable
3574 :::::::::::::::::
3575
3576 The form variable is a bit special because it's actually a python
3577 FieldStorage object. That means that you have two ways to access its
3578 contents. For example, to look up the CGI form value for the variable
3579 "name", use the path expression::
3580
3581 request/form/name/value
3582
3583 or the python expression::
3584
3585 python:request.form['name'].value
3586
3587 Note the "item" access used in the python case, and also note the
3588 explicit "value" attribute we have to access. That's because the form
3589 variables are stored as MiniFieldStorages. If there's more than one
3590 "name" value in the form, then the above will break since
3591 ``request/form/name`` is actually a *list* of MiniFieldStorages. So it's
3592 best to know beforehand what you're dealing with.
3593
3594
3595 The db variable
3596 ~~~~~~~~~~~~~~~
3597
3598 This is implemented by the ``roundup.cgi.templating.HTMLDatabase``
3599 class.
3600
3601 Allows access to all hyperdb classes as attributes of this variable. If
3602 you want access to the "user" class, for example, you would use::
3603
3604 db/user
3605 python:db.user
3606
3607 Also, the current id of the current user is available as
3608 ``db.getuid()``. This isn't so useful in templates (where you have
3609 ``request/user``), but it can be useful in detectors or interfaces.
3610
3611 The access results in a `hyperdb class wrapper`_.
3612
3613
3614 The templates variable
3615 ~~~~~~~~~~~~~~~~~~~~~~
3616
3617 This was implemented by the ``roundup.cgi.templating.Templates``
3618 class before 1.4.20. In later versions it is the instance of appropriate
3619 template engine loader class.
3620
3621 This variable is used to access other templates in expressions and
3622 template macros. It doesn't have any useful methods defined. The
3623 templates can be accessed using the following path expression::
3624
3625 templates/name
3626
3627 or the python expression::
3628
3629 templates[name]
3630
3631 where "name" is the name of the template you wish to access. The
3632 template has one useful attribute, namely "macros". To access a specific
3633 macro (called "macro_name"), use the path expression::
3634
3635 templates/name/macros/macro_name
3636
3637 or the python expression::
3638
3639 templates[name].macros[macro_name]
3640
3641 The repeat variable
3642 ~~~~~~~~~~~~~~~~~~~
3643
3644 The repeat variable holds an entry for each active iteration. That is, if
3645 you have a ``tal:repeat="user db/users"`` command, then there will be a
3646 repeat variable entry called "user". This may be accessed as either::
3647
3648 repeat/user
3649 python:repeat['user']
3650
3651 The "user" entry has a number of methods available for information:
3652
3653 =============== =========================================================
3654 Method Description
3655 =============== =========================================================
3656 first True if the current item is the first in the sequence.
3657 last True if the current item is the last in the sequence.
3658 even True if the current item is an even item in the sequence.
3659 odd True if the current item is an odd item in the sequence.
3660 number Current position in the sequence, starting from 1.
3661 letter Current position in the sequence as a letter, a through
3662 z, then aa through zz, and so on.
3663 Letter Same as letter(), except uppercase.
3664 roman Current position in the sequence as lowercase roman
3665 numerals.
3666 Roman Same as roman(), except uppercase.
3667 =============== =========================================================
3668
3669 .. _templating utilities:
3670
3671 The utils variable
3672 ~~~~~~~~~~~~~~~~~~
3673
3674 This is implemented by the
3675 ``roundup.cgi.templating.TemplatingUtils`` class, which may be extended
3676 with additional methods by extensions_.
3677
3678 =============== ========================================================
3679 Method Description
3680 =============== ========================================================
3681 Batch return a batch object using the supplied list
3682 url_quote quote some text as safe for a URL (ie. space, %, ...)
3683 html_quote quote some text as safe in HTML (ie. <, >, ...)
3684 html_calendar renders an HTML calendar used by the
3685 ``_generic.calendar.html`` template (itself invoked by
3686 the popupCalendar DateHTMLProperty method
3687 anti_csrf_nonce returns the random noncue generated for this session
3688 =============== ========================================================
3689
3690
3691 Batching
3692 ::::::::
3693
3694 Use Batch to turn a list of items, or item ids of a given class, into a
3695 series of batches. Its usage is::
3696
3697 python:utils.Batch(sequence, size, start, end=0, orphan=0,
3698 overlap=0)
3699
3700 or, to get the current index batch::
3701
3702 request/batch
3703
3704 The parameters are:
3705
3706 ========= ==============================================================
3707 Parameter Usage
3708 ========= ==============================================================
3709 sequence a list of HTMLItems
3710 size how big to make the sequence.
3711 start where to start (0-indexed) in the sequence.
3712 end where to end (0-indexed) in the sequence.
3713 orphan if the next batch would contain less items than this value,
3714 then it is combined with this batch
3715 overlap the number of items shared between adjacent batches
3716 ========= ==============================================================
3717
3718 All of the parameters are assigned as attributes on the batch object. In
3719 addition, it has several more attributes:
3720
3721 =============== ========================================================
3722 Attribute Description
3723 =============== ========================================================
3724 start indicates the start index of the batch. *Unlike
3725 the argument, is a 1-based index (I know, lame)*
3726 first indicates the start index of the batch *as a 0-based
3727 index*
3728 length the actual number of elements in the batch
3729 sequence_length the length of the original, unbatched, sequence.
3730 =============== ========================================================
3731
3732 And several methods:
3733
3734 =============== ========================================================
3735 Method Description
3736 =============== ========================================================
3737 previous returns a new Batch with the previous batch settings
3738 next returns a new Batch with the next batch settings
3739 propchanged detect if the named property changed on the current item
3740 when compared to the last item
3741 =============== ========================================================
3742
3743 An example of batching::
3744
3745 <table class="otherinfo">
3746 <tr><th colspan="4" class="header">Existing Keywords</th></tr>
3747 <tr tal:define="keywords db/keyword/list"
3748 tal:repeat="start python:range(0, len(keywords), 4)">
3749 <td tal:define="batch python:utils.Batch(keywords, 4, start)"
3750 tal:repeat="keyword batch" tal:content="keyword/name">
3751 keyword here</td>
3752 </tr>
3753 </table>
3754
3755 ... which will produce a table with four columns containing the items of
3756 the "keyword" class (well, their "name" anyway).
3757
3758
3759 Translations
3760 ~~~~~~~~~~~~
3761
3762 Should you wish to enable multiple languages in template content that you
3763 create you'll need to add new locale files in the tracker home under a
3764 ``locale`` directory. Use the `translation instructions in the
3765 developer's guide <developers.html#extracting-translatable-messages>`_ to
3766 create the locale files.
3767
3768
3769 Displaying Properties
3770 ---------------------
3771
3772 Properties appear in the user interface in three contexts: in indices,
3773 in editors, and as search arguments. For each type of property, there
3774 are several display possibilities. For example, in an index view, a
3775 string property may just be printed as a plain string, but in an editor
3776 view, that property may be displayed in an editable field.
3777
3778
3779 Index Views
3780 -----------
3781
3782 This is one of the class context views. It is also the default view for
3783 classes. The template used is "*classname*.index".
3784
3785
3786 Index View Specifiers
3787 ~~~~~~~~~~~~~~~~~~~~~
3788
3789 An index view specifier (URL fragment) looks like this (whitespace has
3790 been added for clarity)::
3791
3792 /issue?status=unread,in-progress,resolved&
3793 keyword=security,ui&
3794 @group=priority,-status&
3795 @sort=-activity&
3796 @filters=status,keyword&
3797 @columns=title,status,fixer
3798
3799 The index view is determined by two parts of the specifier: the layout
3800 part and the filter part. The layout part consists of the query
3801 parameters that begin with colons, and it determines the way that the
3802 properties of selected items are displayed. The filter part consists of
3803 all the other query parameters, and it determines the criteria by which
3804 items are selected for display. The filter part is interactively
3805 manipulated with the form widgets displayed in the filter section. The
3806 layout part is interactively manipulated by clicking on the column
3807 headings in the table.
3808
3809 The filter part selects the union of the sets of items with values
3810 matching any specified Link properties and the intersection of the sets
3811 of items with values matching any specified Multilink properties.
3812
3813 The example specifies an index of "issue" items. Only items with a
3814 "status" of either "unread" or "in-progress" or "resolved" are
3815 displayed, and only items with "keyword" values including both "security"
3816 and "ui" are displayed. The items are grouped by priority arranged in
3817 ascending order and in descending order by status; and within
3818 groups, sorted by activity, arranged in descending order. The filter
3819 section shows filters for the "status" and "keyword" properties, and the
3820 table includes columns for the "title", "status", and "fixer"
3821 properties.
3822
3823 ============ =============================================================
3824 Argument Description
3825 ============ =============================================================
3826 @sort sort by prop name, optionally preceeded with '-' to give
3827 descending or nothing for ascending sorting. Several
3828 properties can be specified delimited with comma.
3829 Internally a search-page using several sort properties may
3830 use @sort0, @sort1 etc. with option @sortdir0, @sortdir1
3831 etc. for the direction of sorting (a non-empty value of
3832 sortdir0 specifies reverse order).
3833 @group group by prop name, optionally preceeded with '-' or to sort
3834 in descending or nothing for ascending order. Several
3835 properties can be specified delimited with comma.
3836 Internally a search-page using several grouping properties may
3837 use @group0, @group1 etc. with option @groupdir0, @groupdir1
3838 etc. for the direction of grouping (a non-empty value of
3839 groupdir0 specifies reverse order).
3840 @columns selects the columns that should be displayed. Default is
3841 all.
3842 @filter indicates which properties are being used in filtering.
3843 Default is none.
3844 propname selects the values the item properties given by propname must
3845 have (very basic search/filter).
3846 @search_text if supplied, performs a full-text search (message bodies,
3847 issue titles, etc)
3848 ============ =============================================================
3849
3850
3851 Searching Views
3852 ---------------
3853
3854 .. note::
3855 if you add a new column to the ``@columns`` form variable potentials
3856 then you will need to add the column to the appropriate `index views`_
3857 template so that it is actually displayed.
3858
3859 This is one of the class context views. The template used is typically
3860 "*classname*.search". The form on this page should have "search" as its
3861 ``@action`` variable. The "search" action:
3862
3863 - sets up additional filtering, as well as performing indexed text
3864 searching
3865 - sets the ``@filter`` variable correctly
3866 - saves the query off if ``@query_name`` is set.
3867
3868 The search page should lay out any fields that you wish to allow the
3869 user to search on. If your schema contains a large number of properties,
3870 you should be wary of making all of those properties available for
3871 searching, as this can cause confusion. If the additional properties are
3872 Strings, consider having their value indexed, and then they will be
3873 searchable using the full text indexed search. This is both faster, and
3874 more useful for the end user.
3875
3876 If the search view does specify the "search" ``@action``, then it may also
3877 provide an additional argument:
3878
3879 ============ =============================================================
3880 Argument Description
3881 ============ =============================================================
3882 @query_name if supplied, the index parameters (including @search_text)
3883 will be saved off as a the query item and registered against
3884 the user's queries property. Note that the *classic* template
3885 schema has this ability, but the *minimal* template schema
3886 does not.
3887 ============ =============================================================
3888
3889
3890 Item Views
3891 ----------
3892
3893 The basic view of a hyperdb item is provided by the "*classname*.item"
3894 template. It generally has three sections; an "editor", a "spool" and a
3895 "history" section.
3896
3897
3898 Editor Section
3899 ~~~~~~~~~~~~~~
3900
3901 The editor section is used to manipulate the item - it may be a static
3902 display if the user doesn't have permission to edit the item.
3903
3904 Here's an example of a basic editor template (this is the default
3905 "classic" template issue item edit form - from the "issue.item.html"
3906 template)::
3907
3908 <table class="form">
3909 <tr>
3910 <th>Title</th>
3911 <td colspan="3" tal:content="structure python:context.title.field(size=60)">title</td>
3912 </tr>
3913
3914 <tr>
3915 <th>Priority</th>
3916 <td tal:content="structure context/priority/menu">priority</td>
3917 <th>Status</th>
3918 <td tal:content="structure context/status/menu">status</td>
3919 </tr>
3920
3921 <tr>
3922 <th>Superseder</th>
3923 <td>
3924 <span tal:replace="structure python:context.superseder.field(showid=1, size=20)" />
3925 <span tal:replace="structure python:db.issue.classhelp('id,title')" />
3926 <span tal:condition="context/superseder">
3927 <br>View: <span tal:replace="structure python:context.superseder.link(showid=1)" />
3928 </span>
3929 </td>
3930 <th>Nosy List</th>
3931 <td>
3932 <span tal:replace="structure context/nosy/field" />
3933 <span tal:replace="structure python:db.user.classhelp('username,realname,address,phone')" />
3934 </td>
3935 </tr>
3936
3937 <tr>
3938 <th>Assigned To</th>
3939 <td tal:content="structure context/assignedto/menu">
3940 assignedto menu
3941 </td>
3942 <td>&nbsp;</td>
3943 <td>&nbsp;</td>
3944 </tr>
3945
3946 <tr>
3947 <th>Change Note</th>
3948 <td colspan="3">
3949 <textarea name=":note" wrap="hard" rows="5" cols="60"></textarea>
3950 </td>
3951 </tr>
3952
3953 <tr>
3954 <th>File</th>
3955 <td colspan="3"><input type="file" name=":file" size="40"></td>
3956 </tr>
3957
3958 <tr>
3959 <td>&nbsp;</td>
3960 <td colspan="3" tal:content="structure context/submit">
3961 submit button will go here
3962 </td>
3963 </tr>
3964 </table>
3965
3966
3967 When a change is submitted, the system automatically generates a message
3968 describing the changed properties. As shown in the example, the editor
3969 template can use the ":note" and ":file" fields, which are added to the
3970 standard changenote message generated by Roundup.
3971
3972
3973 Form values
3974 :::::::::::
3975
3976 We have a number of ways to pull properties out of the form in order to
3977 meet the various needs of:
3978
3979 1. editing the current item (perhaps an issue item)
3980 2. editing information related to the current item (eg. messages or
3981 attached files)
3982 3. creating new information to be linked to the current item (eg. time
3983 spent on an issue)
3984
3985 In the following, ``<bracketed>`` values are variable, ":" may be one of
3986 ":" or "@", and other text ("required") is fixed.
3987
3988 Properties are specified as form variables:
3989
3990 ``<propname>``
3991 property on the current context item
3992
3993 ``<designator>:<propname>``
3994 property on the indicated item (for editing related information)
3995
3996 ``<classname>-<N>:<propname>``
3997 property on the Nth new item of classname (generally for creating new
3998 items to attach to the current item)
3999
4000 Once we have determined the "propname", we check to see if it is one of
4001 the special form values:
4002
4003 ``@required``
4004 The named property values must be supplied or a ValueError will be
4005 raised.
4006
4007 ``@remove@<propname>=id(s)``
4008 The ids will be removed from the multilink property.
4009
4010 ``:add:<propname>=id(s)``
4011 The ids will be added to the multilink property.
4012
4013 ``:link:<propname>=<designator>``
4014 Used to add a link to new items created during edit. These are
4015 collected and returned in ``all_links``. This will result in an
4016 additional linking operation (either Link set or Multilink append)
4017 after the edit/create is done using ``all_props`` in ``_editnodes``.
4018 The <propname> on the current item will be set/appended the id of the
4019 newly created item of class <designator> (where <designator> must be
4020 <classname>-<N>).
4021
4022 Any of the form variables may be prefixed with a classname or
4023 designator.
4024
4025 Two special form values are supported for backwards compatibility:
4026
4027 ``:note``
4028 create a message (with content, author and date), linked to the
4029 context item. This is ALWAYS designated "msg-1".
4030 ``:file``
4031 create a file, attached to the current item and any message created by
4032 :note. This is ALWAYS designated "file-1".
4033
4034
4035 Spool Section
4036 ~~~~~~~~~~~~~
4037
4038 The spool section lists related information like the messages and files
4039 of an issue.
4040
4041 TODO
4042
4043
4044 History Section
4045 ~~~~~~~~~~~~~~~
4046
4047 The final section displayed is the history of the item - its database
4048 journal. This is generally generated with the template::
4049
4050 <tal:block tal:replace="structure context/history" />
4051
4052 or::
4053
4054 <tal:block
4055 tal:replace="structure python:context.history(showall=True)" />
4056
4057 if you want to show history entries for quiet properties.
4058
4059 *To be done:*
4060
4061 *The actual history entries of the item may be accessed for manual
4062 templating through the "journal" method of the item*::
4063
4064 <tal:block tal:repeat="entry context/journal">
4065 a journal entry
4066 </tal:block>
4067
4068 *where each journal entry is an HTMLJournalEntry.*
4069
4070
4071 Defining new web actions
4072 ------------------------
4073
4074 You may define new actions to be triggered by the ``@action`` form variable.
4075 These are added to the tracker ``extensions`` directory and registered
4076 using ``instance.registerAction``.
4077
4078 All the existing Actions are defined in ``roundup.cgi.actions``.
4079
4080 Adding action classes takes three steps; first you `define the new
4081 action class`_, then you `register the action class`_ with the cgi
4082 interface so it may be triggered by the ``@action`` form variable.
4083 Finally you `use the new action`_ in your HTML form.
4084
4085 See "`setting up a "wizard" (or "druid") for controlled adding of
4086 issues`_" for an example.
4087
4088
4089 Define the new action class
4090 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4091
4092 Create a new action class in your tracker's ``extensions`` directory, for
4093 example ``myaction.py``::
4094
4095 from roundup.cgi.actions import Action
4096
4097 class MyAction(Action):
4098 def handle(self):
4099 ''' Perform some action. No return value is required.
4100 '''
4101
4102 The *self.client* attribute is an instance of ``roundup.cgi.client.Client``.
4103 See the docstring of that class for details of what it can do.
4104
4105 The method will typically check the ``self.form`` variable's contents.
4106 It may then:
4107
4108 - add information to ``self.client._ok_message``
4109 or ``self.client._error_message`` (by using ``self.client.add_ok_message``
4110 or ``self.client.add_error_message``, respectively)
4111 - change the ``self.client.template`` variable to alter what the user will see
4112 next
4113 - raise Unauthorised, SendStaticFile, SendFile, NotFound or Redirect
4114 exceptions (import them from roundup.cgi.exceptions)
4115
4116
4117 Register the action class
4118 ~~~~~~~~~~~~~~~~~~~~~~~~~~
4119
4120 The class is now written, but isn't available to the user until you register
4121 it with the following code appended to your ``myaction.py`` file::
4122
4123 def init(instance):
4124 instance.registerAction('myaction', myActionClass)
4125
4126 This maps the action name "myaction" to the action class we defined.
4127
4128
4129 Use the new action
4130 ~~~~~~~~~~~~~~~~~~
4131
4132 In your HTML form, add a hidden form element like so::
4133
4134 <input type="hidden" name="@action" value="myaction">
4135
4136 where "myaction" is the name you registered in the previous step.
4137
4138 Actions may return content to the user
4139 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4140
4141 Actions generally perform some database manipulation and then pass control
4142 on to the rendering of a template in the current context (see `Determining
4143 web context`_ for how that works.) Some actions will want to generate the
4144 actual content returned to the user. Action methods may return their own
4145 content string to be displayed to the user, overriding the templating step.
4146 In this situation, we assume that the content is HTML by default. You may
4147 override the content type indicated to the user by calling ``setHeader``::
4148
4149 self.client.setHeader('Content-Type', 'text/csv')
4150
4151 This example indicates that the value sent back to the user is actually
4152 comma-separated value content (eg. something to be loaded into a
4153 spreadsheet or database).
4154
4155
4156 8-bit character set support in Web interface
4157 --------------------------------------------
4158
4159 The web interface uses UTF-8 default. It may be overridden in both forms
4160 and a browser cookie.
4161
4162 - In forms, use the ``@charset`` variable.
4163 - To use the cookie override, have the ``roundup_charset`` cookie set.
4164
4165 In both cases, the value is a valid charset name (eg. ``utf-8`` or
4166 ``kio8-r``).
4167
4168 Inside Roundup, all strings are stored and processed in utf-8.
4169 Unfortunately, some older browsers do not work properly with
4170 utf-8-encoded pages (e.g. Netscape Navigator 4 displays wrong
4171 characters in form fields). This version allows one to change
4172 the character set for http transfers. To do so, you may add
4173 the following code to your ``page.html`` template::
4174
4175 <tal:block define="uri string:${request/base}${request/env/PATH_INFO}">
4176 <a tal:attributes="href python:request.indexargs_url(uri,
4177 {'@charset':'utf-8'})">utf-8</a>
4178 <a tal:attributes="href python:request.indexargs_url(uri,
4179 {'@charset':'koi8-r'})">koi8-r</a>
4180 </tal:block>
4181
4182 (substitute ``koi8-r`` with appropriate charset for your language).
4183 Charset preference is kept in the browser cookie ``roundup_charset``.
4184
4185 ``meta http-equiv`` lines added to the tracker templates in version 0.6.0
4186 should be changed to include actual character set name::
4187
4188 <meta http-equiv="Content-Type"
4189 tal:attributes="content string:text/html;; charset=${request/client/charset}"
4190 />
4191
4192 The charset is also sent in the http header.
4193
4194 .. _CustomExamples:
4195
4196 Examples
4197 ========
4198
4199 .. contents::
4200 :local:
4201 :depth: 2
4202
4203
4204 Changing what's stored in the database
4205 --------------------------------------
4206
4207 The following examples illustrate ways to change the information stored in
4208 the database.
4209
4210
4211 Adding a new field to the classic schema
4212 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4213
4214 This example shows how to add a simple field (a due date) to the default
4215 classic schema. It does not add any additional behaviour, such as enforcing
4216 the due date, or causing automatic actions to fire if the due date passes.
4217
4218 You add new fields by editing the ``schema.py`` file in you tracker's home.
4219 Schema changes are automatically applied to the database on the next
4220 tracker access (note that roundup-server would need to be restarted as it
4221 caches the schema).
4222
4223 .. index:: schema; example changes
4224
4225 1. Modify the ``schema.py``::
4226
4227 issue = IssueClass(db, "issue",
4228 assignedto=Link("user"), keyword=Multilink("keyword"),
4229 priority=Link("priority"), status=Link("status"),
4230 due_date=Date())
4231
4232 2. Add an edit field to the ``issue.item.html`` template::
4233
4234 <tr>
4235 <th>Due Date</th>
4236 <td tal:content="structure context/due_date/field" />
4237 </tr>
4238
4239 If you want to show only the date part of due_date then do this instead::
4240
4241 <tr>
4242 <th>Due Date</th>
4243 <td tal:content="structure python:context.due_date.field(format='%Y-%m-%d')" />
4244 </tr>
4245
4246 3. Add the property to the ``issue.index.html`` page::
4247
4248 (in the heading row)
4249 <th tal:condition="request/show/due_date">Due Date</th>
4250 (in the data row)
4251 <td tal:condition="request/show/due_date"
4252 tal:content="i/due_date" />
4253
4254 If you want format control of the display of the due date you can
4255 enter the following in the data row to show only the actual due date::
4256
4257 <td tal:condition="request/show/due_date"
4258 tal:content="python:i.due_date.pretty('%Y-%m-%d')">&nbsp;</td>
4259
4260 4. Add the property to the ``issue.search.html`` page::
4261
4262 <tr tal:define="name string:due_date">
4263 <th i18n:translate="">Due Date:</th>
4264 <td metal:use-macro="search_input"></td>
4265 <td metal:use-macro="column_input"></td>
4266 <td metal:use-macro="sort_input"></td>
4267 <td metal:use-macro="group_input"></td>
4268 </tr>
4269
4270 5. If you wish for the due date to appear in the standard views listed
4271 in the sidebar of the web interface then you'll need to add "due_date"
4272 to the columns and columns_showall lists in your ``page.html``::
4273
4274 columns string:id,activity,due_date,title,creator,status;
4275 columns_showall string:id,activity,due_date,title,creator,assignedto,status;
4276
4277 Adding a new constrained field to the classic schema
4278 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4279
4280 This example shows how to add a new constrained property (i.e. a
4281 selection of distinct values) to your tracker.
4282
4283
4284 Introduction
4285 ::::::::::::
4286
4287 To make the classic schema of Roundup useful as a TODO tracking system
4288 for a group of systems administrators, it needs an extra data field per
4289 issue: a category.
4290
4291 This would let sysadmins quickly list all TODOs in their particular area
4292 of interest without having to do complex queries, and without relying on
4293 the spelling capabilities of other sysadmins (a losing proposition at
4294 best).
4295
4296
4297 Adding a field to the database
4298 ::::::::::::::::::::::::::::::
4299
4300 This is the easiest part of the change. The category would just be a
4301 plain string, nothing fancy. To change what is in the database you need
4302 to add some lines to the ``schema.py`` file of your tracker instance.
4303 Under the comment::
4304
4305 # add any additional database schema configuration here
4306
4307 add::
4308
4309 category = Class(db, "category", name=String())
4310 category.setkey("name")
4311
4312 Here we are setting up a chunk of the database which we are calling
4313 "category". It contains a string, which we are refering to as "name" for
4314 lack of a more imaginative title. (Since "name" is one of the properties
4315 that Roundup looks for on items if you do not set a key for them, it's
4316 probably a good idea to stick with it for new classes if at all
4317 appropriate.) Then we are setting the key of this chunk of the database
4318 to be that "name". This is equivalent to an index for database types.
4319 This also means that there can only be one category with a given name.
4320
4321 Adding the above lines allows us to create categories, but they're not
4322 tied to the issues that we are going to be creating. It's just a list of
4323 categories off on its own, which isn't much use. We need to link it in
4324 with the issues. To do that, find the lines
4325 in ``schema.py`` which set up the "issue" class, and then add a link to
4326 the category::
4327
4328 issue = IssueClass(db, "issue", ... ,
4329 category=Multilink("category"), ... )
4330
4331 The ``Multilink()`` means that each issue can have many categories. If
4332 you were adding something with a one-to-one relationship to issues (such
4333 as the "assignedto" property), use ``Link()`` instead.
4334
4335 That is all you need to do to change the schema. The rest of the effort
4336 is fiddling around so you can actually use the new category.
4337
4338
4339 Populating the new category class
4340 :::::::::::::::::::::::::::::::::
4341
4342 If you haven't initialised the database with the
4343 "``roundup-admin initialise``" command, then you
4344 can add the following to the tracker ``initial_data.py``
4345 under the comment::
4346
4347 # add any additional database creation steps here - but only if you
4348 # haven't initialised the database with the admin "initialise" command
4349
4350 Add::
4351
4352 category = db.getclass('category')
4353 category.create(name="scipy")
4354 category.create(name="chaco")
4355 category.create(name="weave")
4356
4357 .. index:: roundup-admin; create entries in class
4358
4359 If the database has already been initalised, then you need to use the
4360 ``roundup-admin`` tool::
4361
4362 % roundup-admin -i <tracker home>
4363 Roundup <version> ready for input.
4364 Type "help" for help.
4365 roundup> create category name=scipy
4366 1
4367 roundup> create category name=chaco
4368 2
4369 roundup> create category name=weave
4370 3
4371 roundup> exit...
4372 There are unsaved changes. Commit them (y/N)? y
4373
4374
4375 Setting up security on the new objects
4376 ::::::::::::::::::::::::::::::::::::::
4377
4378 By default only the admin user can look at and change objects. This
4379 doesn't suit us, as we want any user to be able to create new categories
4380 as required, and obviously everyone needs to be able to view the
4381 categories of issues for it to be useful.
4382
4383 We therefore need to change the security of the category objects. This
4384 is also done in ``schema.py``.
4385
4386 There are currently two loops which set up permissions and then assign
4387 them to various roles. Simply add the new "category" to both lists::
4388
4389 # Assign the access and edit permissions for issue, file and message
4390 # to regular users now
4391 for cl in 'issue', 'file', 'msg', 'category':
4392 p = db.security.getPermission('View', cl)
4393 db.security.addPermissionToRole('User', 'View', cl)
4394 db.security.addPermissionToRole('User', 'Edit', cl)
4395 db.security.addPermissionToRole('User', 'Create', cl)
4396
4397 These lines assign the "View" and "Edit" Permissions to the "User" role,
4398 so that normal users can view and edit "category" objects.
4399
4400 This is all the work that needs to be done for the database. It will
4401 store categories, and let users view and edit them. Now on to the
4402 interface stuff.
4403
4404
4405 Changing the web left hand frame
4406 ::::::::::::::::::::::::::::::::
4407
4408 We need to give the users the ability to create new categories, and the
4409 place to put the link to this functionality is in the left hand function
4410 bar, under the "Issues" area. The file that defines how this area looks
4411 is ``html/page.html``, which is what we are going to be editing next.
4412
4413 If you look at this file you can see that it contains a lot of
4414 "classblock" sections which are chunks of HTML that will be included or
4415 excluded in the output depending on whether the condition in the
4416 classblock is met. We are going to add the category code at the end of
4417 the classblock for the *issue* class::
4418
4419 <p class="classblock"
4420 tal:condition="python:request.user.hasPermission('View', 'category')">
4421 <b>Categories</b><br>
4422 <a tal:condition="python:request.user.hasPermission('Edit', 'category')"
4423 href="category?@template=item">New Category<br></a>
4424 </p>
4425
4426 The first two lines is the classblock definition, which sets up a
4427 condition that only users who have "View" permission for the "category"
4428 object will have this section included in their output. Next comes a
4429 plain "Categories" header in bold. Everyone who can view categories will
4430 get that.
4431
4432 Next comes the link to the editing area of categories. This link will
4433 only appear if the condition - that the user has "Edit" permissions for
4434 the "category" objects - is matched. If they do have permission then
4435 they will get a link to another page which will let the user add new
4436 categories.
4437
4438 Note that if you have permission to *view* but not to *edit* categories,
4439 then all you will see is a "Categories" header with nothing underneath
4440 it. This is obviously not very good interface design, but will do for
4441 now. I just claim that it is so I can add more links in this section
4442 later on. However, to fix the problem you could change the condition in
4443 the classblock statement, so that only users with "Edit" permission
4444 would see the "Categories" stuff.
4445
4446
4447 Setting up a page to edit categories
4448 ::::::::::::::::::::::::::::::::::::
4449
4450 We defined code in the previous section which let users with the
4451 appropriate permissions see a link to a page which would let them edit
4452 conditions. Now we have to write that page.
4453
4454 The link was for the *item* template of the *category* object. This
4455 translates into Roundup looking for a file called ``category.item.html``
4456 in the ``html`` tracker directory. This is the file that we are going to
4457 write now.
4458
4459 First, we add an info tag in a comment which doesn't affect the outcome
4460 of the code at all, but is useful for debugging. If you load a page in a
4461 browser and look at the page source, you can see which sections come
4462 from which files by looking for these comments::
4463
4464 <!-- category.item -->
4465
4466 Next we need to add in the METAL macro stuff so we get the normal page
4467 trappings::
4468
4469 <tal:block metal:use-macro="templates/page/macros/icing">
4470 <title metal:fill-slot="head_title">Category editing</title>
4471 <td class="page-header-top" metal:fill-slot="body_title">
4472 <h2>Category editing</h2>
4473 </td>
4474 <td class="content" metal:fill-slot="content">
4475
4476 Next we need to setup up a standard HTML form, which is the whole
4477 purpose of this file. We link to some handy javascript which sends the
4478 form through only once. This is to stop users hitting the send button
4479 multiple times when they are impatient and thus having the form sent
4480 multiple times::
4481
4482 <form method="POST" onSubmit="return submit_once()"
4483 enctype="multipart/form-data">
4484
4485 Next we define some code which sets up the minimum list of fields that
4486 we require the user to enter. There will be only one field - "name" - so
4487 they better put something in it, otherwise the whole form is pointless::
4488
4489 <input type="hidden" name="@required" value="name">
4490
4491 To get everything to line up properly we will put everything in a table,
4492 and put a nice big header on it so the user has an idea what is
4493 happening::
4494
4495 <table class="form">
4496 <tr><th class="header" colspan="2">Category</th></tr>
4497
4498 Next, we need the field into which the user is going to enter the new
4499 category. The ``context.name.field(size=60)`` bit tells Roundup to
4500 generate a normal HTML field of size 60, and the contents of that field
4501 will be the "name" variable of the current context (namely "category").
4502 The upshot of this is that when the user types something in
4503 to the form, a new category will be created with that name::
4504
4505 <tr>
4506 <th>Name</th>
4507 <td tal:content="structure python:context.name.field(size=60)">
4508 name</td>
4509 </tr>
4510
4511 Then a submit button so that the user can submit the new category::
4512
4513 <tr>
4514 <td>&nbsp;</td>
4515 <td colspan="3" tal:content="structure context/submit">
4516 submit button will go here
4517 </td>
4518 </tr>
4519
4520 The ``context/submit`` bit generates the submit button but also
4521 generates the @action and @csrf hidden fields. The @action field is
4522 used to tell Roundup how to process the form. The @csrf field provides
4523 a unique single use token to defend against CSRF attacks. (More about
4524 anti-csrf measures can be found in ``upgrading.txt``.)
4525
4526 Finally we finish off the tags we used at the start to do the METAL
4527 stuff::
4528
4529 </td>
4530 </tal:block>
4531
4532 So putting it all together, and closing the table and form we get::
4533
4534 <!-- category.item -->
4535 <tal:block metal:use-macro="templates/page/macros/icing">
4536 <title metal:fill-slot="head_title">Category editing</title>
4537 <td class="page-header-top" metal:fill-slot="body_title">
4538 <h2>Category editing</h2>
4539 </td>
4540 <td class="content" metal:fill-slot="content">
4541 <form method="POST" onSubmit="return submit_once()"
4542 enctype="multipart/form-data">
4543
4544 <table class="form">
4545 <tr><th class="header" colspan="2">Category</th></tr>
4546
4547 <tr>
4548 <th>Name</th>
4549 <td tal:content="structure python:context.name.field(size=60)">
4550 name</td>
4551 </tr>
4552
4553 <tr>
4554 <td>
4555 &nbsp;
4556 <input type="hidden" name="@required" value="name">
4557 </td>
4558 <td colspan="3" tal:content="structure context/submit">
4559 submit button will go here
4560 </td>
4561 </tr>
4562 </table>
4563 </form>
4564 </td>
4565 </tal:block>
4566
4567 This is quite a lot to just ask the user one simple question, but there
4568 is a lot of setup for basically one line (the form line) to do its work.
4569 To add another field to "category" would involve one more line (well,
4570 maybe a few extra to get the formatting correct).
4571
4572
4573 Adding the category to the issue
4574 ::::::::::::::::::::::::::::::::
4575
4576 We now have the ability to create issues to our heart's content, but
4577 that is pointless unless we can assign categories to issues. Just like
4578 the ``html/category.item.html`` file was used to define how to add a new
4579 category, the ``html/issue.item.html`` is used to define how a new issue
4580 is created.
4581
4582 Just like ``category.issue.html``, this file defines a form which has a
4583 table to lay things out. It doesn't matter where in the table we add new
4584 stuff, it is entirely up to your sense of aesthetics::
4585
4586 <th>Category</th>
4587 <td>
4588 <span tal:replace="structure context/category/field" />
4589 <span tal:replace="structure python:db.category.classhelp('name',
4590 property='category', width='200')" />
4591 </td>
4592
4593 First, we define a nice header so that the user knows what the next
4594 section is, then the middle line does what we are most interested in.
4595 This ``context/category/field`` gets replaced by a field which contains
4596 the category in the current context (the current context being the new
4597 issue).
4598
4599 The classhelp lines generate a link (labelled "list") to a popup window
4600 which contains the list of currently known categories.
4601
4602
4603 Searching on categories
4604 :::::::::::::::::::::::
4605
4606 Now we can add categories, and create issues with categories. The next
4607 obvious thing that we would like to be able to do, would be to search
4608 for issues based on their category, so that, for example, anyone working
4609 on the web server could look at all issues in the category "Web".
4610
4611 If you look for "Search Issues" in the ``html/page.html`` file, you will
4612 find that it looks something like
4613 ``<a href="issue?@template=search">Search Issues</a>``. This shows us
4614 that when you click on "Search Issues" it will be looking for a
4615 ``issue.search.html`` file to display. So that is the file that we will
4616 change.
4617
4618 If you look at this file it should begin to seem familiar, although it
4619 does use some new macros. You can add the new category search code anywhere you
4620 like within that form::
4621
4622 <tr tal:define="name string:category;
4623 db_klass string:category;
4624 db_content string:name;">
4625 <th>Priority:</th>
4626 <td metal:use-macro="search_select"></td>
4627 <td metal:use-macro="column_input"></td>
4628 <td metal:use-macro="sort_input"></td>
4629 <td metal:use-macro="group_input"></td>
4630 </tr>
4631
4632 The definitions in the ``<tr>`` opening tag are used by the macros:
4633
4634 - ``search_select`` expands to a drop-down box with all categories using
4635 ``db_klass`` and ``db_content``.
4636 - ``column_input`` expands to a checkbox for selecting what columns
4637 should be displayed.
4638 - ``sort_input`` expands to a radio button for selecting what property
4639 should be sorted on.
4640 - ``group_input`` expands to a radio button for selecting what property
4641 should be grouped on.
4642
4643 The category search code above would expand to the following::
4644
4645 <tr>
4646 <th>Category:</th>
4647 <td>
4648 <select name="category">
4649 <option value="">don't care</option>
4650 <option value="">------------</option>
4651 <option value="1">scipy</option>
4652 <option value="2">chaco</option>
4653 <option value="3">weave</option>
4654 </select>
4655 </td>
4656 <td><input type="checkbox" name=":columns" value="category"></td>
4657 <td><input type="radio" name=":sort0" value="category"></td>
4658 <td><input type="radio" name=":group0" value="category"></td>
4659 </tr>
4660
4661 Adding category to the default view
4662 :::::::::::::::::::::::::::::::::::
4663
4664 We can now add categories, add issues with categories, and search for
4665 issues based on categories. This is everything that we need to do;
4666 however, there is some more icing that we would like. I think the
4667 category of an issue is important enough that it should be displayed by
4668 default when listing all the issues.
4669
4670 Unfortunately, this is a bit less obvious than the previous steps. The
4671 code defining how the issues look is in ``html/issue.index.html``. This
4672 is a large table with a form down at the bottom for redisplaying and so
4673 forth.
4674
4675 Firstly we need to add an appropriate header to the start of the table::
4676
4677 <th tal:condition="request/show/category">Category</th>
4678
4679 The *condition* part of this statement is to avoid displaying the
4680 Category column if the user has selected not to see it.
4681
4682 The rest of the table is a loop which will go through every issue that
4683 matches the display criteria. The loop variable is "i" - which means
4684 that every issue gets assigned to "i" in turn.
4685
4686 The new part of code to display the category will look like this::
4687
4688 <td tal:condition="request/show/category"
4689 tal:content="i/category"></td>
4690
4691 The condition is the same as above: only display the condition when the
4692 user hasn't asked for it to be hidden. The next part is to set the
4693 content of the cell to be the category part of "i" - the current issue.
4694
4695 Finally we have to edit ``html/page.html`` again. This time, we need to
4696 tell it that when the user clicks on "Unassigned Issues" or "All Issues",
4697 the category column should be included in the resulting list. If you
4698 scroll down the page file, you can see the links with lots of options.
4699 The option that we are interested in is the ``:columns=`` one which
4700 tells Roundup which fields of the issue to display. Simply add
4701 "category" to that list and it all should work.
4702
4703 Adding a time log to your issues
4704 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4705
4706 We want to log the dates and amount of time spent working on issues, and
4707 be able to give a summary of the total time spent on a particular issue.
4708
4709 1. Add a new class to your tracker ``schema.py``::
4710
4711 # storage for time logging
4712 timelog = Class(db, "timelog", period=Interval())
4713
4714 Note that we automatically get the date of the time log entry
4715 creation through the standard property "creation".
4716
4717 You will need to grant "Creation" permission to the users who are
4718 allowed to add timelog entries. You may do this with::
4719
4720 db.security.addPermissionToRole('User', 'Create', 'timelog')
4721 db.security.addPermissionToRole('User', 'View', 'timelog')
4722
4723 If users are also able to *edit* timelog entries, then also include::
4724
4725 db.security.addPermissionToRole('User', 'Edit', 'timelog')
4726
4727 .. index:: schema; example changes
4728
4729 2. Link to the new class from your issue class (again, in
4730 ``schema.py``)::
4731
4732 issue = IssueClass(db, "issue",
4733 assignedto=Link("user"), keyword=Multilink("keyword"),
4734 priority=Link("priority"), status=Link("status"),
4735 times=Multilink("timelog"))
4736
4737 the "times" property is the new link to the "timelog" class.
4738
4739 3. We'll need to let people add in times to the issue, so in the web
4740 interface we'll have a new entry field. This is a special field
4741 because unlike the other fields in the ``issue.item`` template, it
4742 affects a different item (a timelog item) and not the template's
4743 item (an issue). We have a special syntax for form fields that affect
4744 items other than the template default item (see the cgi
4745 documentation on `special form variables`_). In particular, we add a
4746 field to capture a new timelog item's period::
4747
4748 <tr>
4749 <th>Time Log</th>
4750 <td colspan=3><input type="text" name="timelog-1@period" />
4751 (enter as '3y 1m 4d 2:40:02' or parts thereof)
4752 </td>
4753 </tr>
4754
4755 and another hidden field that links that new timelog item (new
4756 because it's marked as having id "-1") to the issue item. It looks
4757 like this::
4758
4759 <input type="hidden" name="@link@times" value="timelog-1" />
4760
4761 On submission, the "-1" timelog item will be created and assigned a
4762 real item id. The "times" property of the issue will have the new id
4763 added to it.
4764
4765 The full entry will now look like this::
4766
4767 <tr>
4768 <th>Time Log</th>
4769 <td colspan=3><input type="text" name="timelog-1@period" />
4770 (enter as '3y 1m 4d 2:40:02' or parts thereof)
4771 <input type="hidden" name="@link@times" value="timelog-1" />
4772 </td>
4773 </tr>
4774
4775
4776 4. We want to display a total of the timelog times that have been
4777 accumulated for an issue. To do this, we'll need to actually write
4778 some Python code, since it's beyond the scope of PageTemplates to
4779 perform such calculations. We do this by adding a module ``timespent.py``
4780 to the ``extensions`` directory in our tracker. The contents of this
4781 file is as follows::
4782
4783 from roundup import date
4784
4785 def totalTimeSpent(times):
4786 ''' Call me with a list of timelog items (which have an
4787 Interval "period" property)
4788 '''
4789 total = date.Interval('0d')
4790 for time in times:
4791 total += time.period._value
4792 return total
4793
4794 def init(instance):
4795 instance.registerUtil('totalTimeSpent', totalTimeSpent)
4796
4797 We will now be able to access the ``totalTimeSpent`` function via the
4798 ``utils`` variable in our templates, as shown in the next step.
4799
4800 5. Display the timelog for an issue::
4801
4802 <table class="otherinfo" tal:condition="context/times">
4803 <tr><th colspan="3" class="header">Time Log
4804 <tal:block
4805 tal:replace="python:utils.totalTimeSpent(context.times)" />
4806 </th></tr>
4807 <tr><th>Date</th><th>Period</th><th>Logged By</th></tr>
4808 <tr tal:repeat="time context/times">
4809 <td tal:content="time/creation"></td>
4810 <td tal:content="time/period"></td>
4811 <td tal:content="time/creator"></td>
4812 </tr>
4813 </table>
4814
4815 I put this just above the Messages log in my issue display. Note our
4816 use of the ``totalTimeSpent`` method which will total up the times
4817 for the issue and return a new Interval. That will be automatically
4818 displayed in the template as text like "+ 1y 2:40" (1 year, 2 hours
4819 and 40 minutes).
4820
4821 6. If you're using a persistent web server - ``roundup-server`` or
4822 ``mod_wsgi`` for example - then you'll need to restart that to pick up
4823 the code changes. When that's done, you'll be able to use the new
4824 time logging interface.
4825
4826 An extension of this modification attaches the timelog entries to any
4827 change message entered at the time of the timelog entry:
4828
4829 A. Add a link to the timelog to the msg class in ``schema.py``:
4830
4831 msg = FileClass(db, "msg",
4832 author=Link("user", do_journal='no'),
4833 recipients=Multilink("user", do_journal='no'),
4834 date=Date(),
4835 summary=String(),
4836 files=Multilink("file"),
4837 messageid=String(),
4838 inreplyto=String(),
4839 times=Multilink("timelog"))
4840
4841 B. Add a new hidden field that links that new timelog item (new
4842 because it's marked as having id "-1") to the new message.
4843 The link is placed in ``issue.item.html`` in the same section that
4844 handles the timelog entry.
4845
4846 It looks like this after this addition::
4847
4848 <tr>
4849 <th>Time Log</th>
4850 <td colspan=3><input type="text" name="timelog-1@period" />
4851 (enter as '3y 1m 4d 2:40:02' or parts thereof)
4852 <input type="hidden" name="@link@times" value="timelog-1" />
4853 <input type="hidden" name="msg-1@link@times" value="timelog-1" />
4854 </td>
4855 </tr>
4856
4857 The "times" property of the message will have the new id added to it.
4858
4859 C. Add the timelog listing from step 5. to the ``msg.item.html`` template
4860 so that the timelog entry appears on the message view page. Note that
4861 the call to totalTimeSpent is not used here since there will only be one
4862 single timelog entry for each message.
4863
4864 I placed it after the Date entry like this::
4865
4866 <tr>
4867 <th i18n:translate="">Date:</th>
4868 <td tal:content="context/date"></td>
4869 </tr>
4870 </table>
4871
4872 <table class="otherinfo" tal:condition="context/times">
4873 <tr><th colspan="3" class="header">Time Log</th></tr>
4874 <tr><th>Date</th><th>Period</th><th>Logged By</th></tr>
4875 <tr tal:repeat="time context/times">
4876 <td tal:content="time/creation"></td>
4877 <td tal:content="time/period"></td>
4878 <td tal:content="time/creator"></td>
4879 </tr>
4880 </table>
4881
4882 <table class="messages">
4883
4884
4885 Tracking different types of issues
4886 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4887
4888 Sometimes you will want to track different types of issues - developer,
4889 customer support, systems, sales leads, etc. A single Roundup tracker is
4890 able to support multiple types of issues. This example demonstrates adding
4891 a system support issue class to a tracker.
4892
4893 1. Figure out what information you're going to want to capture. OK, so
4894 this is obvious, but sometimes it's better to actually sit down for a
4895 while and think about the schema you're going to implement.
4896
4897 2. Add the new issue class to your tracker's ``schema.py``. Just after the
4898 "issue" class definition, add::
4899
4900 # list our systems
4901 system = Class(db, "system", name=String(), order=Number())
4902 system.setkey("name")
4903
4904 # store issues related to those systems
4905 support = IssueClass(db, "support",
4906 assignedto=Link("user"), keyword=Multilink("keyword"),
4907 status=Link("status"), deadline=Date(),
4908 affects=Multilink("system"))
4909
4910 3. Copy the existing ``issue.*`` (item, search and index) templates in the
4911 tracker's ``html`` to ``support.*``. Edit them so they use the properties
4912 defined in the ``support`` class. Be sure to check for hidden form
4913 variables like "required" to make sure they have the correct set of
4914 required properties.
4915
4916 4. Edit the modules in the ``detectors``, adding lines to their ``init``
4917 functions where appropriate. Look for ``audit`` and ``react`` registrations
4918 on the ``issue`` class, and duplicate them for ``support``.
4919
4920 5. Create a new sidebar box for the new support class. Duplicate the
4921 existing issues one, changing the ``issue`` class name to ``support``.
4922
4923 6. Re-start your tracker and start using the new ``support`` class.
4924
4925
4926 Optionally, you might want to restrict the users able to access this new
4927 class to just the users with a new "SysAdmin" Role. To do this, we add
4928 some security declarations::
4929
4930 db.security.addPermissionToRole('SysAdmin', 'View', 'support')
4931 db.security.addPermissionToRole('SysAdmin', 'Create', 'support')
4932 db.security.addPermissionToRole('SysAdmin', 'Edit', 'support')
4933
4934 You would then (as an "admin" user) edit the details of the appropriate
4935 users, and add "SysAdmin" to their Roles list.
4936
4937 Alternatively, you might want to change the Edit/View permissions granted
4938 for the ``issue`` class so that it's only available to users with the "System"
4939 or "Developer" Role, and then the new class you're adding is available to
4940 all with the "User" Role.
4941
4942
4943 .. _external-authentication:
4944
4945 Using External User Databases
4946 -----------------------------
4947
4948 Using an external password validation source
4949 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4950
4951 .. note:: You will need to either have an "admin" user in your external
4952 password source *or* have one of your regular users have
4953 the Admin Role assigned. If you need to assign the Role *after*
4954 making the changes below, you may use the ``roundup-admin``
4955 program to edit a user's details.
4956
4957 We have a centrally-managed password changing system for our users. This
4958 results in a UN*X passwd-style file that we use for verification of
4959 users. Entries in the file consist of ``name:password`` where the
4960 password is encrypted using the standard UN*X ``crypt()`` function (see
4961 the ``crypt`` module in your Python distribution). An example entry
4962 would be::
4963
4964 admin:aamrgyQfDFSHw
4965
4966 Each user of Roundup must still have their information stored in the Roundup
4967 database - we just use the passwd file to check their password. To do this, we
4968 need to override the standard ``verifyPassword`` method defined in
4969 ``roundup.cgi.actions.LoginAction`` and register the new class. The
4970 following is added as ``externalpassword.py`` in the tracker ``extensions``
4971 directory::
4972
4973 import os, crypt
4974 from roundup.cgi.actions import LoginAction
4975
4976 class ExternalPasswordLoginAction(LoginAction):
4977 def verifyPassword(self, userid, password):
4978 '''Look through the file, line by line, looking for a
4979 name that matches.
4980 '''
4981 # get the user's username
4982 username = self.db.user.get(userid, 'username')
4983
4984 # the passwords are stored in the "passwd.txt" file in the
4985 # tracker home
4986 file = os.path.join(self.db.config.TRACKER_HOME, 'passwd.txt')
4987
4988 # see if we can find a match
4989 for ent in [line.strip().split(':') for line in
4990 open(file).readlines()]:
4991 if ent[0] == username:
4992 return crypt.crypt(password, ent[1][:2]) == ent[1]
4993
4994 # user doesn't exist in the file
4995 return 0
4996
4997 def init(instance):
4998 instance.registerAction('login', ExternalPasswordLoginAction)
4999
5000 You should also remove the redundant password fields from the ``user.item``
5001 template.
5002
5003
5004 Using a UN*X passwd file as the user database
5005 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5006
5007 On some systems the primary store of users is the UN*X passwd file. It
5008 holds information on users such as their username, real name, password
5009 and primary user group.
5010
5011 Roundup can use this store as its primary source of user information,
5012 but it needs additional information too - email address(es), Roundup
5013 Roles, vacation flags, Roundup hyperdb item ids, etc. Also, "retired"
5014 users must still exist in the user database, unlike some passwd files in
5015 which the users are removed when they no longer have access to a system.
5016
5017 To make use of the passwd file, we therefore synchronise between the two
5018 user stores. We also use the passwd file to validate the user logins, as
5019 described in the previous example, `using an external password
5020 validation source`_. We keep the user lists in sync using a fairly
5021 simple script that runs once a day, or several times an hour if more
5022 immediate access is needed. In short, it:
5023
5024 1. parses the passwd file, finding usernames, passwords and real names,
5025 2. compares that list to the current Roundup user list:
5026
5027 a. entries no longer in the passwd file are *retired*
5028 b. entries with mismatching real names are *updated*
5029 c. entries only exist in the passwd file are *created*
5030
5031 3. send an email to administrators to let them know what's been done.
5032
5033 The retiring and updating are simple operations, requiring only a call
5034 to ``retire()`` or ``set()``. The creation operation requires more
5035 information though - the user's email address and their Roundup Roles.
5036 We're going to assume that the user's email address is the same as their
5037 login name, so we just append the domain name to that. The Roles are
5038 determined using the passwd group identifier - mapping their UN*X group
5039 to an appropriate set of Roles.
5040
5041 The script to perform all this, broken up into its main components, is
5042 as follows. Firstly, we import the necessary modules and open the
5043 tracker we're to work on::
5044
5045 import sys, os, smtplib
5046 from roundup import instance, date
5047
5048 # open the tracker
5049 tracker_home = sys.argv[1]
5050 tracker = instance.open(tracker_home)
5051
5052 Next we read in the *passwd* file from the tracker home::
5053
5054 # read in the users from the "passwd.txt" file
5055 file = os.path.join(tracker_home, 'passwd.txt')
5056 users = [x.strip().split(':') for x in open(file).readlines()]
5057
5058 Handle special users (those to ignore in the file, and those who don't
5059 appear in the file)::
5060
5061 # users to not keep ever, pre-load with the users I know aren't
5062 # "real" users
5063 ignore = ['ekmmon', 'bfast', 'csrmail']
5064
5065 # users to keep - pre-load with the roundup-specific users
5066 keep = ['comment_pool', 'network_pool', 'admin', 'dev-team',
5067 'cs_pool', 'anonymous', 'system_pool', 'automated']
5068
5069 Now we map the UN*X group numbers to the Roles that users should have::
5070
5071 roles = {
5072 '501': 'User,Tech', # tech
5073 '502': 'User', # finance
5074 '503': 'User,CSR', # customer service reps
5075 '504': 'User', # sales
5076 '505': 'User', # marketing
5077 }
5078
5079 Now we do all the work. Note that the body of the script (where we have
5080 the tracker database open) is wrapped in a ``try`` / ``finally`` clause,
5081 so that we always close the database cleanly when we're finished. So, we
5082 now do all the work::
5083
5084 # open the database
5085 db = tracker.open('admin')
5086 try:
5087 # store away messages to send to the tracker admins
5088 msg = []
5089
5090 # loop over the users list read in from the passwd file
5091 for user,passw,uid,gid,real,home,shell in users:
5092 if user in ignore:
5093 # this user shouldn't appear in our tracker
5094 continue
5095 keep.append(user)
5096 try:
5097 # see if the user exists in the tracker
5098 uid = db.user.lookup(user)
5099
5100 # yes, they do - now check the real name for correctness
5101 if real != db.user.get(uid, 'realname'):
5102 db.user.set(uid, realname=real)
5103 msg.append('FIX %s - %s'%(user, real))
5104 except KeyError:
5105 # nope, the user doesn't exist
5106 db.user.create(username=user, realname=real,
5107 address='%s@ekit-inc.com'%user, roles=roles[gid])
5108 msg.append('ADD %s - %s (%s)'%(user, real, roles[gid]))
5109
5110 # now check that all the users in the tracker are also in our
5111 # "keep" list - retire those who aren't
5112 for uid in db.user.list():
5113 user = db.user.get(uid, 'username')
5114 if user not in keep:
5115 db.user.retire(uid)
5116 msg.append('RET %s'%user)
5117
5118 # if we did work, then send email to the tracker admins
5119 if msg:
5120 # create the email
5121 msg = '''Subject: %s user database maintenance
5122
5123 %s
5124 '''%(db.config.TRACKER_NAME, '\n'.join(msg))
5125
5126 # send the email
5127 smtp = smtplib.SMTP(db.config.MAILHOST)
5128 addr = db.config.ADMIN_EMAIL
5129 smtp.sendmail(addr, addr, msg)
5130
5131 # now we're done - commit the changes
5132 db.commit()
5133 finally:
5134 # always close the database cleanly
5135 db.close()
5136
5137 And that's it!
5138
5139
5140 Using an LDAP database for user information
5141 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5142
5143 A script that reads users from an LDAP store using
5144 https://pypi.org/project/python-ldap/ and then compares the list to the users in the
5145 Roundup user database would be pretty easy to write. You'd then have it run
5146 once an hour / day (or on demand if you can work that into your LDAP store
5147 workflow). See the example `Using a UN*X passwd file as the user database`_
5148 for more information about doing this.
5149
5150 To authenticate off the LDAP store (rather than using the passwords in the
5151 Roundup user database) you'd use the same python-ldap module inside an
5152 extension to the cgi interface. You'd do this by overriding the method called
5153 ``verifyPassword`` on the ``LoginAction`` class in your tracker's
5154 ``extensions`` directory (see `using an external password validation
5155 source`_). The method is implemented by default as::
5156
5157 def verifyPassword(self, userid, password):
5158 ''' Verify the password that the user has supplied
5159 '''
5160 stored = self.db.user.get(self.userid, 'password')
5161 if password == stored:
5162 return 1
5163 if not password and not stored:
5164 return 1
5165 return 0
5166
5167 So you could reimplement this as something like::
5168
5169 def verifyPassword(self, userid, password):
5170 ''' Verify the password that the user has supplied
5171 '''
5172 # look up some unique LDAP information about the user
5173 username = self.db.user.get(self.userid, 'username')
5174 # now verify the password supplied against the LDAP store
5175
5176
5177 Changes to Tracker Behaviour
5178 ----------------------------
5179
5180 .. index:: single: auditors; how to register (example)
5181 single: reactors; how to register (example)
5182
5183 Preventing SPAM
5184 ~~~~~~~~~~~~~~~
5185
5186 The following detector code may be installed in your tracker's
5187 ``detectors`` directory. It will block any messages being created that
5188 have HTML attachments (a very common vector for spam and phishing)
5189 and any messages that have more than 2 HTTP URLs in them. Just copy
5190 the following into ``detectors/anti_spam.py`` in your tracker::
5191
5192 from roundup.exceptions import Reject
5193
5194 def reject_html(db, cl, nodeid, newvalues):
5195 if newvalues['type'] == 'text/html':
5196 raise Reject('not allowed')
5197
5198 def reject_manylinks(db, cl, nodeid, newvalues):
5199 content = newvalues['content']
5200 if content.count('http://') > 2:
5201 raise Reject('not allowed')
5202
5203 def init(db):
5204 db.file.audit('create', reject_html)
5205 db.msg.audit('create', reject_manylinks)
5206
5207 You may also wish to block image attachments if your tracker does not
5208 need that ability::
5209
5210 if newvalues['type'].startswith('image/'):
5211 raise Reject('not allowed')
5212
5213
5214 Stop "nosy" messages going to people on vacation
5215 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5216
5217 When users go on vacation and set up vacation email bouncing, you'll
5218 start to see a lot of messages come back through Roundup "Fred is on
5219 vacation". Not very useful, and relatively easy to stop.
5220
5221 1. add a "vacation" flag to your users::
5222
5223 user = Class(db, "user",
5224 username=String(), password=Password(),
5225 address=String(), realname=String(),
5226 phone=String(), organisation=String(),
5227 alternate_addresses=String(),
5228 roles=String(), queries=Multilink("query"),
5229 vacation=Boolean())
5230
5231 2. So that users may edit the vacation flags, add something like the
5232 following to your ``user.item`` template::
5233
5234 <tr>
5235 <th>On Vacation</th>
5236 <td tal:content="structure context/vacation/field">vacation</td>
5237 </tr>
5238
5239 3. edit your detector ``nosyreactor.py`` so that the ``nosyreaction()``
5240 consists of::
5241
5242 def nosyreaction(db, cl, nodeid, oldvalues):
5243 users = db.user
5244 messages = db.msg
5245 # send a copy of all new messages to the nosy list
5246 for msgid in determineNewMessages(cl, nodeid, oldvalues):
5247 try:
5248 # figure the recipient ids
5249 sendto = []
5250 seen_message = {}
5251 recipients = messages.get(msgid, 'recipients')
5252 for recipid in messages.get(msgid, 'recipients'):
5253 seen_message[recipid] = 1
5254
5255 # figure the author's id, and indicate they've received
5256 # the message
5257 authid = messages.get(msgid, 'author')
5258
5259 # possibly send the message to the author, as long as
5260 # they aren't anonymous
5261 if (db.config.MESSAGES_TO_AUTHOR == 'yes' and
5262 users.get(authid, 'username') != 'anonymous'):
5263 sendto.append(authid)
5264 seen_message[authid] = 1
5265
5266 # now figure the nosy people who weren't recipients
5267 nosy = cl.get(nodeid, 'nosy')
5268 for nosyid in nosy:
5269 # Don't send nosy mail to the anonymous user (that
5270 # user shouldn't appear in the nosy list, but just
5271 # in case they do...)
5272 if users.get(nosyid, 'username') == 'anonymous':
5273 continue
5274 # make sure they haven't seen the message already
5275 if nosyid not in seen_message:
5276 # send it to them
5277 sendto.append(nosyid)
5278 recipients.append(nosyid)
5279
5280 # generate a change note
5281 if oldvalues:
5282 note = cl.generateChangeNote(nodeid, oldvalues)
5283 else:
5284 note = cl.generateCreateNote(nodeid)
5285
5286 # we have new recipients
5287 if sendto:
5288 # filter out the people on vacation
5289 sendto = [i for i in sendto
5290 if not users.get(i, 'vacation', 0)]
5291
5292 # map userids to addresses
5293 sendto = [users.get(i, 'address') for i in sendto]
5294
5295 # update the message's recipients list
5296 messages.set(msgid, recipients=recipients)
5297
5298 # send the message
5299 cl.send_message(nodeid, msgid, note, sendto)
5300 except roundupdb.MessageSendError as message:
5301 raise roundupdb.DetectorError(message)
5302
5303 Note that this is the standard nosy reaction code, with the small
5304 addition of::
5305
5306 # filter out the people on vacation
5307 sendto = [i for i in sendto if not users.get(i, 'vacation', 0)]
5308
5309 which filters out the users that have the vacation flag set to true.
5310
5311 Adding in state transition control
5312 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5313
5314 Sometimes tracker admins want to control the states to which users may
5315 move issues. You can do this by following these steps:
5316
5317 1. make "status" a required variable. This is achieved by adding the
5318 following to the top of the form in the ``issue.item.html``
5319 template::
5320
5321 <input type="hidden" name="@required" value="status">
5322
5323 This will force users to select a status.
5324
5325 2. add a Multilink property to the status class::
5326
5327 stat = Class(db, "status", ... , transitions=Multilink('status'),
5328 ...)
5329
5330 and then edit the statuses already created, either:
5331
5332 a. through the web using the class list -> status class editor, or
5333 b. using the ``roundup-admin`` "set" command.
5334
5335 3. add an auditor module ``checktransition.py`` in your tracker's
5336 ``detectors`` directory, for example::
5337
5338 def checktransition(db, cl, nodeid, newvalues):
5339 ''' Check that the desired transition is valid for the "status"
5340 property.
5341 '''
5342 if 'status' not in newvalues:
5343 return
5344 current = cl.get(nodeid, 'status')
5345 new = newvalues['status']
5346 if new == current:
5347 return
5348 ok = db.status.get(current, 'transitions')
5349 if new not in ok:
5350 raise ValueError('Status not allowed to move from "%s" to "%s"'%(
5351 db.status.get(current, 'name'), db.status.get(new, 'name')))
5352
5353 def init(db):
5354 db.issue.audit('set', checktransition)
5355
5356 4. in the ``issue.item.html`` template, change the status editing bit
5357 from::
5358
5359 <th>Status</th>
5360 <td tal:content="structure context/status/menu">status</td>
5361
5362 to::
5363
5364 <th>Status</th>
5365 <td>
5366 <select tal:condition="context/id" name="status">
5367 <tal:block tal:define="ok context/status/transitions"
5368 tal:repeat="state db/status/list">
5369 <option tal:condition="python:state.id in ok"
5370 tal:attributes="
5371 value state/id;
5372 selected python:state.id == context.status.id"
5373 tal:content="state/name"></option>
5374 </tal:block>
5375 </select>
5376 <tal:block tal:condition="not:context/id"
5377 tal:replace="structure context/status/menu" />
5378 </td>
5379
5380 which displays only the allowed status to transition to.
5381
5382
5383 Blocking issues that depend on other issues
5384 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5385
5386 We needed the ability to mark certain issues as "blockers" - that is,
5387 they can't be resolved until another issue (the blocker) they rely on is
5388 resolved. To achieve this:
5389
5390 1. Create a new property on the ``issue`` class:
5391 ``blockers=Multilink("issue")``. To do this, edit the definition of
5392 this class in your tracker's ``schema.py`` file. Change this::
5393
5394 issue = IssueClass(db, "issue",
5395 assignedto=Link("user"), keyword=Multilink("keyword"),
5396 priority=Link("priority"), status=Link("status"))
5397
5398 to this, adding the blockers entry::
5399
5400 issue = IssueClass(db, "issue",
5401 blockers=Multilink("issue"),
5402 assignedto=Link("user"), keyword=Multilink("keyword"),
5403 priority=Link("priority"), status=Link("status"))
5404
5405 2. Add the new ``blockers`` property to the ``issue.item.html`` edit
5406 page, using something like::
5407
5408 <th>Waiting On</th>
5409 <td>
5410 <span tal:replace="structure python:context.blockers.field(showid=1,
5411 size=20)" />
5412 <span tal:replace="structure python:db.issue.classhelp('id,title',
5413 property='blockers')" />
5414 <span tal:condition="context/blockers"
5415 tal:repeat="blk context/blockers">
5416 <br>View: <a tal:attributes="href string:issue${blk/id}"
5417 tal:content="blk/id"></a>
5418 </span>
5419 </td>
5420
5421 You'll need to fiddle with your item page layout to find an
5422 appropriate place to put it - I'll leave that fun part up to you.
5423 Just make sure it appears in the first table, possibly somewhere near
5424 the "superseders" field.
5425
5426 3. Create a new detector module (see below) which enforces the rules:
5427
5428 - issues may not be resolved if they have blockers
5429 - when a blocker is resolved, it's removed from issues it blocks
5430
5431 The contents of the detector should be something like this::
5432
5433
5434 def blockresolution(db, cl, nodeid, newvalues):
5435 ''' If the issue has blockers, don't allow it to be resolved.
5436 '''
5437 if nodeid is None:
5438 blockers = []
5439 else:
5440 blockers = cl.get(nodeid, 'blockers')
5441 blockers = newvalues.get('blockers', blockers)
5442
5443 # don't do anything if there's no blockers or the status hasn't
5444 # changed
5445 if not blockers or 'status' not in newvalues:
5446 return
5447
5448 # get the resolved state ID
5449 resolved_id = db.status.lookup('resolved')
5450
5451 # format the info
5452 u = db.config.TRACKER_WEB
5453 s = ', '.join(['<a href="%sissue%s">%s</a>'%(
5454 u,id,id) for id in blockers])
5455 if len(blockers) == 1:
5456 s = 'issue %s is'%s
5457 else:
5458 s = 'issues %s are'%s
5459
5460 # ok, see if we're trying to resolve
5461 if newvalues['status'] == resolved_id:
5462 raise ValueError("This issue can't be resolved until %s resolved."%s)
5463
5464
5465 def resolveblockers(db, cl, nodeid, oldvalues):
5466 ''' When we resolve an issue that's a blocker, remove it from the
5467 blockers list of the issue(s) it blocks.
5468 '''
5469 newstatus = cl.get(nodeid,'status')
5470
5471 # no change?
5472 if oldvalues.get('status', None) == newstatus:
5473 return
5474
5475 resolved_id = db.status.lookup('resolved')
5476
5477 # interesting?
5478 if newstatus != resolved_id:
5479 return
5480
5481 # yes - find all the blocked issues, if any, and remove me from
5482 # their blockers list
5483 issues = cl.find(blockers=nodeid)
5484 for issueid in issues:
5485 blockers = cl.get(issueid, 'blockers')
5486 if nodeid in blockers:
5487 blockers.remove(nodeid)
5488 cl.set(issueid, blockers=blockers)
5489
5490 def init(db):
5491 # might, in an obscure situation, happen in a create
5492 db.issue.audit('create', blockresolution)
5493 db.issue.audit('set', blockresolution)
5494
5495 # can only happen on a set
5496 db.issue.react('set', resolveblockers)
5497
5498 Put the above code in a file called "blockers.py" in your tracker's
5499 "detectors" directory.
5500
5501 4. Finally, and this is an optional step, modify the tracker web page
5502 URLs so they filter out issues with any blockers. You do this by
5503 adding an additional filter on "blockers" for the value "-1". For
5504 example, the existing "Show All" link in the "page" template (in the
5505 tracker's "html" directory) looks like this::
5506
5507 <a href="#"
5508 tal:attributes="href python:request.indexargs_url('issue', {
5509 '@sort': '-activity',
5510 '@group': 'priority',
5511 '@filter': 'status',
5512 '@columns': columns_showall,
5513 '@search_text': '',
5514 'status': status_notresolved,
5515 '@dispname': i18n.gettext('Show All'),
5516 })"
5517 i18n:translate="">Show All</a><br>
5518
5519 modify it to add the "blockers" info to the URL (note, both the
5520 "@filter" *and* "blockers" values must be specified)::
5521
5522 <a href="#"
5523 tal:attributes="href python:request.indexargs_url('issue', {
5524 '@sort': '-activity',
5525 '@group': 'priority',
5526 '@filter': 'status,blockers',
5527 '@columns': columns_showall,
5528 '@search_text': '',
5529 'status': status_notresolved,
5530 'blockers': '-1',
5531 '@dispname': i18n.gettext('Show All'),
5532 })"
5533 i18n:translate="">Show All</a><br>
5534
5535 The above examples are line-wrapped on the trailing & and should
5536 be unwrapped.
5537
5538 That's it. You should now be able to set blockers on your issues. Note
5539 that if you want to know whether an issue has any other issues dependent
5540 on it (i.e. it's in their blockers list) you can look at the journal
5541 history at the bottom of the issue page - look for a "link" event to
5542 another issue's "blockers" property.
5543
5544 Add users to the nosy list based on the keyword
5545 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5546
5547 Let's say we need the ability to automatically add users to the nosy
5548 list based
5549 on the occurance of a keyword. Every user should be allowed to edit their
5550 own list of keywords for which they want to be added to the nosy list.
5551
5552 Below, we'll show that this change can be done with minimal
5553 understanding of the Roundup system, using only copy and paste.
5554
5555 This requires three changes to the tracker: a change in the database to
5556 allow per-user recording of the lists of keywords for which he wants to
5557 be put on the nosy list, a change in the user view allowing them to edit
5558 this list of keywords, and addition of an auditor which updates the nosy
5559 list when a keyword is set.
5560
5561 Adding the nosy keyword list
5562 ::::::::::::::::::::::::::::
5563
5564 The change to make in the database, is that for any user there should be a list
5565 of keywords for which he wants to be put on the nosy list. Adding a
5566 ``Multilink`` of ``keyword`` seems to fullfill this. As such, all that has to
5567 be done is to add a new field to the definition of ``user`` within the file
5568 ``schema.py``. We will call this new field ``nosy_keywords``, and the updated
5569 definition of user will be::
5570
5571 user = Class(db, "user",
5572 username=String(), password=Password(),
5573 address=String(), realname=String(),
5574 phone=String(), organisation=String(),
5575 alternate_addresses=String(),
5576 queries=Multilink('query'), roles=String(),
5577 timezone=String(),
5578 nosy_keywords=Multilink('keyword'))
5579
5580 Changing the user view to allow changing the nosy keyword list
5581 ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
5582
5583 We want any user to be able to change the list of keywords for which
5584 he will by default be added to the nosy list. We choose to add this
5585 to the user view, as is generated by the file ``html/user.item.html``.
5586 We can easily
5587 see that the keyword field in the issue view has very similar editing
5588 requirements as our nosy keywords, both being lists of keywords. As
5589 such, we look for Keywords in ``issue.item.html``, and extract the
5590 associated parts from there. We add this to ``user.item.html`` at the
5591 bottom of the list of viewed items (i.e. just below the 'Alternate
5592 E-mail addresses' in the classic template)::
5593
5594 <tr>
5595 <th>Nosy Keywords</th>
5596 <td>
5597 <span tal:replace="structure context/nosy_keywords/field" />
5598 <span tal:replace="structure python:db.keyword.classhelp(property='nosy_keywords')" />
5599 </td>
5600 </tr>
5601
5602
5603 Addition of an auditor to update the nosy list
5604 ::::::::::::::::::::::::::::::::::::::::::::::
5605
5606 The more difficult part is the logic to add
5607 the users to the nosy list when required.
5608 We choose to perform this action whenever the keywords on an
5609 item are set (this includes the creation of items).
5610 Here we choose to start out with a copy of the
5611 ``detectors/nosyreaction.py`` detector, which we copy to the file
5612 ``detectors/nosy_keyword_reaction.py``.
5613 This looks like a good start as it also adds users
5614 to the nosy list. A look through the code reveals that the
5615 ``nosyreaction`` function actually sends the e-mail.
5616 We don't need this. Therefore, we can change the ``init`` function to::
5617
5618 def init(db):
5619 db.issue.audit('create', update_kw_nosy)
5620 db.issue.audit('set', update_kw_nosy)
5621
5622 After that, we rename the ``updatenosy`` function to ``update_kw_nosy``.
5623 The first two blocks of code in that function relate to setting
5624 ``current`` to a combination of the old and new nosy lists. This
5625 functionality is left in the new auditor. The following block of
5626 code, which handled adding the assignedto user(s) to the nosy list in
5627 ``updatenosy``, should be replaced by a block of code to add the
5628 interested users to the nosy list. We choose here to loop over all
5629 new keywords, than looping over all users,
5630 and assign the user to the nosy list when the keyword occurs in the user's
5631 ``nosy_keywords``. The next part in ``updatenosy`` -- adding the author
5632 and/or recipients of a message to the nosy list -- is obviously not
5633 relevant here and is thus deleted from the new auditor. The last
5634 part, copying the new nosy list to ``newvalues``, can stay as is.
5635 This results in the following function::
5636
5637 def update_kw_nosy(db, cl, nodeid, newvalues):
5638 '''Update the nosy list for changes to the keywords
5639 '''
5640 # nodeid will be None if this is a new node
5641 current = {}
5642 if nodeid is None:
5643 ok = ('new', 'yes')
5644 else:
5645 ok = ('yes',)
5646 # old node, get the current values from the node if they haven't
5647 # changed
5648 if 'nosy' not in newvalues:
5649 nosy = cl.get(nodeid, 'nosy')
5650 for value in nosy:
5651 if value not in current:
5652 current[value] = 1
5653
5654 # if the nosy list changed in this transaction, init from the new value
5655 if 'nosy' in newvalues:
5656 nosy = newvalues.get('nosy', [])
5657 for value in nosy:
5658 if not db.hasnode('user', value):
5659 continue
5660 if value not in current:
5661 current[value] = 1
5662
5663 # add users with keyword in nosy_keywords to the nosy list
5664 if 'keyword' in newvalues and newvalues['keyword'] is not None:
5665 keyword_ids = newvalues['keyword']
5666 for keyword in keyword_ids:
5667 # loop over all users,
5668 # and assign user to nosy when keyword in nosy_keywords
5669 for user_id in db.user.list():
5670 nosy_kw = db.user.get(user_id, "nosy_keywords")
5671 found = 0
5672 for kw in nosy_kw:
5673 if kw == keyword:
5674 found = 1
5675 if found:
5676 current[user_id] = 1
5677
5678 # that's it, save off the new nosy list
5679 newvalues['nosy'] = list(current.keys())
5680
5681 These two function are the only ones needed in the file.
5682
5683 TODO: update this example to use the ``find()`` Class method.
5684
5685 Caveats
5686 :::::::
5687
5688 A few problems with the design here can be noted:
5689
5690 Multiple additions
5691 When a user, after automatic selection, is manually removed
5692 from the nosy list, he is added to the nosy list again when the
5693 keyword list of the issue is updated. A better design might be
5694 to only check which keywords are new compared to the old list
5695 of keywords, and only add users when they have indicated
5696 interest on a new keyword.
5697
5698 The code could also be changed to only trigger on the ``create()``
5699 event, rather than also on the ``set()`` event, thus only setting
5700 the nosy list when the issue is created.
5701
5702 Scalability
5703 In the auditor, there is a loop over all users. For a site with
5704 only few users this will pose no serious problem; however, with
5705 many users this will be a serious performance bottleneck.
5706 A way out would be to link from the keywords to the users who
5707 selected these keywords as nosy keywords. This will eliminate the
5708 loop over all users. See the ``rev_multilink`` attribute to make
5709 this easier.
5710
5711 Restricting updates that arrive by email
5712 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5713
5714 Roundup supports multiple update methods:
5715
5716 1. command line
5717 2. plain email
5718 3. pgp signed email
5719 4. web access
5720
5721 in some cases you may need to prevent changes to properties by some of
5722 these methods. For example you can set up issues that are viewable
5723 only by people on the nosy list. So you must prevent unauthenticated
5724 changes to the nosy list.
5725
5726 Since plain email can be easily forged, it does not provide sufficient
5727 authentication in this senario.
5728
5729 To prevent this we can add a detector that audits the source of the
5730 transaction and rejects the update if it changes the nosy list.
5731
5732 Create the detector (auditor) module and add it to the detectors
5733 directory of your tracker::
5734
5735 from roundup import roundupdb, hyperdb
5736
5737 from roundup.mailgw import Unauthorized
5738
5739 def restrict_nosy_changes(db, cl, nodeid, newvalues):
5740 '''Do not permit changes to nosy via email.'''
5741
5742 if 'nosy' not in newvalues:
5743 # the nosy field has not changed so no need to check.
5744 return
5745
5746 if db.tx_Source in ['web', 'rest', 'xmlrpc', 'email-sig-openpgp', 'cli' ]:
5747 # if the source of the transaction is from an authenticated
5748 # source or a privileged process allow the transaction.
5749 # Other possible sources: 'email'
5750 return
5751
5752 # otherwise raise an error
5753 raise Unauthorized( \
5754 'Changes to nosy property not allowed via %s for this issue.'%\
5755 tx_Source)
5756
5757 def init(db):
5758 ''' Install restrict_nosy_changes to run after other auditors.
5759
5760 Allow initial creation email to set nosy.
5761 So don't execute: db.issue.audit('create', requestedbyauditor)
5762
5763 Set priority to 110 to run this auditor after other auditors
5764 that can cause nosy to change.
5765 '''
5766 db.issue.audit('set', restrict_nosy_changes, 110)
5767
5768 This detector (auditor) will prevent updates to the nosy field if it
5769 arrives by email. Since it runs after other auditors (due to the
5770 priority of 110), it will also prevent changes to the nosy field that
5771 are done by other auditors if triggered by an email.
5772
5773 Note that db.tx_Source was not present in roundup versions before
5774 1.4.22, so you must be running a newer version to use this detector.
5775 Read the CHANGES.txt document in the roundup source code for further
5776 details on tx_Source.
5777
5778 Changes to Security and Permissions
5779 -----------------------------------
5780
5781 Restricting the list of users that are assignable to a task
5782 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5783
5784 1. In your tracker's ``schema.py``, create a new Role, say "Developer"::
5785
5786 db.security.addRole(name='Developer', description='A developer')
5787
5788 2. Just after that, create a new Permission, say "Fixer", specific to
5789 "issue"::
5790
5791 p = db.security.addPermission(name='Fixer', klass='issue',
5792 description='User is allowed to be assigned to fix issues')
5793
5794 3. Then assign the new Permission to your "Developer" Role::
5795
5796 db.security.addPermissionToRole('Developer', p)
5797
5798 4. In the issue item edit page (``html/issue.item.html`` in your tracker
5799 directory), use the new Permission in restricting the "assignedto"
5800 list::
5801
5802 <select name="assignedto">
5803 <option value="-1">- no selection -</option>
5804 <tal:block tal:repeat="user db/user/list">
5805 <option tal:condition="python:user.hasPermission(
5806 'Fixer', context._classname)"
5807 tal:attributes="
5808 value user/id;
5809 selected python:user.id == context.assignedto"
5810 tal:content="user/realname"></option>
5811 </tal:block>
5812 </select>
5813
5814 For extra security, you may wish to setup an auditor to enforce the
5815 Permission requirement (install this as ``assignedtoFixer.py`` in your
5816 tracker ``detectors`` directory)::
5817
5818 def assignedtoMustBeFixer(db, cl, nodeid, newvalues):
5819 ''' Ensure the assignedto value in newvalues is used with the
5820 Fixer Permission
5821 '''
5822 if 'assignedto' not in newvalues:
5823 # don't care
5824 return
5825
5826 # get the userid
5827 userid = newvalues['assignedto']
5828 if not db.security.hasPermission('Fixer', userid, cl.classname):
5829 raise ValueError('You do not have permission to edit %s'%cl.classname)
5830
5831 def init(db):
5832 db.issue.audit('set', assignedtoMustBeFixer)
5833 db.issue.audit('create', assignedtoMustBeFixer)
5834
5835 So now, if an edit action attempts to set "assignedto" to a user that
5836 doesn't have the "Fixer" Permission, the error will be raised.
5837
5838
5839 Users may only edit their issues
5840 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5841
5842 In this case, users registering themselves are granted Provisional
5843 access, meaning they
5844 have access to edit the issues they submit, but not others. We create a new
5845 Role called "Provisional User" which is granted to newly-registered users,
5846 and has limited access. One of the Permissions they have is the new "Edit
5847 Own" on issues (regular users have "Edit".)
5848
5849 First up, we create the new Role and Permission structure in
5850 ``schema.py``::
5851
5852 #
5853 # New users not approved by the admin
5854 #
5855 db.security.addRole(name='Provisional User',
5856 description='New user registered via web or email')
5857
5858 # These users need to be able to view and create issues but only edit
5859 # and view their own
5860 db.security.addPermissionToRole('Provisional User', 'Create', 'issue')
5861 def own_issue(db, userid, itemid):
5862 '''Determine whether the userid matches the creator of the issue.'''
5863 return userid == db.issue.get(itemid, 'creator')
5864 p = db.security.addPermission(name='Edit', klass='issue',
5865 check=own_issue, description='Can only edit own issues')
5866 db.security.addPermissionToRole('Provisional User', p)
5867 p = db.security.addPermission(name='View', klass='issue',
5868 check=own_issue, description='Can only view own issues')
5869 db.security.addPermissionToRole('Provisional User', p)
5870 # This allows the interface to get the names of the properties
5871 # in the issue. Used for selecting sorting and grouping
5872 # on the index page.
5873 p = db.security.addPermission(name='Search', klass='issue')
5874 db.security.addPermissionToRole ('Provisional User', p)
5875
5876
5877 # Assign the Permissions for issue-related classes
5878 for cl in 'file', 'msg', 'query', 'keyword':
5879 db.security.addPermissionToRole('Provisional User', 'View', cl)
5880 db.security.addPermissionToRole('Provisional User', 'Edit', cl)
5881 db.security.addPermissionToRole('Provisional User', 'Create', cl)
5882 for cl in 'priority', 'status':
5883 db.security.addPermissionToRole('Provisional User', 'View', cl)
5884
5885 # and give the new users access to the web and email interface
5886 db.security.addPermissionToRole('Provisional User', 'Web Access')
5887 db.security.addPermissionToRole('Provisional User', 'Email Access')
5888
5889 # make sure they can view & edit their own user record
5890 def own_record(db, userid, itemid):
5891 '''Determine whether the userid matches the item being accessed.'''
5892 return userid == itemid
5893 p = db.security.addPermission(name='View', klass='user', check=own_record,
5894 description="User is allowed to view their own user details")
5895 db.security.addPermissionToRole('Provisional User', p)
5896 p = db.security.addPermission(name='Edit', klass='user', check=own_record,
5897 description="User is allowed to edit their own user details")
5898 db.security.addPermissionToRole('Provisional User', p)
5899
5900 Then, in ``config.ini``, we change the Role assigned to newly-registered
5901 users, replacing the existing ``'User'`` values::
5902
5903 [main]
5904 ...
5905 new_web_user_roles = Provisional User
5906 new_email_user_roles = Provisional User
5907
5908
5909 All users may only view and edit issues, files and messages they create
5910 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5911
5912 Replace the standard "classic" tracker View and Edit Permission assignments
5913 for the "issue", "file" and "msg" classes with the following::
5914
5915 def checker(klass):
5916 def check(db, userid, itemid, klass=klass):
5917 return db.getclass(klass).get(itemid, 'creator') == userid
5918 return check
5919 for cl in 'issue', 'file', 'msg':
5920 p = db.security.addPermission(name='View', klass=cl,
5921 check=checker(cl),
5922 description='User can view only if creator.')
5923 db.security.addPermissionToRole('User', p)
5924 p = db.security.addPermission(name='Edit', klass=cl,
5925 check=checker(cl),
5926 description='User can edit only if creator.')
5927 db.security.addPermissionToRole('User', p)
5928 db.security.addPermissionToRole('User', 'Create', cl)
5929 # This allows the interface to get the names of the properties
5930 # in the issue. Used for selecting sorting and grouping
5931 # on the index page.
5932 p = db.security.addPermission(name='Search', klass='issue')
5933 db.security.addPermissionToRole ('User', p)
5934
5935
5936 Moderating user registration
5937 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5938
5939 You could set up new-user moderation in a public tracker by:
5940
5941 1. creating a new highly-restricted user role "Pending",
5942 2. set the config new_web_user_roles and/or new_email_user_roles to that
5943 role,
5944 3. have an auditor that emails you when new users are created with that
5945 role using roundup.mailer
5946 4. edit the role to "User" for valid users.
5947
5948 Some simple javascript might help in the last step. If you have high volume
5949 you could search for all currently-Pending users and do a bulk edit of all
5950 their roles at once (again probably with some simple javascript help).
5951
5952
5953 Changes to the Web User Interface
5954 ---------------------------------
5955
5956 Adding action links to the index page
5957 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5958
5959 Add a column to the ``item.index.html`` template.
5960
5961 Resolving the issue::
5962
5963 <a tal:attributes="href
5964 string:issue${i/id}?:status=resolved&:action=edit">resolve</a>
5965
5966 "Take" the issue::
5967
5968 <a tal:attributes="href
5969 string:issue${i/id}?:assignedto=${request/user/id}&:action=edit">take</a>
5970
5971 ... and so on.
5972
5973 Colouring the rows in the issue index according to priority
5974 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5975
5976 A simple ``tal:attributes`` statement will do the bulk of the work here. In
5977 the ``issue.index.html`` template, add this to the ``<tr>`` that
5978 displays the rows of data::
5979
5980 <tr tal:attributes="class string:priority-${i/priority/plain}">
5981
5982 and then in your stylesheet (``style.css``) specify the colouring for the
5983 different priorities, as follows::
5984
5985 tr.priority-critical td {
5986 background-color: red;
5987 }
5988
5989 tr.priority-urgent td {
5990 background-color: orange;
5991 }
5992
5993 and so on, with far less offensive colours :)
5994
5995 Editing multiple items in an index view
5996 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5997
5998 To edit the status of all items in the item index view, edit the
5999 ``issue.index.html``:
6000
6001 1. add a form around the listing table (separate from the existing
6002 index-page form), so at the top it reads::
6003
6004 <form method="POST" tal:attributes="action request/classname">
6005 <table class="list">
6006
6007 and at the bottom of that table::
6008
6009 </table>
6010 </form
6011
6012 making sure you match the ``</table>`` from the list table, not the
6013 navigation table or the subsequent form table.
6014
6015 2. in the display for the issue property, change::
6016
6017 <td tal:condition="request/show/status"
6018 tal:content="python:i.status.plain() or default">&nbsp;</td>
6019
6020 to::
6021
6022 <td tal:condition="request/show/status"
6023 tal:content="structure i/status/field">&nbsp;</td>
6024
6025 this will result in an edit field for the status property.
6026
6027 3. after the ``tal:block`` which lists the index items (marked by
6028 ``tal:repeat="i batch"``) add a new table row::
6029
6030 <tr>
6031 <td tal:attributes="colspan python:len(request.columns)">
6032 <input name="@csrf" type="hidden"
6033 tal:attributes="value python:utils.anti_csrf_nonce()">
6034 <input type="submit" value=" Save Changes ">
6035 <input type="hidden" name="@action" value="edit">
6036 <tal:block replace="structure request/indexargs_form" />
6037 </td>
6038 </tr>
6039
6040 which gives us a submit button, indicates that we are performing an
6041 edit on any changed statuses, and provides a defense against cross
6042 site request forgery attacks.
6043
6044 The final ``tal:block`` will make sure that the current index view
6045 parameters (filtering, columns, etc) will be used in rendering the
6046 next page (the results of the editing).
6047
6048
6049 Displaying only message summaries in the issue display
6050 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
6051
6052 Alter the ``issue.item`` template section for messages to::
6053
6054 <table class="messages" tal:condition="context/messages">
6055 <tr><th colspan="5" class="header">Messages</th></tr>
6056 <tr tal:repeat="msg context/messages">
6057 <td><a tal:attributes="href string:msg${msg/id}"
6058 tal:content="string:msg${msg/id}"></a></td>
6059 <td tal:content="msg/author">author</td>
6060 <td class="date" tal:content="msg/date/pretty">date</td>
6061 <td tal:content="msg/summary">summary</td>
6062 <td>
6063 <a tal:attributes="href string:?@remove@messages=${msg/id}&@action=edit">
6064 remove</a>
6065 </td>
6066 </tr>
6067 </table>
6068
6069
6070 Enabling display of either message summaries or the entire messages
6071 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
6072
6073 This is pretty simple - all we need to do is copy the code from the
6074 example `displaying only message summaries in the issue display`_ into
6075 our template alongside the summary display, and then introduce a switch
6076 that shows either the one or the other. We'll use a new form variable,
6077 ``@whole_messages`` to achieve this::
6078
6079 <table class="messages" tal:condition="context/messages">
6080 <tal:block tal:condition="not:request/form/@whole_messages/value | python:0">
6081 <tr><th colspan="3" class="header">Messages</th>
6082 <th colspan="2" class="header">
6083 <a href="?@whole_messages=yes">show entire messages</a>
6084 </th>
6085 </tr>
6086 <tr tal:repeat="msg context/messages">
6087 <td><a tal:attributes="href string:msg${msg/id}"
6088 tal:content="string:msg${msg/id}"></a></td>
6089 <td tal:content="msg/author">author</td>
6090 <td class="date" tal:content="msg/date/pretty">date</td>
6091 <td tal:content="msg/summary">summary</td>
6092 <td>
6093 <a tal:attributes="href string:?@remove@messages=${msg/id}&@action=edit">remove</a>
6094 </td>
6095 </tr>
6096 </tal:block>
6097
6098 <tal:block tal:condition="request/form/@whole_messages/value | python:0">
6099 <tr><th colspan="2" class="header">Messages</th>
6100 <th class="header">
6101 <a href="?@whole_messages=">show only summaries</a>
6102 </th>
6103 </tr>
6104 <tal:block tal:repeat="msg context/messages">
6105 <tr>
6106 <th tal:content="msg/author">author</th>
6107 <th class="date" tal:content="msg/date/pretty">date</th>
6108 <th style="text-align: right">
6109 (<a tal:attributes="href string:?@remove@messages=${msg/id}&@action=edit">remove</a>)
6110 </th>
6111 </tr>
6112 <tr><td colspan="3" tal:content="msg/content"></td></tr>
6113 </tal:block>
6114 </tal:block>
6115 </table>
6116
6117
6118 Setting up a "wizard" (or "druid") for controlled adding of issues
6119 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
6120
6121 1. Set up the page templates you wish to use for data input. My wizard
6122 is going to be a two-step process: first figuring out what category
6123 of issue the user is submitting, and then getting details specific to
6124 that category. The first page includes a table of help, explaining
6125 what the category names mean, and then the core of the form::
6126
6127 <form method="POST" onSubmit="return submit_once()"
6128 enctype="multipart/form-data">
6129 <input name="@csrf" type="hidden"
6130 tal:attributes="value python:utils.anti_csrf_nonce()">
6131 <input type="hidden" name="@template" value="add_page1">
6132 <input type="hidden" name="@action" value="page1_submit">
6133
6134 <strong>Category:</strong>
6135 <tal:block tal:replace="structure context/category/menu" />
6136 <input type="submit" value="Continue">
6137 </form>
6138
6139 The next page has the usual issue entry information, with the
6140 addition of the following form fragments::
6141
6142 <form method="POST" onSubmit="return submit_once()"
6143 enctype="multipart/form-data"
6144 tal:condition="context/is_edit_ok"
6145 tal:define="cat request/form/category/value">
6146
6147 <input name="@csrf" type="hidden"
6148 tal:attributes="value python:utils.anti_csrf_nonce()">
6149 <input type="hidden" name="@template" value="add_page2">
6150 <input type="hidden" name="@required" value="title">
6151 <input type="hidden" name="category" tal:attributes="value cat">
6152 .
6153 .
6154 .
6155 </form>
6156
6157 Note that later in the form, I use the value of "cat" to decide which
6158 form elements should be displayed. For example::
6159
6160 <tal:block tal:condition="python:cat in '6 10 13 14 15 16 17'.split()">
6161 <tr>
6162 <th>Operating System</th>
6163 <td tal:content="structure context/os/field"></td>
6164 </tr>
6165 <tr>
6166 <th>Web Browser</th>
6167 <td tal:content="structure context/browser/field"></td>
6168 </tr>
6169 </tal:block>
6170
6171 ... the above section will only be displayed if the category is one
6172 of 6, 10, 13, 14, 15, 16 or 17.
6173
6174 3. Determine what actions need to be taken between the pages - these are
6175 usually to validate user choices and determine what page is next. Now encode
6176 those actions in a new ``Action`` class (see `defining new web actions`_)::
6177
6178 from roundup.cgi.actions import Action
6179
6180 class Page1SubmitAction(Action):
6181 def handle(self):
6182 ''' Verify that the user has selected a category, and then move
6183 on to page 2.
6184 '''
6185 category = self.form['category'].value
6186 if category == '-1':
6187 self.client.add_error_message('You must select a category of report')
6188 return
6189 # everything's ok, move on to the next page
6190 self.client.template = 'add_page2'
6191
6192 def init(instance):
6193 instance.registerAction('page1_submit', Page1SubmitAction)
6194
6195 4. Use the usual "new" action as the ``@action`` on the final page, and
6196 you're done (the standard context/submit method can do this for you).
6197
6198
6199 Silent Submit
6200 ~~~~~~~~~~~~~
6201
6202 When working on an issue, most of the time the people on the nosy list
6203 need to be notified of changes. There are cases where a user wants to
6204 add a comment to an issue and not bother other users on the nosy
6205 list.
6206 This feature is called Silent Submit because it allows the user to
6207 silently modify an issue and not tell anyone.
6208
6209 There are several parts to this change. The main activity part
6210 involves editing the stock detectors/nosyreaction.py file in your
6211 tracker. Insert the following lines near the top of the nosyreaction
6212 function::
6213
6214 # Did user click button to do a silent change?
6215 try:
6216 if db.web['submit'] == "silent_change":
6217 return
6218 except (AttributeError, KeyError) as err:
6219 # The web attribute or submit key don't exist.
6220 # That's fine. We were probably triggered by an email
6221 # or cli based change.
6222 pass
6223
6224 This checks the submit button to see if it is the silent type. If there
6225 are exceptions trying to make that determination they are ignored and
6226 processing continues. You may wonder how db.web gets set. This is done
6227 by creating an extension. Add the file extensions/edit.py with
6228 this content::
6229
6230 from roundup.cgi.actions import EditItemAction
6231
6232 class Edit2Action(EditItemAction):
6233 def handle(self):
6234 self.db.web = {} # create the dict
6235 # populate the dict by getting the value of the submit_button
6236 # element from the form.
6237 self.db.web['submit'] = self.form['submit_button'].value
6238
6239 # call the core EditItemAction to process the edit.
6240 EditItemAction.handle(self)
6241
6242 def init(instance):
6243 '''Override the default edit action with this new version'''
6244 instance.registerAction('edit', Edit2Action)
6245
6246 This code is a wrapper for the Roundup EditItemAction. It checks the
6247 form's submit button to save the value element. The rest of the changes
6248 needed for the Silent Submit feature involves editing
6249 html/issue.item.html to add the silent submit button. In
6250 the stock issue.item.html the submit button is on a line that contains
6251 "submit button". Replace that line with something like the following::
6252
6253 <input type="submit" name="submit_button"
6254 tal:condition="context/is_edit_ok"
6255 value="Submit Changes">&nbsp;
6256 <button type="submit" name="submit_button"
6257 tal:condition="context/is_edit_ok"
6258 title="Click this to submit but not send nosy email."
6259 value="silent_change" i18n:translate="">
6260 Silent Change</button>
6261
6262 Note the difference in the value attribute for the two submit buttons.
6263 The value "silent_change" in the button specification must match the
6264 string in the nosy reaction function.
6265
6266 Debugging Trackers
6267 ==================
6268
6269 There are three switches in tracker configs that turn on debugging in
6270 Roundup:
6271
6272 1. web :: debug
6273 2. mail :: debug
6274 3. logging :: level
6275
6276 See the config.ini file or the `tracker configuration`_ section above for
6277 more information.
6278
6279 Additionally, the ``roundup-server.py`` script has its own debugging mode
6280 in which it reloads edited templates immediately when they are changed,
6281 rather than requiring a web server restart.
6282
6283 2372
6284 .. _`design documentation`: design.html 2373 .. _`design documentation`: design.html
6285 .. _`developer's guide`: developers.html 2374 .. _`developer's guide`: developers.html
6286 .. _`rest interface documentation`: rest.html#programming-the-rest-api 2375 .. _`rest interface documentation`: rest.html#programming-the-rest-api
6287 .. _change the rate limiting method: rest.html#creating-custom-rate-limits 2376 .. _change the rate limiting method: rest.html#creating-custom-rate-limits

Roundup Issue Tracker: http://roundup-tracker.org/