Mercurial > p > roundup > code
comparison doc/customizing.txt @ 1356:83f33642d220 maint-0.5
[[Metadata associated with this commit was garbled during conversion from CVS
to Subversion.]]
| author | Richard Jones <richard@users.sourceforge.net> |
|---|---|
| date | Thu, 09 Jan 2003 22:59:22 +0000 |
| parents | |
| children | 56c5b4509378 |
comparison
equal
deleted
inserted
replaced
| 1242:3d0158c8c32b | 1356:83f33642d220 |
|---|---|
| 1 =================== | |
| 2 Customising Roundup | |
| 3 =================== | |
| 4 | |
| 5 :Version: $Revision: 1.68 $ | |
| 6 | |
| 7 .. This document borrows from the ZopeBook section on ZPT. The original is at: | |
| 8 http://www.zope.org/Documentation/Books/ZopeBook/current/ZPT.stx | |
| 9 | |
| 10 .. contents:: | |
| 11 :depth: 1 | |
| 12 | |
| 13 What You Can Do | |
| 14 =============== | |
| 15 | |
| 16 Before you get too far, it's probably worth having a quick read of the Roundup | |
| 17 `design documentation`_. | |
| 18 | |
| 19 Customisation of Roundup can take one of five forms: | |
| 20 | |
| 21 1. `tracker configuration`_ file changes | |
| 22 2. database, or `tracker schema`_ changes | |
| 23 3. "definition" class `database content`_ changes | |
| 24 4. behavioural changes, through detectors_ | |
| 25 5. `access controls`_ | |
| 26 6. change the `web interface`_ | |
| 27 | |
| 28 The third case is special because it takes two distinctly different forms | |
| 29 depending upon whether the tracker has been initialised or not. The other two | |
| 30 may be done at any time, before or after tracker initialisation. Yes, this | |
| 31 includes adding or removing properties from classes. | |
| 32 | |
| 33 | |
| 34 Trackers in a Nutshell | |
| 35 ====================== | |
| 36 | |
| 37 Trackers have the following structure: | |
| 38 | |
| 39 =================== ======================================================== | |
| 40 Tracker File Description | |
| 41 =================== ======================================================== | |
| 42 config.py Holds the basic `tracker configuration`_ | |
| 43 dbinit.py Holds the `tracker schema`_ | |
| 44 interfaces.py Defines the Web and E-Mail interfaces for the tracker | |
| 45 select_db.py Selects the database back-end for the tracker | |
| 46 db/ Holds the tracker's database | |
| 47 db/files/ Holds the tracker's upload files and messages | |
| 48 detectors/ Auditors and reactors for this tracker | |
| 49 html/ Web interface templates, images and style sheets | |
| 50 =================== ======================================================== | |
| 51 | |
| 52 Tracker Configuration | |
| 53 ===================== | |
| 54 | |
| 55 The config.py located in your tracker home contains the basic configuration | |
| 56 for the web and e-mail components of roundup's interfaces. As the name | |
| 57 suggests, this file is a Python module. This means that any valid python | |
| 58 expression may be used in the file. Mostly though, you'll be setting the | |
| 59 configuration variables to string values. Python string values must be quoted | |
| 60 with either single or double quotes:: | |
| 61 | |
| 62 'this is a string' | |
| 63 "this is also a string - use it when you have a 'single quote' in the value" | |
| 64 this is not a string - it's not quoted | |
| 65 | |
| 66 Python strings may use formatting that's almost identical to C string | |
| 67 formatting. The ``%`` operator is used to perform the formatting, like so:: | |
| 68 | |
| 69 'roundup-admin@%s'%MAIL_DOMAIN | |
| 70 | |
| 71 this will create a string ``'roundup-admin@tracker.domain.example'`` if | |
| 72 MAIL_DOMAIN is set to ``'tracker.domain.example'``. | |
| 73 | |
| 74 You'll also note some values are set to:: | |
| 75 | |
| 76 os.path.join(TRACKER_HOME, 'db') | |
| 77 | |
| 78 or similar. This creates a new string which holds the path to the "db" | |
| 79 directory in the TRACKER_HOME directory. This is just a convenience so if the | |
| 80 TRACKER_HOME changes you don't have to edit multiple valoues. | |
| 81 | |
| 82 The configuration variables available are: | |
| 83 | |
| 84 **TRACKER_HOME** - ``os.path.split(__file__)[0]`` | |
| 85 The tracker home directory. The above default code will automatically | |
| 86 determine the tracker home for you, so you can just leave it alone. | |
| 87 | |
| 88 **MAILHOST** - ``'localhost'`` | |
| 89 The SMTP mail host that roundup will use to send e-mail. | |
| 90 | |
| 91 **MAIL_DOMAIN** - ``'tracker.domain.example'`` | |
| 92 The domain name used for email addresses. | |
| 93 | |
| 94 **DATABASE** - ``os.path.join(TRACKER_HOME, 'db')`` | |
| 95 This is the directory that the database is going to be stored in. By default | |
| 96 it is in the tracker home. | |
| 97 | |
| 98 **TEMPLATES** - ``os.path.join(TRACKER_HOME, 'html')`` | |
| 99 This is the directory that the HTML templates reside in. By default they are | |
| 100 in the tracker home. | |
| 101 | |
| 102 **TRACKER_NAME** - ``'Roundup issue tracker'`` | |
| 103 A descriptive name for your roundup tracker. This is sent out in e-mails and | |
| 104 appears in the heading of CGI pages. | |
| 105 | |
| 106 **TRACKER_EMAIL** - ``'issue_tracker@%s'%MAIL_DOMAIN`` | |
| 107 The email address that e-mail sent to roundup should go to. Think of it as the | |
| 108 tracker's personal e-mail address. | |
| 109 | |
| 110 **TRACKER_WEB** - ``'http://your.tracker.url.example/'`` | |
| 111 The web address that the tracker is viewable at. This will be included in | |
| 112 information sent to users of the tracker. The URL must include the cgi-bin | |
| 113 part or anything else that is required to get to the home page of the | |
| 114 tracker. You must include a trailing '/' in the URL. | |
| 115 | |
| 116 **ADMIN_EMAIL** - ``'roundup-admin@%s'%MAIL_DOMAIN`` | |
| 117 The email address that roundup will complain to if it runs into trouble. | |
| 118 | |
| 119 **MESSAGES_TO_AUTHOR** - ``'yes'`` or``'no'`` | |
| 120 Send nosy messages to the author of the message. | |
| 121 | |
| 122 **ADD_AUTHOR_TO_NOSY** - ``'new'``, ``'yes'`` or ``'no'`` | |
| 123 Does the author of a message get placed on the nosy list automatically? | |
| 124 If ``'new'`` is used, then the author will only be added when a message | |
| 125 creates a new issue. If ``'yes'``, then the author will be added on followups | |
| 126 too. If ``'no'``, they're never added to the nosy. | |
| 127 | |
| 128 **ADD_RECIPIENTS_TO_NOSY** - ``'new'``, ``'yes'`` or ``'no'`` | |
| 129 Do the recipients (To:, Cc:) of a message get placed on the nosy list? | |
| 130 If ``'new'`` is used, then the recipients will only be added when a message | |
| 131 creates a new issue. If ``'yes'``, then the recipients will be added on | |
| 132 followups too. If ``'no'``, they're never added to the nosy. | |
| 133 | |
| 134 **EMAIL_SIGNATURE_POSITION** - ``'top'``, ``'bottom'`` or ``'none'`` | |
| 135 Where to place the email signature in messages that Roundup generates. | |
| 136 | |
| 137 **EMAIL_KEEP_QUOTED_TEXT** - ``'yes'`` or ``'no'`` | |
| 138 Keep email citations. Citations are the part of e-mail which the sender has | |
| 139 quoted in their reply to previous e-mail. | |
| 140 | |
| 141 **EMAIL_LEAVE_BODY_UNCHANGED** - ``'no'`` | |
| 142 Preserve the email body as is. Enabiling this will cause the entire message | |
| 143 body to be stored, including all citations and signatures. It should be | |
| 144 either ``'yes'`` or ``'no'``. | |
| 145 | |
| 146 **MAIL_DEFAULT_CLASS** - ``'issue'`` or ``''`` | |
| 147 Default class to use in the mailgw if one isn't supplied in email | |
| 148 subjects. To disable, comment out the variable below or leave it blank. | |
| 149 | |
| 150 The default config.py is given below - as you | |
| 151 can see, the MAIL_DOMAIN must be edited before any interaction with the | |
| 152 tracker is attempted.:: | |
| 153 | |
| 154 # roundup home is this package's directory | |
| 155 TRACKER_HOME=os.path.split(__file__)[0] | |
| 156 | |
| 157 # The SMTP mail host that roundup will use to send mail | |
| 158 MAILHOST = 'localhost' | |
| 159 | |
| 160 # The domain name used for email addresses. | |
| 161 MAIL_DOMAIN = 'your.tracker.email.domain.example' | |
| 162 | |
| 163 # This is the directory that the database is going to be stored in | |
| 164 DATABASE = os.path.join(TRACKER_HOME, 'db') | |
| 165 | |
| 166 # This is the directory that the HTML templates reside in | |
| 167 TEMPLATES = os.path.join(TRACKER_HOME, 'html') | |
| 168 | |
| 169 # A descriptive name for your roundup tracker | |
| 170 TRACKER_NAME = 'Roundup issue tracker' | |
| 171 | |
| 172 # The email address that mail to roundup should go to | |
| 173 TRACKER_EMAIL = 'issue_tracker@%s'%MAIL_DOMAIN | |
| 174 | |
| 175 # The web address that the tracker is viewable at | |
| 176 TRACKER_WEB = 'http://your.tracker.url.example/' | |
| 177 | |
| 178 # The email address that roundup will complain to if it runs into trouble | |
| 179 ADMIN_EMAIL = 'roundup-admin@%s'%MAIL_DOMAIN | |
| 180 | |
| 181 # Send nosy messages to the author of the message | |
| 182 MESSAGES_TO_AUTHOR = 'no' # either 'yes' or 'no' | |
| 183 | |
| 184 # Does the author of a message get placed on the nosy list automatically? | |
| 185 # If 'new' is used, then the author will only be added when a message | |
| 186 # creates a new issue. If 'yes', then the author will be added on followups | |
| 187 # too. If 'no', they're never added to the nosy. | |
| 188 ADD_AUTHOR_TO_NOSY = 'new' # one of 'yes', 'no', 'new' | |
| 189 | |
| 190 # Do the recipients (To:, Cc:) of a message get placed on the nosy list? | |
| 191 # If 'new' is used, then the recipients will only be added when a message | |
| 192 # creates a new issue. If 'yes', then the recipients will be added on followups | |
| 193 # too. If 'no', they're never added to the nosy. | |
| 194 ADD_RECIPIENTS_TO_NOSY = 'new' # either 'yes', 'no', 'new' | |
| 195 | |
| 196 # Where to place the email signature | |
| 197 EMAIL_SIGNATURE_POSITION = 'bottom' # one of 'top', 'bottom', 'none' | |
| 198 | |
| 199 # Keep email citations | |
| 200 EMAIL_KEEP_QUOTED_TEXT = 'no' # either 'yes' or 'no' | |
| 201 | |
| 202 # Preserve the email body as is | |
| 203 EMAIL_LEAVE_BODY_UNCHANGED = 'no' # either 'yes' or 'no' | |
| 204 | |
| 205 # Default class to use in the mailgw if one isn't supplied in email | |
| 206 # subjects. To disable, comment out the variable below or leave it blank. | |
| 207 # Examples: | |
| 208 MAIL_DEFAULT_CLASS = 'issue' # use "issue" class by default | |
| 209 #MAIL_DEFAULT_CLASS = '' # disable (or just comment the var out) | |
| 210 | |
| 211 Tracker Schema | |
| 212 ============== | |
| 213 | |
| 214 Note: if you modify the schema, you'll most likely need to edit the | |
| 215 `web interface`_ HTML template files and `detectors`_ to reflect | |
| 216 your changes. | |
| 217 | |
| 218 A tracker schema defines what data is stored in the tracker's database. | |
| 219 Schemas are defined using Python code in the ``dbinit.py`` module of your | |
| 220 tracker. The "classic" schema looks like this:: | |
| 221 | |
| 222 pri = Class(db, "priority", name=String(), order=String()) | |
| 223 pri.setkey("name") | |
| 224 | |
| 225 stat = Class(db, "status", name=String(), order=String()) | |
| 226 stat.setkey("name") | |
| 227 | |
| 228 keyword = Class(db, "keyword", name=String()) | |
| 229 keyword.setkey("name") | |
| 230 | |
| 231 user = Class(db, "user", username=String(), organisation=String(), | |
| 232 password=String(), address=String(), realname=String(), phone=String()) | |
| 233 user.setkey("username") | |
| 234 | |
| 235 msg = FileClass(db, "msg", author=Link("user"), summary=String(), | |
| 236 date=Date(), recipients=Multilink("user"), files=Multilink("file")) | |
| 237 | |
| 238 file = FileClass(db, "file", name=String(), type=String()) | |
| 239 | |
| 240 issue = IssueClass(db, "issue", topic=Multilink("keyword"), | |
| 241 status=Link("status"), assignedto=Link("user"), | |
| 242 priority=Link("priority")) | |
| 243 issue.setkey('title') | |
| 244 | |
| 245 Classes and Properties - creating a new information store | |
| 246 --------------------------------------------------------- | |
| 247 | |
| 248 In the tracker above, we've defined 7 classes of information: | |
| 249 | |
| 250 priority | |
| 251 Defines the possible levels of urgency for issues. | |
| 252 | |
| 253 status | |
| 254 Defines the possible states of processing the issue may be in. | |
| 255 | |
| 256 keyword | |
| 257 Initially empty, will hold keywords useful for searching issues. | |
| 258 | |
| 259 user | |
| 260 Initially holding the "admin" user, will eventually have an entry for all | |
| 261 users using roundup. | |
| 262 | |
| 263 msg | |
| 264 Initially empty, will all e-mail messages sent to or generated by | |
| 265 roundup. | |
| 266 | |
| 267 file | |
| 268 Initially empty, will all files attached to issues. | |
| 269 | |
| 270 issue | |
| 271 Initially empty, this is where the issue information is stored. | |
| 272 | |
| 273 We define the "priority" and "status" classes to allow two things: reduction in | |
| 274 the amount of information stored on the issue and more powerful, accurate | |
| 275 searching of issues by priority and status. By only requiring a link on the | |
| 276 issue (which is stored as a single number) we reduce the chance that someone | |
| 277 mis-types a priority or status - or simply makes a new one up. | |
| 278 | |
| 279 Class and Items | |
| 280 ~~~~~~~~~~~~~~~ | |
| 281 | |
| 282 A Class defines a particular class (or type) of data that will be stored in the | |
| 283 database. A class comprises one or more properties, which given the information | |
| 284 about the class items. | |
| 285 The actual data entered into the database, using class.create() are called | |
| 286 items. They have a special immutable property called id. We sometimes refer to | |
| 287 this as the itemid. | |
| 288 | |
| 289 Properties | |
| 290 ~~~~~~~~~~ | |
| 291 | |
| 292 A Class is comprised of one or more properties of the following types: | |
| 293 | |
| 294 * String properties are for storing arbitrary-length strings. | |
| 295 * Password properties are for storing encoded arbitrary-length strings. The | |
| 296 default encoding is defined on the roundup.password.Password class. | |
| 297 * Date properties store date-and-time stamps. Their values are Timestamp | |
| 298 objects. | |
| 299 * Number properties store numeric values. | |
| 300 * Boolean properties store on/off, yes/no, true/false values. | |
| 301 * A Link property refers to a single other item selected from a specified | |
| 302 class. The class is part of the property; the value is an integer, the id | |
| 303 of the chosen item. | |
| 304 * A Multilink property refers to possibly many items in a specified class. | |
| 305 The value is a list of integers. | |
| 306 | |
| 307 FileClass | |
| 308 ~~~~~~~~~ | |
| 309 | |
| 310 FileClasses save their "content" attribute off in a separate file from the rest | |
| 311 of the database. This reduces the number of large entries in the database, | |
| 312 which generally makes databases more efficient, and also allows us to use | |
| 313 command-line tools to operate on the files. They are stored in the files sub- | |
| 314 directory of the db directory in your tracker. | |
| 315 | |
| 316 IssueClass | |
| 317 ~~~~~~~~~~ | |
| 318 | |
| 319 IssueClasses automatically include the "messages", "files", "nosy", and | |
| 320 "superseder" properties. | |
| 321 The messages and files properties list the links to the messages and files | |
| 322 related to the issue. The nosy property is a list of links to users who wish to | |
| 323 be informed of changes to the issue - they get "CC'ed" e-mails when messages | |
| 324 are sent to or generated by the issue. The nosy reactor (in the detectors | |
| 325 directory) handles this action. The superceder link indicates an issue which | |
| 326 has superceded this one. | |
| 327 They also have the dynamically generated "creation", "activity" and "creator" | |
| 328 properties. | |
| 329 The value of the "creation" property is the date when an item was created, and | |
| 330 the value of the "activity" property is the date when any property on the item | |
| 331 was last edited (equivalently, these are the dates on the first and last | |
| 332 records in the item's journal). The "creator" property holds a link to the user | |
| 333 that created the issue. | |
| 334 | |
| 335 setkey(property) | |
| 336 ~~~~~~~~~~~~~~~~ | |
| 337 | |
| 338 Select a String property of the class to be the key property. The key property | |
| 339 muse be unique, and allows references to the items in the class by the content | |
| 340 of the key property. That is, we can refer to users by their username, e.g. | |
| 341 let's say that there's an issue in roundup, issue 23. There's also a user, | |
| 342 richard who happens to be user 2. To assign an issue to him, we could do either | |
| 343 of:: | |
| 344 | |
| 345 roundup-admin set issue assignedto=2 | |
| 346 | |
| 347 or:: | |
| 348 | |
| 349 roundup-admin set issue assignedto=richard | |
| 350 | |
| 351 Note, the same thing can be done in the web and e-mail interfaces. | |
| 352 | |
| 353 create(information) | |
| 354 ~~~~~~~~~~~~~~~~~~~ | |
| 355 | |
| 356 Create an item in the database. This is generally used to create items in the | |
| 357 "definitional" classes like "priority" and "status". | |
| 358 | |
| 359 | |
| 360 Examples of adding to your schema | |
| 361 --------------------------------- | |
| 362 | |
| 363 TODO | |
| 364 | |
| 365 | |
| 366 Detectors - adding behaviour to your tracker | |
| 367 ============================================ | |
| 368 .. _detectors: | |
| 369 | |
| 370 Detectors are initialised every time you open your tracker database, so you're | |
| 371 free to add and remove them any time, even after the database is initliased | |
| 372 via the "roundup-admin initalise" command. | |
| 373 | |
| 374 The detectors in your tracker fire before (*auditors*) and after (*reactors*) | |
| 375 changes to the contents of your database. They are Python modules that sit in | |
| 376 your tracker's ``detectors`` directory. You will have some installed by | |
| 377 default - have a look. You can write new detectors or modify the existing | |
| 378 ones. The existing detectors installed for you are: | |
| 379 | |
| 380 **nosyreaction.py** | |
| 381 This provides the automatic nosy list maintenance and email sending. The nosy | |
| 382 reactor (``nosyreaction``) fires when new messages are added to issues. | |
| 383 The nosy auditor (``updatenosy``) fires when issues are changed and figures | |
| 384 what changes need to be made to the nosy list (like adding new authors etc) | |
| 385 **statusauditor.py** | |
| 386 This provides the ``chatty`` auditor which changes the issue status from | |
| 387 ``unread`` or ``closed`` to ``chatting`` if new messages appear. It also | |
| 388 provides the ``presetunread`` auditor which pre-sets the status to | |
| 389 ``unread`` on new items if the status isn't explicitly defined. | |
| 390 | |
| 391 See the detectors section in the `design document`__ for details of the | |
| 392 interface for detectors. | |
| 393 | |
| 394 __ design.html | |
| 395 | |
| 396 Sample additional detectors that have been found useful will appear in the | |
| 397 ``detectors`` directory of the Roundup distribution: | |
| 398 | |
| 399 **newissuecopy.py** | |
| 400 This detector sends an email to a team address whenever a new issue is | |
| 401 created. The address is hard-coded into the detector, so edit it before you | |
| 402 use it (look for the text 'team@team.host') or you'll get email errors! | |
| 403 | |
| 404 The detector code:: | |
| 405 | |
| 406 from roundup import roundupdb | |
| 407 | |
| 408 def newissuecopy(db, cl, nodeid, oldvalues): | |
| 409 ''' Copy a message about new issues to a team address. | |
| 410 ''' | |
| 411 # so use all the messages in the create | |
| 412 change_note = cl.generateCreateNote(nodeid) | |
| 413 | |
| 414 # send a copy to the nosy list | |
| 415 for msgid in cl.get(nodeid, 'messages'): | |
| 416 try: | |
| 417 # note: last arg must be a list | |
| 418 cl.send_message(nodeid, msgid, change_note, ['team@team.host']) | |
| 419 except roundupdb.MessageSendError, message: | |
| 420 raise roundupdb.DetectorError, message | |
| 421 | |
| 422 def init(db): | |
| 423 db.issue.react('create', newissuecopy) | |
| 424 | |
| 425 | |
| 426 Database Content | |
| 427 ================ | |
| 428 | |
| 429 Note: if you modify the content of definitional classes, you'll most likely | |
| 430 need to edit the tracker `detectors`_ to reflect your changes. | |
| 431 | |
| 432 Customisation of the special "definitional" classes (eg. status, priority, | |
| 433 resolution, ...) may be done either before or after the tracker is | |
| 434 initialised. The actual method of doing so is completely different in each | |
| 435 case though, so be careful to use the right one. | |
| 436 | |
| 437 **Changing content before tracker initialisation** | |
| 438 Edit the dbinit module in your tracker to alter the items created in using | |
| 439 the create() methods. | |
| 440 | |
| 441 **Changing content after tracker initialisation** | |
| 442 As the "admin" user, click on the "class list" link in the web interface | |
| 443 to bring up a list of all database classes. Click on the name of the class | |
| 444 you wish to change the content of. | |
| 445 | |
| 446 You may also use the roundup-admin interface's create, set and retire | |
| 447 methods to add, alter or remove items from the classes in question. | |
| 448 | |
| 449 See "`adding a new field to the classic schema`_" for an example that requires | |
| 450 database content changes. | |
| 451 | |
| 452 | |
| 453 Access Controls | |
| 454 =============== | |
| 455 | |
| 456 A set of Permissions are built in to the security module by default: | |
| 457 | |
| 458 - Edit (everything) | |
| 459 - View (everything) | |
| 460 | |
| 461 The default interfaces define: | |
| 462 | |
| 463 - Web Registration | |
| 464 - Web Access | |
| 465 - Web Roles | |
| 466 - Email Registration | |
| 467 - Email Access | |
| 468 | |
| 469 These are hooked into the default Roles: | |
| 470 | |
| 471 - Admin (Edit everything, View everything, Web Roles) | |
| 472 - User (Web Access, Email Access) | |
| 473 - Anonymous (Web Registration, Email Registration) | |
| 474 | |
| 475 And finally, the "admin" user gets the "Admin" Role, and the "anonymous" user | |
| 476 gets the "Anonymous" assigned when the database is initialised on installation. | |
| 477 The two default schemas then define: | |
| 478 | |
| 479 - Edit issue, View issue (both) | |
| 480 - Edit file, View file (both) | |
| 481 - Edit msg, View msg (both) | |
| 482 - Edit support, View support (extended only) | |
| 483 | |
| 484 and assign those Permissions to the "User" Role. Put together, these settings | |
| 485 appear in the ``open()`` function of the tracker ``dbinit.py`` (the following | |
| 486 is taken from the "minimal" template ``dbinit.py``):: | |
| 487 | |
| 488 # | |
| 489 # SECURITY SETTINGS | |
| 490 # | |
| 491 # new permissions for this schema | |
| 492 for cl in ('user', ): | |
| 493 db.security.addPermission(name="Edit", klass=cl, | |
| 494 description="User is allowed to edit "+cl) | |
| 495 db.security.addPermission(name="View", klass=cl, | |
| 496 description="User is allowed to access "+cl) | |
| 497 | |
| 498 # and give the regular users access to the web and email interface | |
| 499 p = db.security.getPermission('Web Access') | |
| 500 db.security.addPermissionToRole('User', p) | |
| 501 p = db.security.getPermission('Email Access') | |
| 502 db.security.addPermissionToRole('User', p) | |
| 503 | |
| 504 # May users view other user information? Comment these lines out | |
| 505 # if you don't want them to | |
| 506 p = db.security.getPermission('View', 'user') | |
| 507 db.security.addPermissionToRole('User', p) | |
| 508 | |
| 509 # Assign the appropriate permissions to the anonymous user's Anonymous | |
| 510 # Role. Choices here are: | |
| 511 # - Allow anonymous users to register through the web | |
| 512 p = db.security.getPermission('Web Registration') | |
| 513 db.security.addPermissionToRole('Anonymous', p) | |
| 514 # - Allow anonymous (new) users to register through the email gateway | |
| 515 p = db.security.getPermission('Email Registration') | |
| 516 db.security.addPermissionToRole('Anonymous', p) | |
| 517 | |
| 518 | |
| 519 New User Roles | |
| 520 -------------- | |
| 521 | |
| 522 New users are assigned the Roles defined in the config file as: | |
| 523 | |
| 524 - NEW_WEB_USER_ROLES | |
| 525 - NEW_EMAIL_USER_ROLES | |
| 526 | |
| 527 | |
| 528 Changing Access Controls | |
| 529 ------------------------ | |
| 530 | |
| 531 You may alter the configuration variables to change the Role that new web or | |
| 532 email users get, for example to not give them access to the web interface if | |
| 533 they register through email. | |
| 534 | |
| 535 You may use the ``roundup-admin`` "``security``" command to display the | |
| 536 current Role and Permission configuration in your tracker. | |
| 537 | |
| 538 Adding a new Permission | |
| 539 ~~~~~~~~~~~~~~~~~~~~~~~ | |
| 540 | |
| 541 When adding a new Permission, you will need to: | |
| 542 | |
| 543 1. add it to your tracker's dbinit so it is created | |
| 544 2. enable it for the Roles that should have it (verify with | |
| 545 "``roundup-admin security``") | |
| 546 3. add it to the relevant HTML interface templates | |
| 547 4. add it to the appropriate xxxPermission methods on in your tracker | |
| 548 interfaces module | |
| 549 | |
| 550 Example Scenarios | |
| 551 ~~~~~~~~~~~~~~~~~ | |
| 552 | |
| 553 **automatic registration of users in the e-mail gateway** | |
| 554 By giving the "anonymous" user the "Email Registration" Role, any | |
| 555 unidentified user will automatically be registered with the tracker (with | |
| 556 no password, so they won't be able to log in through the web until an admin | |
| 557 sets them a password). Note: this is the default behaviour in the tracker | |
| 558 templates that ship with Roundup. | |
| 559 | |
| 560 **anonymous access through the e-mail gateway** | |
| 561 Give the "anonymous" user the "Email Access" and ("Edit", "issue") Roles | |
| 562 but not giving them the "Email Registration" Role. This means that when an | |
| 563 unknown user sends email into the tracker, they're automatically logged in | |
| 564 as "anonymous". Since they don't have the "Email Registration" Role, they | |
| 565 won't be automatically registered, but since "anonymous" has permission | |
| 566 to use the gateway, they'll still be able to submit issues. Note that the | |
| 567 Sender information - their email address - will not be available - they're | |
| 568 *anonymous*. | |
| 569 | |
| 570 **only developers may be assigned issues** | |
| 571 Create a new Permission called "Fixer" for the "issue" class. Create a new | |
| 572 Role "Developer" which has that Permission, and assign that to the | |
| 573 appropriate users. Filter the list of users available in the assignedto | |
| 574 list to include only those users. Enforce the Permission with an auditor. See | |
| 575 the example `restricting the list of users that are assignable to a task`_. | |
| 576 | |
| 577 **only managers may sign off issues as complete** | |
| 578 Create a new Permission called "Closer" for the "issue" class. Create a new | |
| 579 Role "Manager" which has that Permission, and assign that to the appropriate | |
| 580 users. In your web interface, only display the "resolved" issue state option | |
| 581 when the user has the "Closer" Permissions. Enforce the Permission with | |
| 582 an auditor. This is very similar to the previous example, except that the | |
| 583 web interface check would look like:: | |
| 584 | |
| 585 <option tal:condition="python:request.user.hasPermission('Closer')" | |
| 586 value="resolved">Resolved</option> | |
| 587 | |
| 588 **don't give users who register through email web access** | |
| 589 Create a new Role called "Email User" which has all the Permissions of the | |
| 590 normal "User" Role minus the "Web Access" Permission. This will allow users | |
| 591 to send in emails to the tracker, but not access the web interface. | |
| 592 | |
| 593 **let some users edit the details of all users** | |
| 594 Create a new Role called "User Admin" which has the Permission for editing | |
| 595 users:: | |
| 596 | |
| 597 db.security.addRole(name='User Admin', description='Managing users') | |
| 598 p = db.security.getPermission('Edit', 'user') | |
| 599 db.security.addPermissionToRole('User Admin', p) | |
| 600 | |
| 601 and assign the Role to the users who need the permission. | |
| 602 | |
| 603 | |
| 604 Web Interface | |
| 605 ============= | |
| 606 | |
| 607 .. contents:: | |
| 608 :local: | |
| 609 :depth: 1 | |
| 610 | |
| 611 The web is provided by the roundup.cgi.client module and is used by | |
| 612 roundup.cgi, roundup-server and ZRoundup. | |
| 613 In all cases, we determine which tracker is being accessed | |
| 614 (the first part of the URL path inside the scope of the CGI handler) and pass | |
| 615 control on to the tracker interfaces.Client class - which uses the Client class | |
| 616 from roundup.cgi.client - which handles the rest of | |
| 617 the access through its main() method. This means that you can do pretty much | |
| 618 anything you want as a web interface to your tracker. | |
| 619 | |
| 620 Repurcussions of changing the tracker schema | |
| 621 --------------------------------------------- | |
| 622 | |
| 623 If you choose to change the `tracker schema`_ you will need to ensure the web | |
| 624 interface knows about it: | |
| 625 | |
| 626 1. Index, item and search pages for the relevant classes may need to have | |
| 627 properties added or removed, | |
| 628 2. The "page" template may require links to be changed, as might the "home" | |
| 629 page's content arguments. | |
| 630 | |
| 631 How requests are processed | |
| 632 -------------------------- | |
| 633 | |
| 634 The basic processing of a web request proceeds as follows: | |
| 635 | |
| 636 1. figure out who we are, defaulting to the "anonymous" user | |
| 637 2. figure out what the request is for - we call this the "context" | |
| 638 3. handle any requested action (item edit, search, ...) | |
| 639 4. render the template requested by the context, resulting in HTML output | |
| 640 | |
| 641 In some situations, exceptions occur: | |
| 642 | |
| 643 - HTTP Redirect (generally raised by an action) | |
| 644 - SendFile (generally raised by determine_context) | |
| 645 here we serve up a FileClass "content" property | |
| 646 - SendStaticFile (generally raised by determine_context) | |
| 647 here we serve up a file from the tracker "html" directory | |
| 648 - Unauthorised (generally raised by an action) | |
| 649 here the action is cancelled, the request is rendered and an error | |
| 650 message is displayed indicating that permission was not | |
| 651 granted for the action to take place | |
| 652 - NotFound (raised wherever it needs to be) | |
| 653 this exception percolates up to the CGI interface that called the client | |
| 654 | |
| 655 Determining web context | |
| 656 ----------------------- | |
| 657 | |
| 658 To determine the "context" of a request, we look at the URL and the special | |
| 659 request variable ``:template``. The URL path after the tracker identifier | |
| 660 is examined. Typical URL paths look like: | |
| 661 | |
| 662 1. ``/tracker/issue`` | |
| 663 2. ``/tracker/issue1`` | |
| 664 3. ``/tracker/_file/style.css`` | |
| 665 4. ``/cgi-bin/roundup.cgi/tracker/file1`` | |
| 666 5. ``/cgi-bin/roundup.cgi/tracker/file1/kitten.png`` | |
| 667 | |
| 668 where the "tracker identifier" is "tracker" in the above cases. That means | |
| 669 we're looking at "issue", "issue1", "_file/style.css", "file1" and | |
| 670 "file1/kitten.png" in the cases above. The path is generally only one | |
| 671 entry long - longer paths are handled differently. | |
| 672 | |
| 673 a. if there is no path, then we are in the "home" context. | |
| 674 b. if the path starts with "_file" (as in example 3, | |
| 675 "/tracker/_file/style.css"), then the additional path entry, | |
| 676 "style.css" specifies the filename of a static file we're to serve up | |
| 677 from the tracker "html" directory. Raises a SendStaticFile | |
| 678 exception. | |
| 679 c. if there is something in the path (as in example 1, "issue"), it identifies | |
| 680 the tracker class we're to display. | |
| 681 d. if the path is an item designator (as in examples 2 and 4, "issue1" and | |
| 682 "file1"), then we're to display a specific item. | |
| 683 e. if the path starts with an item designator and is longer than | |
| 684 one entry (as in example 5, "file1/kitten.png"), then we're assumed | |
| 685 to be handling an item of a | |
| 686 FileClass, and the extra path information gives the filename | |
| 687 that the client is going to label the download with (ie | |
| 688 "file1/kitten.png" is nicer to download than "file1"). This | |
| 689 raises a SendFile exception. | |
| 690 | |
| 691 Both b. and e. stop before we bother to | |
| 692 determine the template we're going to use. That's because they | |
| 693 don't actually use templates. | |
| 694 | |
| 695 The template used is specified by the ``:template`` CGI variable, | |
| 696 which defaults to: | |
| 697 | |
| 698 - only classname suplied: "index" | |
| 699 - full item designator supplied: "item" | |
| 700 | |
| 701 | |
| 702 Performing actions in web requests | |
| 703 ---------------------------------- | |
| 704 | |
| 705 When a user requests a web page, they may optionally also request for an | |
| 706 action to take place. As described in `how requests are processed`_, the | |
| 707 action is performed before the requested page is generated. Actions are | |
| 708 triggered by using a ``:action`` CGI variable, where the value is one of: | |
| 709 | |
| 710 **login** | |
| 711 Attempt to log a user in. | |
| 712 | |
| 713 **logout** | |
| 714 Log the user out - make them "anonymous". | |
| 715 | |
| 716 **register** | |
| 717 Attempt to create a new user based on the contents of the form and then log | |
| 718 them in. | |
| 719 | |
| 720 **edit** | |
| 721 Perform an edit of an item in the database. There are some special form | |
| 722 elements you may use: | |
| 723 | |
| 724 :link=designator:property and :multilink=designator:property | |
| 725 The value specifies an item designator and the property on that | |
| 726 item to add *this* item to as a link or multilink. | |
| 727 :note | |
| 728 Create a message and attach it to the current item's | |
| 729 "messages" property. | |
| 730 :file | |
| 731 Create a file and attach it to the current item's | |
| 732 "files" property. Attach the file to the message created from | |
| 733 the :note if it's supplied. | |
| 734 :required=property,property,... | |
| 735 The named properties are required to be filled in the form. | |
| 736 :remove:<propname>=id(s) | |
| 737 The ids will be removed from the multilink property. You may have multiple | |
| 738 :remove:<propname> form elements for a single <propname>. | |
| 739 :add:<propname>=id(s) | |
| 740 The ids will be added to the multilink property. You may have multiple | |
| 741 :add:<propname> form elements for a single <propname>. | |
| 742 | |
| 743 **new** | |
| 744 Add a new item to the database. You may use the same special form elements | |
| 745 as in the "edit" action. | |
| 746 | |
| 747 **retire** | |
| 748 Retire the item in the database. | |
| 749 | |
| 750 **editCSV** | |
| 751 Performs an edit of all of a class' items in one go. See also the | |
| 752 *class*.csv templating method which generates the CSV data to be edited, and | |
| 753 the "_generic.index" template which uses both of these features. | |
| 754 | |
| 755 **search** | |
| 756 Mangle some of the form variables. | |
| 757 | |
| 758 Set the form ":filter" variable based on the values of the | |
| 759 filter variables - if they're set to anything other than | |
| 760 "dontcare" then add them to :filter. | |
| 761 | |
| 762 Also handle the ":queryname" variable and save off the query to | |
| 763 the user's query list. | |
| 764 | |
| 765 Each of the actions is implemented by a corresponding *actionAction* (where | |
| 766 "action" is the name of the action) method on | |
| 767 the roundup.cgi.Client class, which also happens to be in your tracker as | |
| 768 interfaces.Client. So if you need to define new actions, you may add them | |
| 769 there (see `defining new web actions`_). | |
| 770 | |
| 771 Each action also has a corresponding *actionPermission* (where | |
| 772 "action" is the name of the action) method which determines | |
| 773 whether the action is permissible given the current user. The base permission | |
| 774 checks are: | |
| 775 | |
| 776 **login** | |
| 777 Determine whether the user has permission to log in. | |
| 778 Base behaviour is to check the user has "Web Access". | |
| 779 **logout** | |
| 780 No permission checks are made. | |
| 781 **register** | |
| 782 Determine whether the user has permission to register | |
| 783 Base behaviour is to check the user has "Web Registration". | |
| 784 **edit** | |
| 785 Determine whether the user has permission to edit this item. | |
| 786 Base behaviour is to check the user can edit this class. If we're | |
| 787 editing the "user" class, users are allowed to edit their own | |
| 788 details. Unless it's the "roles" property, which requires the | |
| 789 special Permission "Web Roles". | |
| 790 **new** | |
| 791 Determine whether the user has permission to create (edit) this item. | |
| 792 Base behaviour is to check the user can edit this class. No | |
| 793 additional property checks are made. Additionally, new user items | |
| 794 may be created if the user has the "Web Registration" Permission. | |
| 795 **editCSV** | |
| 796 Determine whether the user has permission to edit this class. | |
| 797 Base behaviour is to check the user can edit this class. | |
| 798 **search** | |
| 799 Determine whether the user has permission to search this class. | |
| 800 Base behaviour is to check the user can view this class. | |
| 801 | |
| 802 | |
| 803 Default templates | |
| 804 ----------------- | |
| 805 | |
| 806 Most customisation of the web view can be done by modifying the templates in | |
| 807 the tracker **html** directory. There are several types of files in there: | |
| 808 | |
| 809 **page** | |
| 810 This template usually defines the overall look of your tracker. When you | |
| 811 view an issue, it appears inside this template. When you view an index, it | |
| 812 also appears inside this template. This template defines a macro called | |
| 813 "icing" which is used by almost all other templates as a coating for their | |
| 814 content, using its "content" slot. It will also define the "head_title" | |
| 815 and "body_title" slots to allow setting of the page title. | |
| 816 **home** | |
| 817 the default page displayed when no other page is indicated by the user | |
| 818 **home.classlist** | |
| 819 a special version of the default page that lists the classes in the tracker | |
| 820 **classname.item** | |
| 821 displays an item of the *classname* class | |
| 822 **classname.index** | |
| 823 displays a list of *classname* items | |
| 824 **classname.search** | |
| 825 displays a search page for *classname* items | |
| 826 **_generic.index** | |
| 827 used to display a list of items where there is no *classname*.index available | |
| 828 **_generic.help** | |
| 829 used to display a "class help" page where there is no *classname*.help | |
| 830 **user.register** | |
| 831 a special page just for the user class that renders the registration page | |
| 832 **style.css** | |
| 833 a static file that is served up as-is | |
| 834 | |
| 835 Note: Remember that you can create any template extension you want to, so | |
| 836 if you just want to play around with the templating for new issues, you can | |
| 837 copy the current "issue.item" template to "issue.test", and then access the | |
| 838 test template using the ":template" URL argument:: | |
| 839 | |
| 840 http://your.tracker.example/tracker/issue?:template=test | |
| 841 | |
| 842 and it won't affect your users using the "issue.item" template. | |
| 843 | |
| 844 | |
| 845 How the templates work | |
| 846 ---------------------- | |
| 847 | |
| 848 Basic Templating Actions | |
| 849 ~~~~~~~~~~~~~~~~~~~~~~~~ | |
| 850 | |
| 851 Roundup's templates consist of special attributes on your template tags. | |
| 852 These attributes form the Template Attribute Language, or TAL. The basic tag | |
| 853 commands are: | |
| 854 | |
| 855 **tal:define="variable expression; variable expression; ..."** | |
| 856 Define a new variable that is local to this tag and its contents. For | |
| 857 example:: | |
| 858 | |
| 859 <html tal:define="title request/description"> | |
| 860 <head><title tal:content="title"></title></head> | |
| 861 </html> | |
| 862 | |
| 863 In the example, the variable "title" is defined as being the result of the | |
| 864 expression "request/description". The tal:content command inside the <html> | |
| 865 tag may then use the "title" variable. | |
| 866 | |
| 867 **tal:condition="expression"** | |
| 868 Only keep this tag and its contents if the expression is true. For example:: | |
| 869 | |
| 870 <p tal:condition="python:request.user.hasPermission('View', 'issue')"> | |
| 871 Display some issue information. | |
| 872 </p> | |
| 873 | |
| 874 In the example, the <p> tag and its contents are only displayed if the | |
| 875 user has the View permission for issues. We consider the number zero, a | |
| 876 blank string, an empty list, and the built-in variable nothing to be false | |
| 877 values. Nearly every other value is true, including non-zero numbers, and | |
| 878 strings with anything in them (even spaces!). | |
| 879 | |
| 880 **tal:repeat="variable expression"** | |
| 881 Repeat this tag and its contents for each element of the sequence that the | |
| 882 expression returns, defining a new local variable and a special "repeat" | |
| 883 variable for each element. For example:: | |
| 884 | |
| 885 <tr tal:repeat="u user/list"> | |
| 886 <td tal:content="u/id"></td> | |
| 887 <td tal:content="u/username"></td> | |
| 888 <td tal:content="u/realname"></td> | |
| 889 </tr> | |
| 890 | |
| 891 The example would iterate over the sequence of users returned by | |
| 892 "user/list" and define the local variable "u" for each entry. | |
| 893 | |
| 894 **tal:replace="expression"** | |
| 895 Replace this tag with the result of the expression. For example:: | |
| 896 | |
| 897 <span tal:replace="request/user/realname"></span> | |
| 898 | |
| 899 The example would replace the <span> tag and its contents with the user's | |
| 900 realname. If the user's realname was "Bruce" then the resultant output | |
| 901 would be "Bruce". | |
| 902 | |
| 903 **tal:content="expression"** | |
| 904 Replace the contents of this tag with the result of the expression. For | |
| 905 example:: | |
| 906 | |
| 907 <span tal:content="request/user/realname">user's name appears here</span> | |
| 908 | |
| 909 The example would replace the contents of the <span> tag with the user's | |
| 910 realname. If the user's realname was "Bruce" then the resultant output | |
| 911 would be "<span>Bruce</span>". | |
| 912 | |
| 913 **tal:attributes="attribute expression; attribute expression; ..."** | |
| 914 Set attributes on this tag to the results of expressions. For example:: | |
| 915 | |
| 916 <a tal:attributes="href string:user${request/user/id}">My Details</a> | |
| 917 | |
| 918 In the example, the "href" attribute of the <a> tag is set to the value of | |
| 919 the "string:user${request/user/id}" expression, which will be something | |
| 920 like "user123". | |
| 921 | |
| 922 **tal:omit-tag="expression"** | |
| 923 Remove this tag (but not its contents) if the expression is true. For | |
| 924 example:: | |
| 925 | |
| 926 <span tal:omit-tag="python:1">Hello, world!</span> | |
| 927 | |
| 928 would result in output of:: | |
| 929 | |
| 930 Hello, world! | |
| 931 | |
| 932 Note that the commands on a given tag are evaulated in the order above, so | |
| 933 *define* comes before *condition*, and so on. | |
| 934 | |
| 935 Additionally, a tag is defined, tal:block, which is removed from output. Its | |
| 936 content is not, but the tag itself is (so don't go using any tal:attributes | |
| 937 commands on it). This is useful for making arbitrary blocks of HTML | |
| 938 conditional or repeatable (very handy for repeating multiple table rows, | |
| 939 which would othewise require an illegal tag placement to effect the repeat). | |
| 940 | |
| 941 | |
| 942 Templating Expressions | |
| 943 ~~~~~~~~~~~~~~~~~~~~~~ | |
| 944 | |
| 945 The expressions you may use in the attibute values may be one of the following | |
| 946 forms: | |
| 947 | |
| 948 **Path Expressions** - eg. ``item/status/checklist`` | |
| 949 These are object attribute / item accesses. Roughly speaking, the path | |
| 950 ``item/status/checklist`` is broken into parts ``item``, ``status`` | |
| 951 and ``checklist``. The ``item`` part is the root of the expression. | |
| 952 We then look for a ``status`` attribute on ``item``, or failing that, a | |
| 953 ``status`` item (as in ``item['status']``). If that | |
| 954 fails, the path expression fails. When we get to the end, the object we're | |
| 955 left with is evaluated to get a string - methods are called, objects are | |
| 956 stringified. Path expressions may have an optional ``path:`` prefix, though | |
| 957 they are the default expression type, so it's not necessary. | |
| 958 | |
| 959 If an expression evaluates to ``default`` then the expression is | |
| 960 "cancelled" - whatever HTML already exists in the template will remain | |
| 961 (tag content in the case of tal:content, attributes in the case of | |
| 962 tal:attributes). | |
| 963 | |
| 964 If an expression evaluates to ``nothing`` then the target of the expression | |
| 965 is removed (tag content in the case of tal:content, attributes in the case | |
| 966 of tal:attributes and the tag itself in the case of tal:replace). | |
| 967 | |
| 968 If an element in the path may not exist, then you can use the ``|`` | |
| 969 operator in the expression to provide an alternative. So, the expression | |
| 970 ``request/form/foo/value | default`` would simply leave the current HTML | |
| 971 in place if the "foo" form variable doesn't exist. | |
| 972 | |
| 973 **String Expressions** - eg. ``string:hello ${user/name}`` | |
| 974 These expressions are simple string interpolations (though they can be just | |
| 975 plain strings with no interpolation if you want. The expression in the | |
| 976 ``${ ... }`` is just a path expression as above. | |
| 977 | |
| 978 **Python Expressions** - eg. ``python: 1+1`` | |
| 979 These expressions give the full power of Python. All the "root level" | |
| 980 variables are available, so ``python:item.status.checklist()`` would be | |
| 981 equivalent to ``item/status/checklist``, assuming that ``checklist`` is | |
| 982 a method. | |
| 983 | |
| 984 Template Macros | |
| 985 ~~~~~~~~~~~~~~~ | |
| 986 | |
| 987 Macros are used in Roundup to save us from repeating the same common page | |
| 988 stuctures over and over. The most common (and probably only) macro you'll use | |
| 989 is the "icing" macro defined in the "page" template. | |
| 990 | |
| 991 Macros are generated and used inside your templates using special attributes | |
| 992 similar to the `basic templating actions`_. In this case though, the | |
| 993 attributes belong to the Macro Expansion Template Attribute Language, or | |
| 994 METAL. The macro commands are: | |
| 995 | |
| 996 **metal:define-macro="macro name"** | |
| 997 Define that the tag and its contents are now a macro that may be inserted | |
| 998 into other templates using the *use-macro* command. For example:: | |
| 999 | |
| 1000 <html metal:define-macro="page"> | |
| 1001 ... | |
| 1002 </html> | |
| 1003 | |
| 1004 defines a macro called "page" using the ``<html>`` tag and its contents. | |
| 1005 Once defined, macros are stored on the template they're defined on in the | |
| 1006 ``macros`` attribute. You can access them later on through the ``templates`` | |
| 1007 variable, eg. the most common ``templates/page/macros/icing`` to access the | |
| 1008 "page" macro of the "page" template. | |
| 1009 | |
| 1010 **metal:use-macro="path expression"** | |
| 1011 Use a macro, which is identified by the path expression (see above). This | |
| 1012 will replace the current tag with the identified macro contents. For | |
| 1013 example:: | |
| 1014 | |
| 1015 <tal:block metal:use-macro="templates/page/macros/icing"> | |
| 1016 ... | |
| 1017 </tal:block> | |
| 1018 | |
| 1019 will replace the tag and its contents with the "page" macro of the "page" | |
| 1020 template. | |
| 1021 | |
| 1022 **metal:define-slot="slot name"** and **metal:fill-slot="slot name"** | |
| 1023 To define *dynamic* parts of the macro, you define "slots" which may be | |
| 1024 filled when the macro is used with a *use-macro* command. For example, the | |
| 1025 ``templates/page/macros/icing`` macro defines a slot like so:: | |
| 1026 | |
| 1027 <title metal:define-slot="head_title">title goes here</title> | |
| 1028 | |
| 1029 In your *use-macro* command, you may now use a *fill-slot* command like | |
| 1030 this:: | |
| 1031 | |
| 1032 <title metal:fill-slot="head_title">My Title</title> | |
| 1033 | |
| 1034 where the tag that fills the slot completely replaces the one defined as | |
| 1035 the slot in the macro. | |
| 1036 | |
| 1037 Note that you may not mix METAL and TAL commands on the same tag, but TAL | |
| 1038 commands may be used freely inside METAL-using tags (so your *fill-slots* | |
| 1039 tags may have all manner of TAL inside them). | |
| 1040 | |
| 1041 | |
| 1042 Information available to templates | |
| 1043 ---------------------------------- | |
| 1044 | |
| 1045 Note: this is implemented by roundup.cgi.templating.RoundupPageTemplate | |
| 1046 | |
| 1047 The following variables are available to templates. | |
| 1048 | |
| 1049 **context** | |
| 1050 The current context. This is either None, a | |
| 1051 `hyperdb class wrapper`_ or a `hyperdb item wrapper`_ | |
| 1052 **request** | |
| 1053 Includes information about the current request, including: | |
| 1054 - the current index information (``filterspec``, ``filter`` args, | |
| 1055 ``properties``, etc) parsed out of the form. | |
| 1056 - methods for easy filterspec link generation | |
| 1057 - *user*, the current user item as an HTMLItem instance | |
| 1058 - *form* | |
| 1059 The current CGI form information as a mapping of form argument | |
| 1060 name to value | |
| 1061 **config** | |
| 1062 This variable holds all the values defined in the tracker config.py file | |
| 1063 (eg. TRACKER_NAME, etc.) | |
| 1064 **db** | |
| 1065 The current database, used to access arbitrary database items. | |
| 1066 **templates** | |
| 1067 Access to all the tracker templates by name. Used mainly in *use-macro* | |
| 1068 commands. | |
| 1069 **utils** | |
| 1070 This variable makes available some utility functions like batching. | |
| 1071 **nothing** | |
| 1072 This is a special variable - if an expression evaluates to this, then the | |
| 1073 tag (in the case of a tal:replace), its contents (in the case of | |
| 1074 tal:content) or some attributes (in the case of tal:attributes) will not | |
| 1075 appear in the the output. So for example:: | |
| 1076 | |
| 1077 <span tal:attributes="class nothing">Hello, World!</span> | |
| 1078 | |
| 1079 would result in:: | |
| 1080 | |
| 1081 <span>Hello, World!</span> | |
| 1082 | |
| 1083 **default** | |
| 1084 Also a special variable - if an expression evaluates to this, then the | |
| 1085 existing HTML in the template will not be replaced or removed, it will | |
| 1086 remain. So:: | |
| 1087 | |
| 1088 <span tal:replace="default">Hello, World!</span> | |
| 1089 | |
| 1090 would result in:: | |
| 1091 | |
| 1092 <span>Hello, World!</span> | |
| 1093 | |
| 1094 The context variable | |
| 1095 ~~~~~~~~~~~~~~~~~~~~ | |
| 1096 | |
| 1097 The *context* variable is one of three things based on the current context | |
| 1098 (see `determining web context`_ for how we figure this out): | |
| 1099 | |
| 1100 1. if we're looking at a "home" page, then it's None | |
| 1101 2. if we're looking at a specific hyperdb class, it's a | |
| 1102 `hyperdb class wrapper`_. | |
| 1103 3. if we're looking at a specific hyperdb item, it's a | |
| 1104 `hyperdb item wrapper`_. | |
| 1105 | |
| 1106 If the context is not None, we can access the properties of the class or item. | |
| 1107 The only real difference between cases 2 and 3 above are: | |
| 1108 | |
| 1109 1. the properties may have a real value behind them, and this will appear if | |
| 1110 the property is displayed through ``context/property`` or | |
| 1111 ``context/property/field``. | |
| 1112 2. the context's "id" property will be a false value in the second case, but | |
| 1113 a real, or true value in the third. Thus we can determine whether we're | |
| 1114 looking at a real item from the hyperdb by testing "context/id". | |
| 1115 | |
| 1116 Hyperdb class wrapper | |
| 1117 ::::::::::::::::::::: | |
| 1118 | |
| 1119 Note: this is implemented by the roundup.cgi.templating.HTMLClass class. | |
| 1120 | |
| 1121 This wrapper object provides access to a hyperb class. It is used primarily | |
| 1122 in both index view and new item views, but it's also usable anywhere else that | |
| 1123 you wish to access information about a class, or the items of a class, when | |
| 1124 you don't have a specific item of that class in mind. | |
| 1125 | |
| 1126 We allow access to properties. There will be no "id" property. The value | |
| 1127 accessed through the property will be the current value of the same name from | |
| 1128 the CGI form. | |
| 1129 | |
| 1130 There are several methods available on these wrapper objects: | |
| 1131 | |
| 1132 =========== ============================================================= | |
| 1133 Method Description | |
| 1134 =========== ============================================================= | |
| 1135 properties return a `hyperdb property wrapper`_ for all of this class' | |
| 1136 properties. | |
| 1137 list lists all of the active (not retired) items in the class. | |
| 1138 csv return the items of this class as a chunk of CSV text. | |
| 1139 propnames lists the names of the properties of this class. | |
| 1140 filter lists of items from this class, filtered and sorted | |
| 1141 by the current *request* filterspec/filter/sort/group args | |
| 1142 classhelp display a link to a javascript popup containing this class' | |
| 1143 "help" template. | |
| 1144 submit generate a submit button (and action hidden element) | |
| 1145 renderWith render this class with the given template. | |
| 1146 history returns 'New node - no history' :) | |
| 1147 is_edit_ok is the user allowed to Edit the current class? | |
| 1148 is_view_ok is the user allowed to View the current class? | |
| 1149 =========== ============================================================= | |
| 1150 | |
| 1151 Note that if you have a property of the same name as one of the above methods, | |
| 1152 you'll need to access it using a python "item access" expression. For example:: | |
| 1153 | |
| 1154 python:context['list'] | |
| 1155 | |
| 1156 will access the "list" property, rather than the list method. | |
| 1157 | |
| 1158 | |
| 1159 Hyperdb item wrapper | |
| 1160 :::::::::::::::::::: | |
| 1161 | |
| 1162 Note: this is implemented by the roundup.cgi.templating.HTMLItem class. | |
| 1163 | |
| 1164 This wrapper object provides access to a hyperb item. | |
| 1165 | |
| 1166 We allow access to properties. There will be no "id" property. The value | |
| 1167 accessed through the property will be the current value of the same name from | |
| 1168 the CGI form. | |
| 1169 | |
| 1170 There are several methods available on these wrapper objects: | |
| 1171 | |
| 1172 =============== ============================================================= | |
| 1173 Method Description | |
| 1174 =============== ============================================================= | |
| 1175 submit generate a submit button (and action hidden element) | |
| 1176 journal return the journal of the current item (**not implemented**) | |
| 1177 history render the journal of the current item as HTML | |
| 1178 renderQueryForm specific to the "query" class - render the search form for | |
| 1179 the query | |
| 1180 hasPermission specific to the "user" class - determine whether the user | |
| 1181 has a Permission | |
| 1182 is_edit_ok is the user allowed to Edit the current item? | |
| 1183 is_view_ok is the user allowed to View the current item? | |
| 1184 =============== ============================================================= | |
| 1185 | |
| 1186 | |
| 1187 Note that if you have a property of the same name as one of the above methods, | |
| 1188 you'll need to access it using a python "item access" expression. For example:: | |
| 1189 | |
| 1190 python:context['journal'] | |
| 1191 | |
| 1192 will access the "journal" property, rather than the journal method. | |
| 1193 | |
| 1194 | |
| 1195 Hyperdb property wrapper | |
| 1196 :::::::::::::::::::::::: | |
| 1197 | |
| 1198 Note: this is implemented by subclasses roundup.cgi.templating.HTMLProperty | |
| 1199 class (HTMLStringProperty, HTMLNumberProperty, and so on). | |
| 1200 | |
| 1201 This wrapper object provides access to a single property of a class. Its | |
| 1202 value may be either: | |
| 1203 | |
| 1204 1. if accessed through a `hyperdb item wrapper`_, then it's a value from the | |
| 1205 hyperdb | |
| 1206 2. if access through a `hyperdb class wrapper`_, then it's a value from the | |
| 1207 CGI form | |
| 1208 | |
| 1209 | |
| 1210 The property wrapper has some useful attributes: | |
| 1211 | |
| 1212 =============== ============================================================= | |
| 1213 Attribute Description | |
| 1214 =============== ============================================================= | |
| 1215 _name the name of the property | |
| 1216 _value the value of the property if any - this is the actual value | |
| 1217 retrieved from the hyperdb for this property | |
| 1218 =============== ============================================================= | |
| 1219 | |
| 1220 There are several methods available on these wrapper objects: | |
| 1221 | |
| 1222 =========== ================================================================= | |
| 1223 Method Description | |
| 1224 =========== ================================================================= | |
| 1225 plain render a "plain" representation of the property | |
| 1226 field render an appropriate form edit field for the property - for most | |
| 1227 types this is a text entry box, but for Booleans it's a tri-state | |
| 1228 yes/no/neither selection. | |
| 1229 stext only on String properties - render the value of the | |
| 1230 property as StructuredText (requires the StructureText module | |
| 1231 to be installed separately) | |
| 1232 multiline only on String properties - render a multiline form edit | |
| 1233 field for the property | |
| 1234 email only on String properties - render the value of the | |
| 1235 property as an obscured email address | |
| 1236 confirm only on Password properties - render a second form edit field for | |
| 1237 the property, used for confirmation that the user typed the | |
| 1238 password correctly. Generates a field with name "name:confirm". | |
| 1239 reldate only on Date properties - render the interval between the | |
| 1240 date and now | |
| 1241 pretty only on Interval properties - render the interval in a | |
| 1242 pretty format (eg. "yesterday") | |
| 1243 menu only on Link and Multilink properties - render a form select | |
| 1244 list for this property | |
| 1245 reverse only on Multilink properties - produce a list of the linked | |
| 1246 items in reverse order | |
| 1247 =========== ================================================================= | |
| 1248 | |
| 1249 The request variable | |
| 1250 ~~~~~~~~~~~~~~~~~~~~ | |
| 1251 | |
| 1252 Note: this is implemented by the roundup.cgi.templating.HTMLRequest class. | |
| 1253 | |
| 1254 The request variable is packed with information about the current request. | |
| 1255 | |
| 1256 .. taken from roundup.cgi.templating.HTMLRequest docstring | |
| 1257 | |
| 1258 =========== ================================================================= | |
| 1259 Variable Holds | |
| 1260 =========== ================================================================= | |
| 1261 form the CGI form as a cgi.FieldStorage | |
| 1262 env the CGI environment variables | |
| 1263 base the base URL for this tracker | |
| 1264 user a HTMLUser instance for this user | |
| 1265 classname the current classname (possibly None) | |
| 1266 template the current template (suffix, also possibly None) | |
| 1267 form the current CGI form variables in a FieldStorage | |
| 1268 =========== ================================================================= | |
| 1269 | |
| 1270 **Index page specific variables (indexing arguments)** | |
| 1271 | |
| 1272 =========== ================================================================= | |
| 1273 Variable Holds | |
| 1274 =========== ================================================================= | |
| 1275 columns dictionary of the columns to display in an index page | |
| 1276 show a convenience access to columns - request/show/colname will | |
| 1277 be true if the columns should be displayed, false otherwise | |
| 1278 sort index sort column (direction, column name) | |
| 1279 group index grouping property (direction, column name) | |
| 1280 filter properties to filter the index on | |
| 1281 filterspec values to filter the index on | |
| 1282 search_text text to perform a full-text search on for an index | |
| 1283 =========== ================================================================= | |
| 1284 | |
| 1285 There are several methods available on the request variable: | |
| 1286 | |
| 1287 =============== ============================================================= | |
| 1288 Method Description | |
| 1289 =============== ============================================================= | |
| 1290 description render a description of the request - handle for the page | |
| 1291 title | |
| 1292 indexargs_form render the current index args as form elements | |
| 1293 indexargs_url render the current index args as a URL | |
| 1294 base_javascript render some javascript that is used by other components of | |
| 1295 the templating | |
| 1296 batch run the current index args through a filter and return a | |
| 1297 list of items (see `hyperdb item wrapper`_, and | |
| 1298 `batching`_) | |
| 1299 =============== ============================================================= | |
| 1300 | |
| 1301 The form variable | |
| 1302 ::::::::::::::::: | |
| 1303 | |
| 1304 The form variable is a little special because it's actually a python | |
| 1305 FieldStorage object. That means that you have two ways to access its | |
| 1306 contents. For example, to look up the CGI form value for the variable | |
| 1307 "name", use the path expression:: | |
| 1308 | |
| 1309 request/form/name/value | |
| 1310 | |
| 1311 or the python expression:: | |
| 1312 | |
| 1313 python:request.form['name'].value | |
| 1314 | |
| 1315 Note the "item" access used in the python case, and also note the explicit | |
| 1316 "value" attribute we have to access. That's because the form variables are | |
| 1317 stored as MiniFieldStorages. If there's more than one "name" value in | |
| 1318 the form, then the above will break since ``request/form/name`` is actually a | |
| 1319 *list* of MiniFieldStorages. So it's best to know beforehand what you're | |
| 1320 dealing with. | |
| 1321 | |
| 1322 | |
| 1323 The db variable | |
| 1324 ~~~~~~~~~~~~~~~ | |
| 1325 | |
| 1326 Note: this is implemented by the roundup.cgi.templating.HTMLDatabase class. | |
| 1327 | |
| 1328 Allows access to all hyperdb classes as attributes of this variable. If you | |
| 1329 want access to the "user" class, for example, you would use:: | |
| 1330 | |
| 1331 db/user | |
| 1332 python:db.user | |
| 1333 | |
| 1334 The access results in a `hyperdb class wrapper`_. | |
| 1335 | |
| 1336 The templates variable | |
| 1337 ~~~~~~~~~~~~~~~~~~~~~~ | |
| 1338 | |
| 1339 Note: this is implemented by the roundup.cgi.templating.Templates class. | |
| 1340 | |
| 1341 This variable doesn't have any useful methods defined. It supports being | |
| 1342 used in expressions to access the templates, and subsequently the template | |
| 1343 macros. You may access the templates using the following path expression:: | |
| 1344 | |
| 1345 templates/name | |
| 1346 | |
| 1347 or the python expression:: | |
| 1348 | |
| 1349 templates[name] | |
| 1350 | |
| 1351 where "name" is the name of the template you wish to access. The template you | |
| 1352 get access to has one useful attribute, "macros". To access a specific macro | |
| 1353 (called "macro_name"), use the path expression:: | |
| 1354 | |
| 1355 templates/name/macros/macro_name | |
| 1356 | |
| 1357 or the python expression:: | |
| 1358 | |
| 1359 templates[name].macros[macro_name] | |
| 1360 | |
| 1361 | |
| 1362 The utils variable | |
| 1363 ~~~~~~~~~~~~~~~~~~ | |
| 1364 | |
| 1365 Note: this is implemented by the roundup.cgi.templating.TemplatingUtils class, | |
| 1366 but it may be extended as described below. | |
| 1367 | |
| 1368 =============== ============================================================= | |
| 1369 Method Description | |
| 1370 =============== ============================================================= | |
| 1371 Batch return a batch object using the supplied list | |
| 1372 =============== ============================================================= | |
| 1373 | |
| 1374 You may add additional utility methods by writing them in your tracker | |
| 1375 ``interfaces.py`` module's ``TemplatingUtils`` class. See `adding a time log | |
| 1376 to your issues`_ for an example. The TemplatingUtils class itself will have a | |
| 1377 single attribute, ``client``, which may be used to access the ``client.db`` | |
| 1378 when you need to perform arbitrary database queries. | |
| 1379 | |
| 1380 Batching | |
| 1381 :::::::: | |
| 1382 | |
| 1383 Use Batch to turn a list of items, or item ids of a given class, into a series | |
| 1384 of batches. Its usage is:: | |
| 1385 | |
| 1386 python:util.Batch(sequence, size, start, end=0, orphan=0, overlap=0) | |
| 1387 | |
| 1388 or, to get the current index batch:: | |
| 1389 | |
| 1390 request/batch | |
| 1391 | |
| 1392 The parameters are: | |
| 1393 | |
| 1394 ========= ================================================================== | |
| 1395 Parameter Usage | |
| 1396 ========= ================================================================== | |
| 1397 sequence a list of HTMLItems | |
| 1398 size how big to make the sequence. | |
| 1399 start where to start (0-indexed) in the sequence. | |
| 1400 end where to end (0-indexed) in the sequence. | |
| 1401 orphan if the next batch would contain less items than this | |
| 1402 value, then it is combined with this batch | |
| 1403 overlap the number of items shared between adjacent batches | |
| 1404 ========= ================================================================== | |
| 1405 | |
| 1406 All of the parameters are assigned as attributes on the batch object. In | |
| 1407 addition, it has several more attributes: | |
| 1408 | |
| 1409 =============== ============================================================ | |
| 1410 Attribute Description | |
| 1411 =============== ============================================================ | |
| 1412 start indicates the start index of the batch. *Note: unlike the | |
| 1413 argument, is a 1-based index (I know, lame)* | |
| 1414 first indicates the start index of the batch *as a 0-based | |
| 1415 index* | |
| 1416 length the actual number of elements in the batch | |
| 1417 sequence_length the length of the original, unbatched, sequence. | |
| 1418 =============== ============================================================ | |
| 1419 | |
| 1420 And several methods: | |
| 1421 | |
| 1422 =============== ============================================================ | |
| 1423 Method Description | |
| 1424 =============== ============================================================ | |
| 1425 previous returns a new Batch with the previous batch settings | |
| 1426 next returns a new Batch with the next batch settings | |
| 1427 propchanged detect if the named property changed on the current item | |
| 1428 when compared to the last item | |
| 1429 =============== ============================================================ | |
| 1430 | |
| 1431 An example of batching:: | |
| 1432 | |
| 1433 <table class="otherinfo"> | |
| 1434 <tr><th colspan="4" class="header">Existing Keywords</th></tr> | |
| 1435 <tr tal:define="keywords db/keyword/list" | |
| 1436 tal:repeat="start python:range(0, len(keywords), 4)"> | |
| 1437 <td tal:define="batch python:utils.Batch(keywords, 4, start)" | |
| 1438 tal:repeat="keyword batch" tal:content="keyword/name">keyword here</td> | |
| 1439 </tr> | |
| 1440 </table> | |
| 1441 | |
| 1442 ... which will produce a table with four columns containing the items of the | |
| 1443 "keyword" class (well, their "name" anyway). | |
| 1444 | |
| 1445 Displaying Properties | |
| 1446 --------------------- | |
| 1447 | |
| 1448 Properties appear in the user interface in three contexts: in indices, in | |
| 1449 editors, and as search arguments. | |
| 1450 For each type of property, there are several display possibilities. | |
| 1451 For example, in an index view, a string property may just be | |
| 1452 printed as a plain string, but in an editor view, that property may be | |
| 1453 displayed in an editable field. | |
| 1454 | |
| 1455 | |
| 1456 Index Views | |
| 1457 ----------- | |
| 1458 | |
| 1459 This is one of the class context views. It is also the default view for | |
| 1460 classes. The template used is "*classname*.index". | |
| 1461 | |
| 1462 Index View Specifiers | |
| 1463 ~~~~~~~~~~~~~~~~~~~~~ | |
| 1464 | |
| 1465 An index view specifier (URL fragment) looks like this (whitespace has been | |
| 1466 added for clarity):: | |
| 1467 | |
| 1468 /issue?status=unread,in-progress,resolved& | |
| 1469 topic=security,ui& | |
| 1470 :group=+priority& | |
| 1471 :sort==activity& | |
| 1472 :filters=status,topic& | |
| 1473 :columns=title,status,fixer | |
| 1474 | |
| 1475 The index view is determined by two parts of the specifier: the layout part and | |
| 1476 the filter part. The layout part consists of the query parameters that begin | |
| 1477 with colons, and it determines the way that the properties of selected items | |
| 1478 are displayed. The filter part consists of all the other query parameters, and | |
| 1479 it determines the criteria by which items are selected for display. | |
| 1480 The filter part is interactively manipulated with the form widgets displayed in | |
| 1481 the filter section. The layout part is interactively manipulated by clicking on | |
| 1482 the column headings in the table. | |
| 1483 | |
| 1484 The filter part selects the union of the sets of items with values matching any | |
| 1485 specified Link properties and the intersection of the sets of items with values | |
| 1486 matching any specified Multilink properties. | |
| 1487 | |
| 1488 The example specifies an index of "issue" items. Only items with a "status" of | |
| 1489 either "unread" or "in-progres" or "resolved" are displayed, and only items | |
| 1490 with "topic" values including both "security" and "ui" are displayed. The items | |
| 1491 are grouped by priority, arranged in ascending order; and within groups, sorted | |
| 1492 by activity, arranged in descending order. The filter section shows filters for | |
| 1493 the "status" and "topic" properties, and the table includes columns for the | |
| 1494 "title", "status", and "fixer" properties. | |
| 1495 | |
| 1496 Searching Views | |
| 1497 --------------- | |
| 1498 | |
| 1499 Note: if you add a new column to the ``:columns`` form variable potentials | |
| 1500 then you will need to add the column to the appropriate `index views`_ | |
| 1501 template so it is actually displayed. | |
| 1502 | |
| 1503 This is one of the class context views. The template used is typically | |
| 1504 "*classname*.search". The form on this page should have "search" as its | |
| 1505 ``:action`` variable. The "search" action: | |
| 1506 | |
| 1507 - sets up additional filtering, as well as performing indexed text searching | |
| 1508 - sets the ``:filter`` variable correctly | |
| 1509 - saves the query off if ``:query_name`` is set. | |
| 1510 | |
| 1511 The searching page should lay out any fields that you wish to allow the user | |
| 1512 to search one. If your schema contains a large number of properties, you | |
| 1513 should be wary of making all of those properties available for searching, as | |
| 1514 this can cause confusion. If the additional properties are Strings, consider | |
| 1515 having their value indexed, and then they will be searchable using the full | |
| 1516 text indexed search. This is both faster, and more useful for the end user. | |
| 1517 | |
| 1518 The two special form values on search pages which are handled by the "search" | |
| 1519 action are: | |
| 1520 | |
| 1521 :search_text | |
| 1522 Text to perform a search of the text index with. Results from that search | |
| 1523 will be used to limit the results of other filters (using an intersection | |
| 1524 operation) | |
| 1525 :query_name | |
| 1526 If supplied, the search parameters (including :search_text) will be saved | |
| 1527 off as a the query item and registered against the user's queries property. | |
| 1528 Note that the *classic* template schema has this ability, but the *minimal* | |
| 1529 template schema does not. | |
| 1530 | |
| 1531 | |
| 1532 Item Views | |
| 1533 ---------- | |
| 1534 | |
| 1535 The basic view of a hyperdb item is provided by the "*classname*.item" | |
| 1536 template. It generally has three sections; an "editor", a "spool" and a | |
| 1537 "history" section. | |
| 1538 | |
| 1539 | |
| 1540 | |
| 1541 Editor Section | |
| 1542 ~~~~~~~~~~~~~~ | |
| 1543 | |
| 1544 The editor section is used to manipulate the item - it may be a | |
| 1545 static display if the user doesn't have permission to edit the item. | |
| 1546 | |
| 1547 Here's an example of a basic editor template (this is the default "classic" | |
| 1548 template issue item edit form - from the "issue.item" template):: | |
| 1549 | |
| 1550 <table class="form"> | |
| 1551 <tr> | |
| 1552 <th nowrap>Title</th> | |
| 1553 <td colspan=3 tal:content="structure python:context.title.field(size=60)">title</td> | |
| 1554 </tr> | |
| 1555 | |
| 1556 <tr> | |
| 1557 <th nowrap>Priority</th> | |
| 1558 <td tal:content="structure context/priority/menu">priority</td> | |
| 1559 <th nowrap>Status</th> | |
| 1560 <td tal:content="structure context/status/menu">status</td> | |
| 1561 </tr> | |
| 1562 | |
| 1563 <tr> | |
| 1564 <th nowrap>Superseder</th> | |
| 1565 <td> | |
| 1566 <span tal:replace="structure python:context.superseder.field(showid=1, size=20)" /> | |
| 1567 <span tal:replace="structure python:db.issue.classhelp('id,title')" /> | |
| 1568 <span tal:condition="context/superseder"> | |
| 1569 <br>View: <span tal:replace="structure python:context.superseder.link(showid=1)" /> | |
| 1570 </span> | |
| 1571 </td> | |
| 1572 <th nowrap>Nosy List</th> | |
| 1573 <td> | |
| 1574 <span tal:replace="structure context/nosy/field" /> | |
| 1575 <span tal:replace="structure python:db.user.classhelp('username,realname,address,phone')" /> | |
| 1576 </td> | |
| 1577 </tr> | |
| 1578 | |
| 1579 <tr> | |
| 1580 <th nowrap>Assigned To</th> | |
| 1581 <td tal:content="structure context/assignedto/menu"> | |
| 1582 assignedto menu | |
| 1583 </td> | |
| 1584 <td> </td> | |
| 1585 <td> </td> | |
| 1586 </tr> | |
| 1587 | |
| 1588 <tr> | |
| 1589 <th nowrap>Change Note</th> | |
| 1590 <td colspan=3> | |
| 1591 <textarea name=":note" wrap="hard" rows="5" cols="60"></textarea> | |
| 1592 </td> | |
| 1593 </tr> | |
| 1594 | |
| 1595 <tr> | |
| 1596 <th nowrap>File</th> | |
| 1597 <td colspan=3><input type="file" name=":file" size="40"></td> | |
| 1598 </tr> | |
| 1599 | |
| 1600 <tr> | |
| 1601 <td> </td> | |
| 1602 <td colspan=3 tal:content="structure context/submit"> | |
| 1603 submit button will go here | |
| 1604 </td> | |
| 1605 </tr> | |
| 1606 </table> | |
| 1607 | |
| 1608 | |
| 1609 When a change is submitted, the system automatically generates a message | |
| 1610 describing the changed properties. As shown in the example, the editor | |
| 1611 template can use the ":note" and ":file" fields, which are added to the | |
| 1612 standard change note message generated by Roundup. | |
| 1613 | |
| 1614 Spool Section | |
| 1615 ~~~~~~~~~~~~~ | |
| 1616 | |
| 1617 The spool section lists related information like the messages and files of | |
| 1618 an issue. | |
| 1619 | |
| 1620 TODO | |
| 1621 | |
| 1622 | |
| 1623 History Section | |
| 1624 ~~~~~~~~~~~~~~~ | |
| 1625 | |
| 1626 The final section displayed is the history of the item - its database journal. | |
| 1627 This is generally generated with the template:: | |
| 1628 | |
| 1629 <tal:block tal:replace="structure context/history" /> | |
| 1630 | |
| 1631 *To be done:* | |
| 1632 | |
| 1633 *The actual history entries of the item may be accessed for manual templating | |
| 1634 through the "journal" method of the item*:: | |
| 1635 | |
| 1636 <tal:block tal:repeat="entry context/journal"> | |
| 1637 a journal entry | |
| 1638 </tal:block> | |
| 1639 | |
| 1640 *where each journal entry is an HTMLJournalEntry.* | |
| 1641 | |
| 1642 Defining new web actions | |
| 1643 ------------------------ | |
| 1644 | |
| 1645 You may define new actions to be triggered by the ``:action`` form variable. | |
| 1646 These are added to the tracker ``interfaces.py`` as methods on the ``Client`` | |
| 1647 class. | |
| 1648 | |
| 1649 Adding action methods takes three steps; first you `define the new action | |
| 1650 method`_, then you `register the action method`_ with the cgi interface so | |
| 1651 it may be triggered by the ``:action`` form variable. Finally you actually | |
| 1652 `use the new action`_ in your HTML form. | |
| 1653 | |
| 1654 See "`setting up a "wizard" (or "druid") for controlled adding of issues`_" | |
| 1655 for an example. | |
| 1656 | |
| 1657 Define the new action method | |
| 1658 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
| 1659 | |
| 1660 The action methods have the following interface:: | |
| 1661 | |
| 1662 def myActionMethod(self): | |
| 1663 ''' Perform some action. No return value is required. | |
| 1664 ''' | |
| 1665 | |
| 1666 The *self* argument is an instance of your tracker ``instance.Client`` class - | |
| 1667 thus it's mostly implemented by ``roundup.cgi.Client``. See the docstring of | |
| 1668 that class for details of what it can do. | |
| 1669 | |
| 1670 The method will typically check the ``self.form`` variable's contents. It | |
| 1671 may then: | |
| 1672 | |
| 1673 - add information to ``self.ok_message`` or ``self.error_message`` | |
| 1674 - change the ``self.template`` variable to alter what the user will see next | |
| 1675 - raise Unauthorised, SendStaticFile, SendFile, NotFound or Redirect | |
| 1676 exceptions | |
| 1677 | |
| 1678 | |
| 1679 Register the action method | |
| 1680 ~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
| 1681 | |
| 1682 The method is now written, but isn't available to the user until you add it to | |
| 1683 the `instance.Client`` class ``actions`` variable, like so:: | |
| 1684 | |
| 1685 actions = client.Class.actions + ( | |
| 1686 ('myaction', 'myActionMethod'), | |
| 1687 ) | |
| 1688 | |
| 1689 This maps the action name "myaction" to the action method we defined. | |
| 1690 | |
| 1691 | |
| 1692 Use the new action | |
| 1693 ~~~~~~~~~~~~~~~~~~ | |
| 1694 | |
| 1695 In your HTML form, add a hidden form element like so:: | |
| 1696 | |
| 1697 <input type="hidden" name=":action" value="myaction"> | |
| 1698 | |
| 1699 where "myaction" is the name you registered in the previous step. | |
| 1700 | |
| 1701 | |
| 1702 Examples | |
| 1703 ======== | |
| 1704 | |
| 1705 .. contents:: | |
| 1706 :local: | |
| 1707 :depth: 1 | |
| 1708 | |
| 1709 Adding a new field to the classic schema | |
| 1710 ---------------------------------------- | |
| 1711 | |
| 1712 This example shows how to add a new constrained property (ie. a selection of | |
| 1713 distinct values) to your tracker. | |
| 1714 | |
| 1715 Introduction | |
| 1716 ~~~~~~~~~~~~ | |
| 1717 | |
| 1718 To make the classic schema of roundup useful as a todo tracking system | |
| 1719 for a group of systems administrators, it needed an extra data field | |
| 1720 per issue: a category. | |
| 1721 | |
| 1722 This would let sysads quickly list all todos in their particular | |
| 1723 area of interest without having to do complex queries, and without | |
| 1724 relying on the spelling capabilities of other sysads (a losing | |
| 1725 proposition at best). | |
| 1726 | |
| 1727 Adding a field to the database | |
| 1728 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
| 1729 | |
| 1730 This is the easiest part of the change. The category would just be a plain | |
| 1731 string, nothing fancy. To change what is in the database you need to add | |
| 1732 some lines to the ``open()`` function in ``dbinit.py`` under the comment:: | |
| 1733 | |
| 1734 # add any additional database schema configuration here | |
| 1735 | |
| 1736 add:: | |
| 1737 | |
| 1738 category = Class(db, "category", name=String()) | |
| 1739 category.setkey("name") | |
| 1740 | |
| 1741 Here we are setting up a chunk of the database which we are calling | |
| 1742 "category". It contains a string, which we are refering to as "name" for | |
| 1743 lack of a more imaginative title. Then we are setting the key of this chunk | |
| 1744 of the database to be that "name". This is equivalent to an index for | |
| 1745 database types. This also means that there can only be one category with a | |
| 1746 given name. | |
| 1747 | |
| 1748 Adding the above lines allows us to create categories, but they're not tied | |
| 1749 to the issues that we are going to be creating. It's just a list of categories | |
| 1750 off on its own, which isn't much use. We need to link it in with the issues. | |
| 1751 To do that, find the lines in the ``open()`` function in ``dbinit.py`` which | |
| 1752 set up the "issue" class, and then add a link to the category:: | |
| 1753 | |
| 1754 issue = IssueClass(db, "issue", ... , category=Multilink("category"), ... ) | |
| 1755 | |
| 1756 The Multilink() means that each issue can have many categories. If you were | |
| 1757 adding something with a more one to one relationship use Link() instead. | |
| 1758 | |
| 1759 That is all you need to do to change the schema. The rest of the effort is | |
| 1760 fiddling around so you can actually use the new category. | |
| 1761 | |
| 1762 Populating the new category class | |
| 1763 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
| 1764 | |
| 1765 If you haven't initialised the database with the roundup-admin "initialise" | |
| 1766 command, then you can add the following to the tracker ``dbinit.py`` in the | |
| 1767 ``init()`` function under the comment:: | |
| 1768 | |
| 1769 # add any additional database create steps here - but only if you | |
| 1770 # haven't initialised the database with the admin "initialise" command | |
| 1771 | |
| 1772 add:: | |
| 1773 | |
| 1774 category = db.getclass('category') | |
| 1775 category.create(name="scipy", order="1") | |
| 1776 category.create(name="chaco", order="2") | |
| 1777 category.create(name="weave", order="3") | |
| 1778 | |
| 1779 If the database is initalised, the you need to use the roundup-admin tool:: | |
| 1780 | |
| 1781 % roundup-admin -i <tracker home> | |
| 1782 Roundup <version> ready for input. | |
| 1783 Type "help" for help. | |
| 1784 roundup> create category name=scipy order=1 | |
| 1785 1 | |
| 1786 roundup> create category name=chaco order=1 | |
| 1787 2 | |
| 1788 roundup> create category name=weave order=1 | |
| 1789 3 | |
| 1790 roundup> exit... | |
| 1791 There are unsaved changes. Commit them (y/N)? y | |
| 1792 | |
| 1793 | |
| 1794 Setting up security on the new objects | |
| 1795 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
| 1796 | |
| 1797 By default only the admin user can look at and change objects. This doesn't | |
| 1798 suit us, as we want any user to be able to create new categories as | |
| 1799 required, and obviously everyone needs to be able to view the categories of | |
| 1800 issues for it to be useful. | |
| 1801 | |
| 1802 We therefore need to change the security of the category objects. This is | |
| 1803 also done in the ``open()`` function of ``dbinit.py``. | |
| 1804 | |
| 1805 There are currently two loops which set up permissions and then assign them | |
| 1806 to various roles. Simply add the new "category" to both lists:: | |
| 1807 | |
| 1808 # new permissions for this schema | |
| 1809 for cl in 'issue', 'file', 'msg', 'user', 'category': | |
| 1810 db.security.addPermission(name="Edit", klass=cl, | |
| 1811 description="User is allowed to edit "+cl) | |
| 1812 db.security.addPermission(name="View", klass=cl, | |
| 1813 description="User is allowed to access "+cl) | |
| 1814 | |
| 1815 # Assign the access and edit permissions for issue, file and message | |
| 1816 # to regular users now | |
| 1817 for cl in 'issue', 'file', 'msg', 'category': | |
| 1818 p = db.security.getPermission('View', cl) | |
| 1819 db.security.addPermissionToRole('User', p) | |
| 1820 p = db.security.getPermission('Edit', cl) | |
| 1821 db.security.addPermissionToRole('User', p) | |
| 1822 | |
| 1823 So you are in effect doing the following:: | |
| 1824 | |
| 1825 db.security.addPermission(name="Edit", klass='category', | |
| 1826 description="User is allowed to edit "+'category') | |
| 1827 db.security.addPermission(name="View", klass='category', | |
| 1828 description="User is allowed to access "+'category') | |
| 1829 | |
| 1830 which is creating two permission types; that of editing and viewing | |
| 1831 "category" objects respectively. Then the following lines assign those new | |
| 1832 permissions to the "User" role, so that normal users can view and edit | |
| 1833 "category" objects:: | |
| 1834 | |
| 1835 p = db.security.getPermission('View', 'category') | |
| 1836 db.security.addPermissionToRole('User', p) | |
| 1837 | |
| 1838 p = db.security.getPermission('Edit', 'category') | |
| 1839 db.security.addPermissionToRole('User', p) | |
| 1840 | |
| 1841 This is all the work that needs to be done for the database. It will store | |
| 1842 categories, and let users view and edit them. Now on to the interface | |
| 1843 stuff. | |
| 1844 | |
| 1845 Changing the web left hand frame | |
| 1846 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
| 1847 | |
| 1848 We need to give the users the ability to create new categories, and the | |
| 1849 place to put the link to this functionality is in the left hand function | |
| 1850 bar, under the "Issues" area. The file that defines how this area looks is | |
| 1851 ``html/page``, which is what we are going to be editing next. | |
| 1852 | |
| 1853 If you look at this file you can see that it contains a lot of "classblock" | |
| 1854 sections which are chunks of HTML that will be included or excluded in the | |
| 1855 output depending on whether the condition in the classblock is met. Under | |
| 1856 the end of the classblock for issue is where we are going to add the | |
| 1857 category code:: | |
| 1858 | |
| 1859 <p class="classblock" | |
| 1860 tal:condition="python:request.user.hasPermission('View', 'category')"> | |
| 1861 <b>Categories</b><br> | |
| 1862 <a tal:condition="python:request.user.hasPermission('Edit', 'category')" | |
| 1863 href="category?:template=item">New Category<br></a> | |
| 1864 </p> | |
| 1865 | |
| 1866 The first two lines is the classblock definition, which sets up a condition | |
| 1867 that only users who have "View" permission to the "category" object will | |
| 1868 have this section included in their output. Next comes a plain "Categories" | |
| 1869 header in bold. Everyone who can view categories will get that. | |
| 1870 | |
| 1871 Next comes the link to the editing area of categories. This link will only | |
| 1872 appear if the condition is matched: that condition being that the user has | |
| 1873 "Edit" permissions for the "category" objects. If they do have permission | |
| 1874 then they will get a link to another page which will let the user add new | |
| 1875 categories. | |
| 1876 | |
| 1877 Note that if you have permission to view but not edit categories then all | |
| 1878 you will see is a "Categories" header with nothing underneath it. This is | |
| 1879 obviously not very good interface design, but will do for now. I just claim | |
| 1880 that it is so I can add more links in this section later on. However to fix | |
| 1881 the problem you could change the condition in the classblock statement, so | |
| 1882 that only users with "Edit" permission would see the "Categories" stuff. | |
| 1883 | |
| 1884 Setting up a page to edit categories | |
| 1885 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
| 1886 | |
| 1887 We defined code in the previous section which let users with the | |
| 1888 appropriate permissions see a link to a page which would let them edit | |
| 1889 conditions. Now we have to write that page. | |
| 1890 | |
| 1891 The link was for the item template for the category object. This translates | |
| 1892 into the system looking for a file called ``category.item`` in the ``html`` | |
| 1893 tracker directory. This is the file that we are going to write now. | |
| 1894 | |
| 1895 First we add an info tag in a comment which doesn't affect the outcome | |
| 1896 of the code at all but is useful for debugging. If you load a page in a | |
| 1897 browser and look at the page source, you can see which sections come | |
| 1898 from which files by looking for these comments:: | |
| 1899 | |
| 1900 <!-- category.item --> | |
| 1901 | |
| 1902 Next we need to add in the METAL macro stuff so we get the normal page | |
| 1903 trappings:: | |
| 1904 | |
| 1905 <tal:block metal:use-macro="templates/page/macros/icing"> | |
| 1906 <title metal:fill-slot="head_title">Category editing</title> | |
| 1907 <td class="page-header-top" metal:fill-slot="body_title"> | |
| 1908 <h2>Category editing</h2> | |
| 1909 </td> | |
| 1910 <td class="content" metal:fill-slot="content"> | |
| 1911 | |
| 1912 Next we need to setup up a standard HTML form, which is the whole | |
| 1913 purpose of this file. We link to some handy javascript which sends the form | |
| 1914 through only once. This is to stop users hitting the send button | |
| 1915 multiple times when they are impatient and thus having the form sent | |
| 1916 multiple times:: | |
| 1917 | |
| 1918 <form method="POST" onSubmit="return submit_once()" | |
| 1919 enctype="multipart/form-data"> | |
| 1920 | |
| 1921 Next we define some code which sets up the minimum list of fields that we | |
| 1922 require the user to enter. There will be only one field, that of "name", so | |
| 1923 they user better put something in it otherwise the whole form is pointless:: | |
| 1924 | |
| 1925 <input type="hidden" name=":required" value="name"> | |
| 1926 | |
| 1927 To get everything to line up properly we will put everything in a table, | |
| 1928 and put a nice big header on it so the user has an idea what is happening:: | |
| 1929 | |
| 1930 <table class="form"> | |
| 1931 <tr><th class="header" colspan=2>Category</th></tr> | |
| 1932 | |
| 1933 Next we need the actual field that the user is going to enter the new | |
| 1934 category. The "context.name.field(size=60)" bit tells roundup to generate a | |
| 1935 normal HTML field of size 60, and the contents of that field will be the | |
| 1936 "name" variable of the current context (which is "category"). The upshot of | |
| 1937 this is that when the user types something in to the form, a new category | |
| 1938 will be created with that name:: | |
| 1939 | |
| 1940 <tr> | |
| 1941 <th nowrap>Name</th> | |
| 1942 <td tal:content="structure python:context.name.field(size=60)">name</td> | |
| 1943 </tr> | |
| 1944 | |
| 1945 Then a submit button so that the user can submit the new category:: | |
| 1946 | |
| 1947 <tr> | |
| 1948 <td> </td> | |
| 1949 <td colspan=3 tal:content="structure context/submit"> | |
| 1950 submit button will go here | |
| 1951 </td> | |
| 1952 </tr> | |
| 1953 | |
| 1954 Finally we finish off the tags we used at the start to do the METAL stuff:: | |
| 1955 | |
| 1956 </td> | |
| 1957 </tal:block> | |
| 1958 | |
| 1959 So putting it all together, and closing the table and form we get:: | |
| 1960 | |
| 1961 <!-- category.item --> | |
| 1962 <tal:block metal:use-macro="templates/page/macros/icing"> | |
| 1963 <title metal:fill-slot="head_title">Category editing</title> | |
| 1964 <td class="page-header-top" metal:fill-slot="body_title"> | |
| 1965 <h2>Category editing</h2> | |
| 1966 </td> | |
| 1967 <td class="content" metal:fill-slot="content"> | |
| 1968 <form method="POST" onSubmit="return submit_once()" | |
| 1969 enctype="multipart/form-data"> | |
| 1970 | |
| 1971 <input type="hidden" name=":required" value="name"> | |
| 1972 | |
| 1973 <table class="form"> | |
| 1974 <tr><th class="header" colspan=2>Category</th></tr> | |
| 1975 | |
| 1976 <tr> | |
| 1977 <th nowrap>Name</th> | |
| 1978 <td tal:content="structure python:context.name.field(size=60)">name</td> | |
| 1979 </tr> | |
| 1980 | |
| 1981 <tr> | |
| 1982 <td> </td> | |
| 1983 <td colspan=3 tal:content="structure context/submit"> | |
| 1984 submit button will go here | |
| 1985 </td> | |
| 1986 </tr> | |
| 1987 </table> | |
| 1988 </form> | |
| 1989 </td> | |
| 1990 </tal:block> | |
| 1991 | |
| 1992 This is quite a lot to just ask the user one simple question, but | |
| 1993 there is a lot of setup for basically one line (the form line) to do | |
| 1994 its work. To add another field to "category" would involve one more line | |
| 1995 (well maybe a few extra to get the formatting correct). | |
| 1996 | |
| 1997 Adding the category to the issue | |
| 1998 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
| 1999 | |
| 2000 We now have the ability to create issues to our hearts content, but | |
| 2001 that is pointless unless we can assign categories to issues. Just like | |
| 2002 the ``html/category.item`` file was used to define how to add a new | |
| 2003 category, the ``html/issue.item`` is used to define how a new issue is | |
| 2004 created. | |
| 2005 | |
| 2006 Just like ``category.issue`` this file defines a form which has a table to lay | |
| 2007 things out. It doesn't matter where in the table we add new stuff, | |
| 2008 it is entirely up to your sense of aesthetics:: | |
| 2009 | |
| 2010 <th nowrap>Category</th> | |
| 2011 <td><span tal:replace="structure context/category/field" /> | |
| 2012 <span tal:replace="structure db/category/classhelp" /> | |
| 2013 </td> | |
| 2014 | |
| 2015 First we define a nice header so that the user knows what the next section | |
| 2016 is, then the middle line does what we are most interested in. This | |
| 2017 ``context/category/field`` gets replaced with a field which contains the | |
| 2018 category in the current context (the current context being the new issue). | |
| 2019 | |
| 2020 The classhelp lines generate a link (labelled "list") to a popup window | |
| 2021 which contains the list of currently known categories. | |
| 2022 | |
| 2023 Searching on categories | |
| 2024 ~~~~~~~~~~~~~~~~~~~~~~~ | |
| 2025 | |
| 2026 We can add categories, and create issues with categories. The next obvious | |
| 2027 thing that we would like to be would be to search issues based on their | |
| 2028 category, so that any one working on the web server could look at all | |
| 2029 issues in the category "Web" for example. | |
| 2030 | |
| 2031 If you look in the html/page file and look for the "Search Issues" you will | |
| 2032 see that it looks something like ``<a href="issue?:template=search">Search | |
| 2033 Issues</a>`` which shows us that when you click on "Search Issues" it will | |
| 2034 be looking for a ``issue.search`` file to display. So that is indeed the file | |
| 2035 that we are going to change. | |
| 2036 | |
| 2037 If you look at this file it should be starting to seem familiar. It is a | |
| 2038 simple HTML form using a table to define structure. You can add the new | |
| 2039 category search code anywhere you like within that form:: | |
| 2040 | |
| 2041 <tr> | |
| 2042 <th>Category:</th> | |
| 2043 <td> | |
| 2044 <select name="category"> | |
| 2045 <option value="">don't care</option> | |
| 2046 <option value="">------------</option> | |
| 2047 <option tal:repeat="s db/category/list" tal:attributes="value s/name" | |
| 2048 tal:content="s/name">category to filter on</option> | |
| 2049 </select> | |
| 2050 </td> | |
| 2051 <td><input type="checkbox" name=":columns" value="category" checked></td> | |
| 2052 <td><input type="radio" name=":sort" value="category"></td> | |
| 2053 <td><input type="radio" name=":group" value="category"></td> | |
| 2054 </tr> | |
| 2055 | |
| 2056 Most of this is straightforward to anyone who knows HTML. It is just | |
| 2057 setting up a select list followed by a checkbox and a couple of radio | |
| 2058 buttons. | |
| 2059 | |
| 2060 The ``tal:repeat`` part repeats the tag for every item in the "category" | |
| 2061 table and setting "s" to be each category in turn. | |
| 2062 | |
| 2063 The ``tal:attributes`` part is setting up the ``value=`` part of the option tag | |
| 2064 to be the name part of "s" which is the current category in the loop. | |
| 2065 | |
| 2066 The ``tal:content`` part is setting the contents of the option tag to be the | |
| 2067 name part of "s" again. For objects more complex than category, obviously | |
| 2068 you would put an id in the value, and the descriptive part in the content; | |
| 2069 but for category they are the same. | |
| 2070 | |
| 2071 Adding category to the default view | |
| 2072 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
| 2073 | |
| 2074 We can now add categories, add issues with categories, and search issues | |
| 2075 based on categories. This is everything that we need to do, however there | |
| 2076 is some more icing that we would like. I think the category of an issue is | |
| 2077 important enough that it should be displayed by default when listing all | |
| 2078 the issues. | |
| 2079 | |
| 2080 Unfortunately, this is a bit less obvious than the previous steps. The code | |
| 2081 defining how the issues look is in ``html/issue.index``. This is a large table | |
| 2082 with a form down the bottom for redisplaying and so forth. | |
| 2083 | |
| 2084 Firstly we need to add an appropriate header to the start of the table:: | |
| 2085 | |
| 2086 <th tal:condition="request/show/category">Category</th> | |
| 2087 | |
| 2088 The condition part of this statement is so that if the user has selected | |
| 2089 not to see the Category column then they won't. | |
| 2090 | |
| 2091 The rest of the table is a loop which will go through every issue that | |
| 2092 matches the display criteria. The loop variable is "i" - which means that | |
| 2093 every issue gets assigned to "i" in turn. | |
| 2094 | |
| 2095 The new part of code to display the category will look like this:: | |
| 2096 | |
| 2097 <td tal:condition="request/show/category" tal:content="i/category"></td> | |
| 2098 | |
| 2099 The condition is the same as above: only display the condition when the | |
| 2100 user hasn't asked for it to be hidden. The next part is to set the content | |
| 2101 of the cell to be the category part of "i" - the current issue. | |
| 2102 | |
| 2103 Finally we have to edit ``html/page`` again. This time to tell it that when the | |
| 2104 user clicks on "Unnasigned Issues" or "All Issues" that the category should | |
| 2105 be displayed. If you scroll down the page file, you can see the links with | |
| 2106 lots of options. The option that we are interested in is the ``:columns=`` one | |
| 2107 which tells roundup which fields of the issue to display. Simply add | |
| 2108 "category" to that list and it all should work. | |
| 2109 | |
| 2110 | |
| 2111 Adding in state transition control | |
| 2112 ---------------------------------- | |
| 2113 | |
| 2114 Sometimes tracker admins want to control the states that users may move issues | |
| 2115 to. | |
| 2116 | |
| 2117 1. add a Multilink property to the status class:: | |
| 2118 | |
| 2119 stat = Class(db, "status", ... , transitions=Multilink('status'), ...) | |
| 2120 | |
| 2121 and then edit the statuses already created either: | |
| 2122 | |
| 2123 a. through the web using the class list -> status class editor, or | |
| 2124 b. using the roundup-admin "set" command. | |
| 2125 | |
| 2126 2. add an auditor module ``checktransition.py`` in your tracker's | |
| 2127 ``detectors`` directory:: | |
| 2128 | |
| 2129 def checktransition(db, cl, nodeid, newvalues): | |
| 2130 ''' Check that the desired transition is valid for the "status" | |
| 2131 property. | |
| 2132 ''' | |
| 2133 if not newvalues.has_key('status'): | |
| 2134 return | |
| 2135 current = cl.get(nodeid, 'status') | |
| 2136 new = newvalues['status'] | |
| 2137 if new == current: | |
| 2138 return | |
| 2139 ok = db.status.get(current, 'transitions') | |
| 2140 if new not in ok: | |
| 2141 raise ValueError, 'Status not allowed to move from "%s" to "%s"'%( | |
| 2142 db.status.get(current, 'name'), db.status.get(new, 'name')) | |
| 2143 | |
| 2144 def init(db): | |
| 2145 db.issue.audit('set', checktransition) | |
| 2146 | |
| 2147 3. in the ``issue.item`` template, change the status editing bit from:: | |
| 2148 | |
| 2149 <th nowrap>Status</th> | |
| 2150 <td tal:content="structure context/status/menu">status</td> | |
| 2151 | |
| 2152 to:: | |
| 2153 | |
| 2154 <th nowrap>Status</th> | |
| 2155 <td> | |
| 2156 <select tal:condition="context/id" name="status"> | |
| 2157 <tal:block tal:define="ok context/status/transitions" | |
| 2158 tal:repeat="state db/status/list"> | |
| 2159 <option tal:condition="python:state.id in ok" | |
| 2160 tal:attributes="value state/id; | |
| 2161 selected python:state.id == context.status.id" | |
| 2162 tal:content="state/name"></option> | |
| 2163 </tal:block> | |
| 2164 </select> | |
| 2165 <tal:block tal:condition="not:context/id" | |
| 2166 tal:replace="structure context/status/menu" /> | |
| 2167 </td> | |
| 2168 | |
| 2169 which displays only the allowed status to transition to. | |
| 2170 | |
| 2171 | |
| 2172 Displaying only message summaries in the issue display | |
| 2173 ------------------------------------------------------ | |
| 2174 | |
| 2175 Alter the issue.item template section for messages to:: | |
| 2176 | |
| 2177 <table class="messages" tal:condition="context/messages"> | |
| 2178 <tr><th colspan=5 class="header">Messages</th></tr> | |
| 2179 <tr tal:repeat="msg context/messages"> | |
| 2180 <td><a tal:attributes="href string:msg${msg/id}" | |
| 2181 tal:content="string:msg${msg/id}"></a></td> | |
| 2182 <td tal:content="msg/author">author</td> | |
| 2183 <td nowrap tal:content="msg/date/pretty">date</td> | |
| 2184 <td tal:content="msg/summary">summary</td> | |
| 2185 <td> | |
| 2186 <a tal:attributes="href string:?:remove:messages=${msg/id}&:action=edit">remove</a> | |
| 2187 </td> | |
| 2188 </tr> | |
| 2189 </table> | |
| 2190 | |
| 2191 Restricting the list of users that are assignable to a task | |
| 2192 ----------------------------------------------------------- | |
| 2193 | |
| 2194 1. In your tracker's "dbinit.py", create a new Role, say "Developer":: | |
| 2195 | |
| 2196 db.security.addRole(name='Developer', description='A developer') | |
| 2197 | |
| 2198 2. Just after that, create a new Permission, say "Fixer", specific to "issue":: | |
| 2199 | |
| 2200 p = db.security.addPermission(name='Fixer', klass='issue', | |
| 2201 description='User is allowed to be assigned to fix issues') | |
| 2202 | |
| 2203 3. Then assign the new Permission to your "Developer" Role:: | |
| 2204 | |
| 2205 db.security.addPermissionToRole('Developer', p) | |
| 2206 | |
| 2207 4. In the issue item edit page ("html/issue.item" in your tracker dir), use | |
| 2208 the new Permission in restricting the "assignedto" list:: | |
| 2209 | |
| 2210 <select name="assignedto"> | |
| 2211 <option value="-1">- no selection -</option> | |
| 2212 <tal:block tal:repeat="user db/user/list"> | |
| 2213 <option tal:condition="python:user.hasPermission('Fixer', context.classname)" | |
| 2214 tal:attributes="value user/id; | |
| 2215 selected python:user.id == context.assignedto" | |
| 2216 tal:content="user/realname"></option> | |
| 2217 </tal:block> | |
| 2218 </select> | |
| 2219 | |
| 2220 For extra security, you may wish to set up an auditor to enforce the | |
| 2221 Permission requirement (install this as "assignedtoFixer.py" in your tracker | |
| 2222 "detectors" directory):: | |
| 2223 | |
| 2224 def assignedtoMustBeFixer(db, cl, nodeid, newvalues): | |
| 2225 ''' Ensure the assignedto value in newvalues is a used with the Fixer | |
| 2226 Permission | |
| 2227 ''' | |
| 2228 if not newvalues.has_key('assignedto'): | |
| 2229 # don't care | |
| 2230 return | |
| 2231 | |
| 2232 # get the userid | |
| 2233 userid = newvalues['assignedto'] | |
| 2234 if not db.security.hasPermission('Fixer', userid, cl.classname): | |
| 2235 raise ValueError, 'You do not have permission to edit %s'%cl.classname | |
| 2236 | |
| 2237 def init(db): | |
| 2238 db.issue.audit('set', assignedtoMustBeFixer) | |
| 2239 db.issue.audit('create', assignedtoMustBeFixer) | |
| 2240 | |
| 2241 So now, if the edit attempts to set the assignedto to a user that doesn't have | |
| 2242 the "Fixer" Permission, the error will be raised. | |
| 2243 | |
| 2244 | |
| 2245 Setting up a "wizard" (or "druid") for controlled adding of issues | |
| 2246 ------------------------------------------------------------------ | |
| 2247 | |
| 2248 1. Set up the page templates you wish to use for data input. My wizard | |
| 2249 is going to be a two-step process, first figuring out what category of | |
| 2250 issue the user is submitting, and then getting details specific to that | |
| 2251 category. The first page includes a table of help, explaining what the | |
| 2252 category names mean, and then the core of the form:: | |
| 2253 | |
| 2254 <form method="POST" onSubmit="return submit_once()" | |
| 2255 enctype="multipart/form-data"> | |
| 2256 <input type="hidden" name=":template" value="add_page1"> | |
| 2257 <input type="hidden" name=":action" value="page1submit"> | |
| 2258 | |
| 2259 <strong>Category:</strong> | |
| 2260 <tal:block tal:replace="structure context/category/menu" /> | |
| 2261 <input type="submit" value="Continue"> | |
| 2262 </form> | |
| 2263 | |
| 2264 The next page has the usual issue entry information, with the addition of | |
| 2265 the following form fragments:: | |
| 2266 | |
| 2267 <form method="POST" onSubmit="return submit_once()" | |
| 2268 enctype="multipart/form-data" tal:condition="context/is_edit_ok" | |
| 2269 tal:define="cat request/form/category/value"> | |
| 2270 | |
| 2271 <input type="hidden" name=":template" value="add_page2"> | |
| 2272 <input type="hidden" name=":required" value="title"> | |
| 2273 <input type="hidden" name="category" tal:attributes="value cat"> | |
| 2274 | |
| 2275 . | |
| 2276 . | |
| 2277 . | |
| 2278 </form> | |
| 2279 | |
| 2280 Note that later in the form, I test the value of "cat" include form | |
| 2281 elements that are appropriate. For example:: | |
| 2282 | |
| 2283 <tal:block tal:condition="python:cat in '6 10 13 14 15 16 17'.split()"> | |
| 2284 <tr> | |
| 2285 <th nowrap>Operating System</th> | |
| 2286 <td tal:content="structure context/os/field"></td> | |
| 2287 </tr> | |
| 2288 <tr> | |
| 2289 <th nowrap>Web Browser</th> | |
| 2290 <td tal:content="structure context/browser/field"></td> | |
| 2291 </tr> | |
| 2292 </tal:block> | |
| 2293 | |
| 2294 ... the above section will only be displayed if the category is one of 6, | |
| 2295 10, 13, 14, 15, 16 or 17. | |
| 2296 | |
| 2297 3. Determine what actions need to be taken between the pages - these are | |
| 2298 usually to validate user choices and determine what page is next. Now | |
| 2299 encode those actions in methods on the interfaces Client class and insert | |
| 2300 hooks to those actions in the "actions" attribute on that class, like so:: | |
| 2301 | |
| 2302 actions = client.Client.actions + ( | |
| 2303 ('page1_submit', 'page1SubmitAction'), | |
| 2304 ) | |
| 2305 | |
| 2306 def page1SubmitAction(self): | |
| 2307 ''' Verify that the user has selected a category, and then move on | |
| 2308 to page 2. | |
| 2309 ''' | |
| 2310 category = self.form['category'].value | |
| 2311 if category == '-1': | |
| 2312 self.error_message.append('You must select a category of report') | |
| 2313 return | |
| 2314 # everything's ok, move on to the next page | |
| 2315 self.template = 'add_page2' | |
| 2316 | |
| 2317 4. Use the usual "new" action as the :action on the final page, and you're | |
| 2318 done (the standard context/submit method can do this for you). | |
| 2319 | |
| 2320 | |
| 2321 Using an external password validation source | |
| 2322 -------------------------------------------- | |
| 2323 | |
| 2324 We have a centrally-managed password changing system for our users. This | |
| 2325 results in a UN*X passwd-style file that we use for verification of users. | |
| 2326 Entries in the file consist of ``name:password`` where the password is | |
| 2327 encrypted using the standard UN*X ``crypt()`` function (see the ``crypt`` | |
| 2328 module in your Python distribution). An example entry would be:: | |
| 2329 | |
| 2330 admin:aamrgyQfDFSHw | |
| 2331 | |
| 2332 Each user of Roundup must still have their information stored in the Roundup | |
| 2333 database - we just use the passwd file to check their password. To do this, we | |
| 2334 add the following code to our ``Client`` class in the tracker home | |
| 2335 ``interfaces.py`` module:: | |
| 2336 | |
| 2337 def verifyPassword(self, userid, password): | |
| 2338 # get the user's username | |
| 2339 username = self.db.user.get(userid, 'username') | |
| 2340 | |
| 2341 # the passwords are stored in the "passwd.txt" file in the tracker | |
| 2342 # home | |
| 2343 file = os.path.join(self.db.config.TRACKER_HOME, 'passwd.txt') | |
| 2344 | |
| 2345 # see if we can find a match | |
| 2346 for ent in [line.strip().split(':') for line in open(file).readlines()]: | |
| 2347 if ent[0] == username: | |
| 2348 return crypt.crypt(password, ent[1][:2]) == ent[1] | |
| 2349 | |
| 2350 # user doesn't exist in the file | |
| 2351 return 0 | |
| 2352 | |
| 2353 What this does is look through the file, line by line, looking for a name that | |
| 2354 matches. | |
| 2355 | |
| 2356 We also remove the redundant password fields from the ``user.item`` template. | |
| 2357 | |
| 2358 | |
| 2359 Adding a "vacation" flag to users for stopping nosy messages | |
| 2360 ------------------------------------------------------------ | |
| 2361 | |
| 2362 When users go on vacation and set up vacation email bouncing, you'll start to | |
| 2363 see a lot of messages come back through Roundup "Fred is on vacation". Not | |
| 2364 very useful, and relatively easy to stop. | |
| 2365 | |
| 2366 1. add a "vacation" flag to your users:: | |
| 2367 | |
| 2368 user = Class(db, "user", | |
| 2369 username=String(), password=Password(), | |
| 2370 address=String(), realname=String(), | |
| 2371 phone=String(), organisation=String(), | |
| 2372 alternate_addresses=String(), | |
| 2373 roles=String(), queries=Multilink("query"), | |
| 2374 vacation=Boolean()) | |
| 2375 | |
| 2376 2. edit your detector ``nosyreactor.py`` so that the ``nosyreaction()`` | |
| 2377 consists of:: | |
| 2378 | |
| 2379 def nosyreaction(db, cl, nodeid, oldvalues): | |
| 2380 # send a copy of all new messages to the nosy list | |
| 2381 for msgid in determineNewMessages(cl, nodeid, oldvalues): | |
| 2382 try: | |
| 2383 users = db.user | |
| 2384 messages = db.msg | |
| 2385 | |
| 2386 # figure the recipient ids | |
| 2387 sendto = [] | |
| 2388 r = {} | |
| 2389 recipients = messages.get(msgid, 'recipients') | |
| 2390 for recipid in messages.get(msgid, 'recipients'): | |
| 2391 r[recipid] = 1 | |
| 2392 | |
| 2393 # figure the author's id, and indicate they've received the | |
| 2394 # message | |
| 2395 authid = messages.get(msgid, 'author') | |
| 2396 | |
| 2397 # possibly send the message to the author, as long as they aren't | |
| 2398 # anonymous | |
| 2399 if (db.config.MESSAGES_TO_AUTHOR == 'yes' and | |
| 2400 users.get(authid, 'username') != 'anonymous'): | |
| 2401 sendto.append(authid) | |
| 2402 r[authid] = 1 | |
| 2403 | |
| 2404 # now figure the nosy people who weren't recipients | |
| 2405 nosy = cl.get(nodeid, 'nosy') | |
| 2406 for nosyid in nosy: | |
| 2407 # Don't send nosy mail to the anonymous user (that user | |
| 2408 # shouldn't appear in the nosy list, but just in case they | |
| 2409 # do...) | |
| 2410 if users.get(nosyid, 'username') == 'anonymous': | |
| 2411 continue | |
| 2412 # make sure they haven't seen the message already | |
| 2413 if not r.has_key(nosyid): | |
| 2414 # send it to them | |
| 2415 sendto.append(nosyid) | |
| 2416 recipients.append(nosyid) | |
| 2417 | |
| 2418 # generate a change note | |
| 2419 if oldvalues: | |
| 2420 note = cl.generateChangeNote(nodeid, oldvalues) | |
| 2421 else: | |
| 2422 note = cl.generateCreateNote(nodeid) | |
| 2423 | |
| 2424 # we have new recipients | |
| 2425 if sendto: | |
| 2426 # filter out the people on vacation | |
| 2427 sendto = [i for i in sendto if not users.get(i, 'vacation', 0)] | |
| 2428 | |
| 2429 # map userids to addresses | |
| 2430 sendto = [users.get(i, 'address') for i in sendto] | |
| 2431 | |
| 2432 # update the message's recipients list | |
| 2433 messages.set(msgid, recipients=recipients) | |
| 2434 | |
| 2435 # send the message | |
| 2436 cl.send_message(nodeid, msgid, note, sendto) | |
| 2437 except roundupdb.MessageSendError, message: | |
| 2438 raise roundupdb.DetectorError, message | |
| 2439 | |
| 2440 Note that this is the standard nosy reaction code, with the small addition | |
| 2441 of:: | |
| 2442 | |
| 2443 # filter out the people on vacation | |
| 2444 sendto = [i for i in sendto if not users.get(i, 'vacation', 0)] | |
| 2445 | |
| 2446 which filters out the users that have the vacation flag set to true. | |
| 2447 | |
| 2448 | |
| 2449 Adding a time log to your issues | |
| 2450 -------------------------------- | |
| 2451 | |
| 2452 We want to log the dates and amount of time spent working on issues, and be | |
| 2453 able to give a summary of the total time spent on a particular issue. | |
| 2454 | |
| 2455 1. Add a new class to your tracker ``dbinit.py``:: | |
| 2456 | |
| 2457 # storage for time logging | |
| 2458 timelog = Class(db, "timelog", period=Interval()) | |
| 2459 | |
| 2460 Note that we automatically get the date of the time log entry creation | |
| 2461 through the standard property "creation". | |
| 2462 | |
| 2463 2. Link to the new class from your issue class (again, in ``dbinit.py``):: | |
| 2464 | |
| 2465 issue = IssueClass(db, "issue", | |
| 2466 assignedto=Link("user"), topic=Multilink("keyword"), | |
| 2467 priority=Link("priority"), status=Link("status"), | |
| 2468 times=Multilink("timelog")) | |
| 2469 | |
| 2470 the "times" property is the new link to the "timelog" class. | |
| 2471 | |
| 2472 3. We'll need to let people add in times to the issue, so in the web interface | |
| 2473 we'll have a new entry field, just below the change note box:: | |
| 2474 | |
| 2475 <tr> | |
| 2476 <th nowrap>Time Log</th> | |
| 2477 <td colspan=3><input name=":timelog"> | |
| 2478 (enter as "3y 1m 4d 2:40:02" or parts thereof) | |
| 2479 </td> | |
| 2480 </tr> | |
| 2481 | |
| 2482 Note that we've made up a new form variable, but since we place a colon ":" | |
| 2483 in front of it, it won't clash with any existing property variables. The | |
| 2484 names you *can't* use are ``:note``, ``:file``, ``:action``, ``:required`` | |
| 2485 and ``:template``. These variables are described in the section | |
| 2486 `performing actions in web requests`_. | |
| 2487 | |
| 2488 4. We also need to handle this new field in the CGI interface - the way to | |
| 2489 do this is through implementing a new form action (see `Setting up a | |
| 2490 "wizard" (or "druid") for controlled adding of issues`_ for another example | |
| 2491 where we implemented a new CGI form action). | |
| 2492 | |
| 2493 In this case, we'll want our action to: | |
| 2494 | |
| 2495 1. create a new "timelog" entry, | |
| 2496 2. fake that the issue's "times" property has been edited, and then | |
| 2497 3. call the normal CGI edit action handler. | |
| 2498 | |
| 2499 The code to do this is:: | |
| 2500 | |
| 2501 actions = client.Client.actions + ( | |
| 2502 ('edit_with_timelog', 'timelogEditAction'), | |
| 2503 ) | |
| 2504 | |
| 2505 def timelogEditAction(self): | |
| 2506 ''' Handle the creation of a new time log entry if necessary. | |
| 2507 | |
| 2508 If we create a new entry, fake up a CGI form value for the altered | |
| 2509 "times" property of the issue being edited. | |
| 2510 | |
| 2511 Punt to the regular edit action when we're done. | |
| 2512 ''' | |
| 2513 # if there's a timelog value specified, create an entry | |
| 2514 if self.form.has_key(':timelog') and \ | |
| 2515 self.form[':timelog'].value.strip(): | |
| 2516 period = Interval(self.form[':timelog'].value) | |
| 2517 # create it | |
| 2518 newid = self.db.timelog.create(period=period) | |
| 2519 | |
| 2520 # if we're editing an existing item, get the old timelog value | |
| 2521 if self.nodeid: | |
| 2522 l = self.db.issue.get(self.nodeid, 'times') | |
| 2523 l.append(newid) | |
| 2524 else: | |
| 2525 l = [newid] | |
| 2526 | |
| 2527 # now make the fake CGI form values | |
| 2528 for entry in l: | |
| 2529 self.form.list.append(MiniFieldStorage('times', entry)) | |
| 2530 | |
| 2531 # punt to the normal edit action | |
| 2532 return self.editItemAction() | |
| 2533 | |
| 2534 you add this code to your Client class in your tracker's ``interfaces.py`` | |
| 2535 file. | |
| 2536 | |
| 2537 5. You'll also need to modify your ``issue.item`` form submit action so it | |
| 2538 calls the time logging action we just created:: | |
| 2539 | |
| 2540 <tr> | |
| 2541 <td> </td> | |
| 2542 <td colspan=3> | |
| 2543 <tal:block tal:condition="context/id"> | |
| 2544 <input type="hidden" name=":action" value="edit_with_timelog"> | |
| 2545 <input type="submit" name="submit" value="Submit Changes"> | |
| 2546 </tal:block> | |
| 2547 <tal:block tal:condition="not:context/id"> | |
| 2548 <input type="hidden" name=":action" value="new"> | |
| 2549 <input type="submit" name="submit" value="Submit New Issue"> | |
| 2550 </tal:block> | |
| 2551 </td> | |
| 2552 </tr> | |
| 2553 | |
| 2554 Note that the "context/submit" command usually handles all that for you - | |
| 2555 isn't it handy? The important change is setting the action to | |
| 2556 "edit_with_timelog" for edit operations (where the item exists) | |
| 2557 | |
| 2558 6. We want to display a total of the time log times that have been accumulated | |
| 2559 for an issue. To do this, we'll need to actually write some Python code, | |
| 2560 since it's beyond the scope of PageTemplates to perform such calculations. | |
| 2561 We do this by adding a method to the TemplatingUtils class in our tracker | |
| 2562 ``interfaces.py`` module:: | |
| 2563 | |
| 2564 | |
| 2565 class TemplatingUtils: | |
| 2566 ''' Methods implemented on this class will be available to HTML | |
| 2567 templates through the 'utils' variable. | |
| 2568 ''' | |
| 2569 def totalTimeSpent(self, times): | |
| 2570 ''' Call me with a list of timelog items (which have an Interval | |
| 2571 "period" property) | |
| 2572 ''' | |
| 2573 total = Interval('') | |
| 2574 for time in times: | |
| 2575 total += time.period._value | |
| 2576 return total | |
| 2577 | |
| 2578 As indicated in the docstrings, we will be able to access the | |
| 2579 ``totalTimeSpent`` method via the ``utils`` variable in our templates. See | |
| 2580 | |
| 2581 7. Display the time log for an issue:: | |
| 2582 | |
| 2583 <table class="otherinfo" tal:condition="context/times"> | |
| 2584 <tr><th colspan="3" class="header">Time Log | |
| 2585 <tal:block tal:replace="python:utils.totalTimeSpent(context.times)" /> | |
| 2586 </th></tr> | |
| 2587 <tr><th>Date</th><th>Period</th><th>Logged By</th></tr> | |
| 2588 <tr tal:repeat="time context/times"> | |
| 2589 <td tal:content="time/creation"></td> | |
| 2590 <td tal:content="time/period"></td> | |
| 2591 <td tal:content="time/creator"></td> | |
| 2592 </tr> | |
| 2593 </table> | |
| 2594 | |
| 2595 I put this just above the Messages log in my issue display. Note our use | |
| 2596 of the ``totalTimeSpent`` method which will total up the times for the | |
| 2597 issue and return a new Interval. That will be automatically displayed in | |
| 2598 the template as text like "+ 1y 2:40" (1 year, 2 hours and 40 minutes). | |
| 2599 | |
| 2600 8. If you're using a persistent web server - roundup-server or mod_python for | |
| 2601 example - then you'll need to restart that to pick up the code changes. | |
| 2602 When that's done, you'll be able to use the new time logging interface. | |
| 2603 | |
| 2604 Using a UN*X passwd file as the user database | |
| 2605 --------------------------------------------- | |
| 2606 | |
| 2607 On some systems the primary store of users is the UN*X passwd file. It holds | |
| 2608 information on users such as their username, real name, password and primary | |
| 2609 user group. | |
| 2610 | |
| 2611 Roundup can use this store as its primary source of user information, but it | |
| 2612 needs additional information too - email address(es), roundup Roles, vacation | |
| 2613 flags, roundup hyperdb item ids, etc. Also, "retired" users must still exist | |
| 2614 in the user database, unlike some passwd files in which the users are removed | |
| 2615 when they no longer have access to a system. | |
| 2616 | |
| 2617 To make use of the passwd file, we therefore synchronise between the two user | |
| 2618 stores. We also use the passwd file to validate the user logins, as described | |
| 2619 in the previous example, `using an external password validation source`_. We | |
| 2620 keep the users lists in sync using a fairly simple script that runs once a | |
| 2621 day, or several times an hour if more immediate access is needed. In short, it: | |
| 2622 | |
| 2623 1. parses the passwd file, finding usernames, passwords and real names, | |
| 2624 2. compares that list to the current roundup user list: | |
| 2625 | |
| 2626 a. entries no longer in the passwd file are *retired* | |
| 2627 b. entries with mismatching real names are *updated* | |
| 2628 c. entries only exist in the passwd file are *created* | |
| 2629 | |
| 2630 3. send an email to administrators to let them know what's been done. | |
| 2631 | |
| 2632 The retiring and updating are simple operations, requiring only a call to | |
| 2633 ``retire()`` or ``set()``. The creation operation requires more information | |
| 2634 though - the user's email address and their roundup Roles. We're going to | |
| 2635 assume that the user's email address is the same as their login name, so we | |
| 2636 just append the domain name to that. The Roles are determined using the | |
| 2637 passwd group identifier - mapping their UN*X group to an appropriate set of | |
| 2638 Roles. | |
| 2639 | |
| 2640 The script to perform all this, broken up into its main components, is as | |
| 2641 follows. Firstly, we import the necessary modules and open the tracker we're | |
| 2642 to work on:: | |
| 2643 | |
| 2644 import sys, os, smtplib | |
| 2645 from roundup import instance, date | |
| 2646 | |
| 2647 # open the tracker | |
| 2648 tracker_home = sys.argv[1] | |
| 2649 tracker = instance.open(tracker_home) | |
| 2650 | |
| 2651 Next we read in the *passwd* file from the tracker home:: | |
| 2652 | |
| 2653 # read in the users | |
| 2654 file = os.path.join(tracker_home, 'users.passwd') | |
| 2655 users = [x.strip().split(':') for x in open(file).readlines()] | |
| 2656 | |
| 2657 Handle special users (those to ignore in the file, and those who don't appear | |
| 2658 in the file):: | |
| 2659 | |
| 2660 # users to not keep ever, pre-load with the users I know aren't | |
| 2661 # "real" users | |
| 2662 ignore = ['ekmmon', 'bfast', 'csrmail'] | |
| 2663 | |
| 2664 # users to keep - pre-load with the roundup-specific users | |
| 2665 keep = ['comment_pool', 'network_pool', 'admin', 'dev-team', 'cs_pool', | |
| 2666 'anonymous', 'system_pool', 'automated'] | |
| 2667 | |
| 2668 Now we map the UN*X group numbers to the Roles that users should have:: | |
| 2669 | |
| 2670 roles = { | |
| 2671 '501': 'User,Tech', # tech | |
| 2672 '502': 'User', # finance | |
| 2673 '503': 'User,CSR', # customer service reps | |
| 2674 '504': 'User', # sales | |
| 2675 '505': 'User', # marketing | |
| 2676 } | |
| 2677 | |
| 2678 Now we do all the work. Note that the body of the script (where we have the | |
| 2679 tracker database open) is wrapped in a ``try`` / ``finally`` clause, so that | |
| 2680 we always close the database cleanly when we're finished. So, we now do all | |
| 2681 the work:: | |
| 2682 | |
| 2683 # open the database | |
| 2684 db = tracker.open('admin') | |
| 2685 try: | |
| 2686 # store away messages to send to the tracker admins | |
| 2687 msg = [] | |
| 2688 | |
| 2689 # loop over the users list read in from the passwd file | |
| 2690 for user,passw,uid,gid,real,home,shell in users: | |
| 2691 if user in ignore: | |
| 2692 # this user shouldn't appear in our tracker | |
| 2693 continue | |
| 2694 keep.append(user) | |
| 2695 try: | |
| 2696 # see if the user exists in the tracker | |
| 2697 uid = db.user.lookup(user) | |
| 2698 | |
| 2699 # yes, they do - now check the real name for correctness | |
| 2700 if real != db.user.get(uid, 'realname'): | |
| 2701 db.user.set(uid, realname=real) | |
| 2702 msg.append('FIX %s - %s'%(user, real)) | |
| 2703 except KeyError: | |
| 2704 # nope, the user doesn't exist | |
| 2705 db.user.create(username=user, realname=real, | |
| 2706 address='%s@ekit-inc.com'%user, roles=roles[gid]) | |
| 2707 msg.append('ADD %s - %s (%s)'%(user, real, roles[gid])) | |
| 2708 | |
| 2709 # now check that all the users in the tracker are also in our "keep" | |
| 2710 # list - retire those who aren't | |
| 2711 for uid in db.user.list(): | |
| 2712 user = db.user.get(uid, 'username') | |
| 2713 if user not in keep: | |
| 2714 db.user.retire(uid) | |
| 2715 msg.append('RET %s'%user) | |
| 2716 | |
| 2717 # if we did work, then send email to the tracker admins | |
| 2718 if msg: | |
| 2719 # create the email | |
| 2720 msg = '''Subject: %s user database maintenance | |
| 2721 | |
| 2722 %s | |
| 2723 '''%(db.config.TRACKER_NAME, '\n'.join(msg)) | |
| 2724 | |
| 2725 # send the email | |
| 2726 smtp = smtplib.SMTP(db.config.MAILHOST) | |
| 2727 addr = db.config.ADMIN_EMAIL | |
| 2728 smtp.sendmail(addr, addr, msg) | |
| 2729 | |
| 2730 # now we're done - commit the changes | |
| 2731 db.commit() | |
| 2732 finally: | |
| 2733 # always close the database cleanly | |
| 2734 db.close() | |
| 2735 | |
| 2736 And that's it! | |
| 2737 | |
| 2738 | |
| 2739 Enabling display of either message summaries or the entire messages | |
| 2740 ------------------------------------------------------------------- | |
| 2741 | |
| 2742 This is pretty simple - all we need to do is copy the code from the example | |
| 2743 `displaying only message summaries in the issue display`_ into our template | |
| 2744 alongside the summary display, and then introduce a switch that shows either | |
| 2745 one or the other. We'll use a new form variable, ``:whole_messages`` to | |
| 2746 achieve this:: | |
| 2747 | |
| 2748 <table class="messages" tal:condition="context/messages"> | |
| 2749 <tal:block tal:condition="not:request/form/:whole_messages/value | python:0"> | |
| 2750 <tr><th colspan=3 class="header">Messages</th> | |
| 2751 <th colspan=2 class="header"> | |
| 2752 <a href="?:whole_messages=yes">show entire messages</a> | |
| 2753 </th> | |
| 2754 </tr> | |
| 2755 <tr tal:repeat="msg context/messages"> | |
| 2756 <td><a tal:attributes="href string:msg${msg/id}" | |
| 2757 tal:content="string:msg${msg/id}"></a></td> | |
| 2758 <td tal:content="msg/author">author</td> | |
| 2759 <td nowrap tal:content="msg/date/pretty">date</td> | |
| 2760 <td tal:content="msg/summary">summary</td> | |
| 2761 <td> | |
| 2762 <a tal:attributes="href string:?:remove:messages=${msg/id}&:action=edit">remove</a> | |
| 2763 </td> | |
| 2764 </tr> | |
| 2765 </tal:block> | |
| 2766 | |
| 2767 <tal:block tal:condition="request/form/:whole_messages/value | python:0"> | |
| 2768 <tr><th colspan=2 class="header">Messages</th> | |
| 2769 <th class="header"><a href="?:whole_messages=">show only summaries</a></th> | |
| 2770 </tr> | |
| 2771 <tal:block tal:repeat="msg context/messages"> | |
| 2772 <tr> | |
| 2773 <th tal:content="msg/author">author</th> | |
| 2774 <th nowrap tal:content="msg/date/pretty">date</th> | |
| 2775 <th style="text-align: right"> | |
| 2776 (<a tal:attributes="href string:?:remove:messages=${msg/id}&:action=edit">remove</a>) | |
| 2777 </th> | |
| 2778 </tr> | |
| 2779 <tr><td colspan=3 tal:content="msg/content"></td></tr> | |
| 2780 </tal:block> | |
| 2781 </tal:block> | |
| 2782 </table> | |
| 2783 | |
| 2784 | |
| 2785 ------------------- | |
| 2786 | |
| 2787 Back to `Table of Contents`_ | |
| 2788 | |
| 2789 .. _`Table of Contents`: index.html | |
| 2790 .. _`design documentation`: design.html | |
| 2791 |
