comparison doc/customizing.txt @ 7464:82bbb95e5690 issue2550923_computed_property

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

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