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