comparison doc/customizing.txt @ 1244:8dd4f736370b

merge from maintenance branch
author Richard Jones <richard@users.sourceforge.net>
date Thu, 03 Oct 2002 06:56:30 +0000
parents dd52bf10f934
children d8c98af869ff
comparison
equal deleted inserted replaced
1243:3a028d2f7830 1244:8dd4f736370b
1 =================== 1 ===================
2 Customising Roundup 2 Customising Roundup
3 =================== 3 ===================
4 4
5 :Version: $Revision: 1.50 $ 5 :Version: $Revision: 1.51 $
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::
11 :depth: 1 11 :depth: 1
12 12
13 What You Can Do 13 What You Can Do
14 =============== 14 ===============
15
16 Before you get too far, it's probably worth having a quick read of the Roundup
17 `design documentation`_.
15 18
16 Customisation of Roundup can take one of five forms: 19 Customisation of Roundup can take one of five forms:
17 20
18 1. `tracker configuration`_ file changes 21 1. `tracker configuration`_ file changes
19 2. database, or `tracker schema`_ changes 22 2. database, or `tracker schema`_ changes
20 3. "definition" class `database content`_ changes 23 3. "definition" class `database content`_ changes
21 4. behavioural changes, through detectors_ 24 4. behavioural changes, through detectors_
22 5. `access controls`_ 25 5. `access controls`_
26 6. change the `web interface`_
23 27
24 The third case is special because it takes two distinctly different forms 28 The third case is special because it takes two distinctly different forms
25 depending upon whether the tracker has been initialised or not. The other two 29 depending upon whether the tracker has been initialised or not. The other two
26 may be done at any time, before or after tracker initialisation. Yes, this 30 may be done at any time, before or after tracker initialisation. Yes, this
27 includes adding or removing properties from classes. 31 includes adding or removing properties from classes.
188 Schemas are defined using Python code in the ``dbinit.py`` module of your 192 Schemas are defined using Python code in the ``dbinit.py`` module of your
189 tracker. The "classic" schema looks like this:: 193 tracker. The "classic" schema looks like this::
190 194
191 pri = Class(db, "priority", name=String(), order=String()) 195 pri = Class(db, "priority", name=String(), order=String())
192 pri.setkey("name") 196 pri.setkey("name")
193 pri.create(name="critical", order="1")
194 pri.create(name="urgent", order="2")
195 pri.create(name="bug", order="3")
196 pri.create(name="feature", order="4")
197 pri.create(name="wish", order="5")
198 197
199 stat = Class(db, "status", name=String(), order=String()) 198 stat = Class(db, "status", name=String(), order=String())
200 stat.setkey("name") 199 stat.setkey("name")
201 stat.create(name="unread", order="1")
202 stat.create(name="deferred", order="2")
203 stat.create(name="chatting", order="3")
204 stat.create(name="need-eg", order="4")
205 stat.create(name="in-progress", order="5")
206 stat.create(name="testing", order="6")
207 stat.create(name="done-cbb", order="7")
208 stat.create(name="resolved", order="8")
209 200
210 keyword = Class(db, "keyword", name=String()) 201 keyword = Class(db, "keyword", name=String())
211 keyword.setkey("name") 202 keyword.setkey("name")
212 203
213 user = Class(db, "user", username=String(), password=String(), 204 user = Class(db, "user", username=String(), organisation=String(),
214 address=String(), realname=String(), phone=String(), 205 password=String(), address=String(), realname=String(), phone=String())
215 organisation=String())
216 user.setkey("username") 206 user.setkey("username")
217 user.create(username="admin", password=adminpw, 207
218 address=config.ADMIN_EMAIL) 208 msg = FileClass(db, "msg", author=Link("user"), summary=String(),
219 209 date=Date(), recipients=Multilink("user"), files=Multilink("file"))
220 msg = FileClass(db, "msg", author=Link("user"), recipients=Multilink
221 ("user"), date=Date(), summary=String(), files=Multilink("file"))
222 210
223 file = FileClass(db, "file", name=String(), type=String()) 211 file = FileClass(db, "file", name=String(), type=String())
224 212
225 issue = IssueClass(db, "issue", assignedto=Link("user"), 213 issue = IssueClass(db, "issue", topic=Multilink("keyword"),
226 topic=Multilink("keyword"), priority=Link("priority"), status=Link 214 status=Link("status"), assignedto=Link("user"),
227 ("status")) 215 priority=Link("priority"))
228 issue.setkey('title') 216 issue.setkey('title')
229
230 XXX security definitions
231 217
232 Classes and Properties - creating a new information store 218 Classes and Properties - creating a new information store
233 --------------------------------------------------------- 219 ---------------------------------------------------------
234 220
235 In the tracker above, we've defined 7 classes of information: 221 In the tracker above, we've defined 7 classes of information:
430 alter or remove items from the classes in question. 416 alter or remove items from the classes in question.
431 417
432 418
433 See "`adding a new field to the classic schema`_" for an example that requires 419 See "`adding a new field to the classic schema`_" for an example that requires
434 database content changes. 420 database content changes.
421
422
423 Access Controls
424 ===============
425
426 A set of Permissions are built in to the security module by default:
427
428 - Edit (everything)
429 - View (everything)
430
431 The default interfaces define:
432
433 - Web Registration
434 - Web Access
435 - Web Roles
436 - Email Registration
437 - Email Access
438
439 These are hooked into the default Roles:
440
441 - Admin (Edit everything, View everything, Web Roles)
442 - User (Web Access, Email Access)
443 - Anonymous (Web Registration, Email Registration)
444
445 And finally, the "admin" user gets the "Admin" Role, and the "anonymous" user
446 gets the "Anonymous" assigned when the database is initialised on installation.
447 The two default schemas then define:
448
449 - Edit issue, View issue (both)
450 - Edit file, View file (both)
451 - Edit msg, View msg (both)
452 - Edit support, View support (extended only)
453
454 and assign those Permissions to the "User" Role. Put together, these settings
455 appear in the ``open()`` function of the tracker ``dbinit.py`` (the following
456 is taken from the "minimal" template ``dbinit.py``)::
457
458 #
459 # SECURITY SETTINGS
460 #
461 # new permissions for this schema
462 for cl in ('user', ):
463 db.security.addPermission(name="Edit", klass=cl,
464 description="User is allowed to edit "+cl)
465 db.security.addPermission(name="View", klass=cl,
466 description="User is allowed to access "+cl)
467
468 # and give the regular users access to the web and email interface
469 p = db.security.getPermission('Web Access')
470 db.security.addPermissionToRole('User', p)
471 p = db.security.getPermission('Email Access')
472 db.security.addPermissionToRole('User', p)
473
474 # May users view other user information? Comment these lines out
475 # if you don't want them to
476 p = db.security.getPermission('View', 'user')
477 db.security.addPermissionToRole('User', p)
478
479 # Assign the appropriate permissions to the anonymous user's Anonymous
480 # Role. Choices here are:
481 # - Allow anonymous users to register through the web
482 p = db.security.getPermission('Web Registration')
483 db.security.addPermissionToRole('Anonymous', p)
484 # - Allow anonymous (new) users to register through the email gateway
485 p = db.security.getPermission('Email Registration')
486 db.security.addPermissionToRole('Anonymous', p)
487
488
489 New User Roles
490 --------------
491
492 New users are assigned the Roles defined in the config file as:
493
494 - NEW_WEB_USER_ROLES
495 - NEW_EMAIL_USER_ROLES
496
497
498 Changing Access Controls
499 ------------------------
500
501 You may alter the configuration variables to change the Role that new web or
502 email users get, for example to not give them access to the web interface if
503 they register through email.
504
505 You may use the ``roundup-admin`` "``security``" command to display the
506 current Role and Permission configuration in your tracker.
507
508 Adding a new Permission
509 ~~~~~~~~~~~~~~~~~~~~~~~
510
511 When adding a new Permission, you will need to:
512
513 1. add it to your tracker's dbinit so it is created
514 2. enable it for the Roles that should have it (verify with
515 "``roundup-admin security``")
516 3. add it to the relevant HTML interface templates
517 4. add it to the appropriate xxxPermission methods on in your tracker
518 interfaces module
519
520 Example Scenarios
521 ~~~~~~~~~~~~~~~~~
522
523 **automatic registration of users in the e-mail gateway**
524 By giving the "anonymous" user the "Email Registration" Role, any
525 unidentified user will automatically be registered with the tracker (with
526 no password, so they won't be able to log in through the web until an admin
527 sets them a password). Note: this is the default behaviour in the tracker
528 templates that ship with Roundup.
529
530 **anonymous access through the e-mail gateway**
531 Give the "anonymous" user the "Email Access" and ("Edit", "issue") Roles
532 but not giving them the "Email Registration" Role. This means that when an
533 unknown user sends email into the tracker, they're automatically logged in
534 as "anonymous". Since they don't have the "Email Registration" Role, they
535 won't be automatically registered, but since "anonymous" has permission
536 to use the gateway, they'll still be able to submit issues. Note that the
537 Sender information - their email address - will not be available - they're
538 *anonymous*.
539
540 **only developers may be assigned issues**
541 Create a new Permission called "Fixer" for the "issue" class. Create a new
542 Role "Developer" which has that Permission, and assign that to the
543 appropriate users. Filter the list of users available in the assignedto
544 list to include only those users. Enforce the Permission with an auditor. See
545 the example `restricting the list of users that are assignable to a task`_.
546
547 **only managers may sign off issues as complete**
548 Create a new Permission called "Closer" for the "issue" class. Create a new
549 Role "Manager" which has that Permission, and assign that to the appropriate
550 users. In your web interface, only display the "resolved" issue state option
551 when the user has the "Closer" Permissions. Enforce the Permission with
552 an auditor. This is very similar to the previous example, except that the
553 web interface check would look like::
554
555 <option tal:condition="python:request.user.hasPermission('Closer')"
556 value="resolved">Resolved</option>
557
435 558
436 559
437 Web Interface 560 Web Interface
438 ============= 561 =============
439 562
775 fails, the path expression fails. When we get to the end, the object we're 898 fails, the path expression fails. When we get to the end, the object we're
776 left with is evaluated to get a string - methods are called, objects are 899 left with is evaluated to get a string - methods are called, objects are
777 stringified. Path expressions may have an optional ``path:`` prefix, though 900 stringified. Path expressions may have an optional ``path:`` prefix, though
778 they are the default expression type, so it's not necessary. 901 they are the default expression type, so it's not necessary.
779 902
780 XXX | components of expressions 903 If an expression evaluates to ``default`` then the expression is
781 904 "cancelled" - whatever HTML already exists in the template will remain
782 XXX "nothing" and "default" 905 (tag content in the case of tal:content, attributes in the case of
906 tal:attributes).
907
908 If an expression evaluates to ``nothing`` then the target of the expression
909 is removed (tag content in the case of tal:content, attributes in the case
910 of tal:attributes and the tag itself in the case of tal:replace).
911
912 If an element in the path may not exist, then you can use the ``|``
913 operator in the expression to provide an alternative. So, the expression
914 ``request/form/foo/value | default`` would simply leave the current HTML
915 in place if the "foo" form variable doesn't exist.
783 916
784 **String Expressions** - eg. ``string:hello ${user/name}`` 917 **String Expressions** - eg. ``string:hello ${user/name}``
785 These expressions are simple string interpolations (though they can be just 918 These expressions are simple string interpolations (though they can be just
786 plain strings with no interpolation if you want. The expression in the 919 plain strings with no interpolation if you want. The expression in the
787 ``${ ... }`` is just a path expression as above. 920 ``${ ... }`` is just a path expression as above.
1419 *where each journal entry is an HTMLJournalEntry.* 1552 *where each journal entry is an HTMLJournalEntry.*
1420 1553
1421 Defining new web actions 1554 Defining new web actions
1422 ------------------------ 1555 ------------------------
1423 1556
1424 XXX 1557 You may define new actions to be triggered by the ``:action`` form variable.
1425 1558 These are added to the tracker ``interfaces.py`` as methods on the ``Client``
1426 1559 class.
1427 Access Controls 1560
1428 =============== 1561 Adding action methods takes three steps; first you `define the new action
1429 1562 method`_, then you `register the action method`_ with the cgi interface so
1430 A set of Permissions are built in to the security module by default: 1563 it may be triggered by the ``:action`` form variable. Finally you actually
1431 1564 `use the new action`_ in your HTML form.
1432 - Edit (everything) 1565
1433 - View (everything) 1566 See "`setting up a "wizard" (or "druid") for controlled adding of issues`_"
1434 1567 for an example.
1435 The default interfaces define: 1568
1436 1569 Define the new action method
1437 - Web Registration 1570 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1438 - Web Access 1571
1439 - Web Roles 1572 The action methods have the following interface::
1440 - Email Registration 1573
1441 - Email Access 1574 def myActionMethod(self):
1442 1575 ''' Perform some action. No return value is required.
1443 These are hooked into the default Roles: 1576 '''
1444 1577
1445 - Admin (Edit everything, View everything, Web Roles) 1578 The *self* argument is an instance of your tracker ``instance.Client`` class -
1446 - User (Web Access, Email Access) 1579 thus it's mostly implemented by ``roundup.cgi.Client``. See the docstring of
1447 - Anonymous (Web Registration, Email Registration) 1580 that class for details of what it can do.
1448 1581
1449 And finally, the "admin" user gets the "Admin" Role, and the "anonymous" user 1582 The method will typically check the ``self.form`` variable's contents. It
1450 gets the "Anonymous" assigned when the database is initialised on installation. 1583 may then:
1451 The two default schemas then define: 1584
1452 1585 - add information to ``self.ok_message`` or ``self.error_message``
1453 - Edit issue, View issue (both) 1586 - change the ``self.template`` variable to alter what the user will see next
1454 - Edit file, View file (both) 1587 - raise Unauthorised, SendStaticFile, SendFile, NotFound or Redirect
1455 - Edit msg, View msg (both) 1588 exceptions
1456 - Edit support, View support (extended only) 1589
1457 1590
1458 and assign those Permissions to the "User" Role. New users are assigned the 1591 Register the action method
1459 Roles defined in the config file as: 1592 ~~~~~~~~~~~~~~~~~~~~~~~~~~
1460 1593
1461 - NEW_WEB_USER_ROLES 1594 The method is now written, but isn't available to the user until you add it to
1462 - NEW_EMAIL_USER_ROLES 1595 the `instance.Client`` class ``actions`` variable, like so::
1463 1596
1464 You may alter the configuration variables to change the Role that new web or 1597 actions = client.Class.actions + (
1465 email users get, for example to not give them access to the web interface if 1598 ('myaction', 'myActionMethod'),
1466 they register through email. 1599 )
1467 1600
1468 You may use the ``roundup-admin`` "``security``" command to display the 1601 This maps the action name "myaction" to the action method we defined.
1469 current Role and Permission configuration in your tracker. 1602
1470 1603
1471 Adding a new Permission 1604 Use the new action
1472 ----------------------- 1605 ~~~~~~~~~~~~~~~~~~
1473 1606
1474 When adding a new Permission, you will need to: 1607 In your HTML form, add a hidden form element like so::
1475 1608
1476 1. add it to your tracker's dbinit so it is created 1609 <input type="hidden" name=":action" value="myaction">
1477 2. enable it for the Roles that should have it (verify with 1610
1478 "``roundup-admin security``") 1611 where "myaction" is the name you registered in the previous step.
1479 3. add it to the relevant HTML interface templates
1480 4. add it to the appropriate xxxPermission methods on in your tracker
1481 interfaces module
1482
1483 Example Scenarios
1484 -----------------
1485
1486 **automatic registration of users in the e-mail gateway**
1487 By giving the "anonymous" user the "Email Registration" Role, any
1488 unidentified user will automatically be registered with the tracker (with
1489 no password, so they won't be able to log in through the web until an admin
1490 sets them a password). Note: this is the default behaviour in the tracker
1491 templates that ship with Roundup.
1492
1493 **anonymous access through the e-mail gateway**
1494 Give the "anonymous" user the "Email Access" and ("Edit", "issue") Roles
1495 but not giving them the "Email Registration" Role. This means that when an
1496 unknown user sends email into the tracker, they're automatically logged in
1497 as "anonymous". Since they don't have the "Email Registration" Role, they
1498 won't be automatically registered, but since "anonymous" has permission
1499 to use the gateway, they'll still be able to submit issues. Note that the
1500 Sender information - their email address - will not be available - they're
1501 *anonymous*.
1502
1503 XXX more examples needed
1504 1612
1505 1613
1506 Examples 1614 Examples
1507 ======== 1615 ========
1616
1617 .. contents::
1618 :local:
1619 :depth: 1
1508 1620
1509 Adding a new field to the classic schema 1621 Adding a new field to the classic schema
1510 ---------------------------------------- 1622 ----------------------------------------
1511 1623
1512 This example shows how to add a new constrained property (ie. a selection of 1624 This example shows how to add a new constrained property (ie. a selection of
2099 usually to validate user choices and determine what page is next. Now 2211 usually to validate user choices and determine what page is next. Now
2100 encode those actions in methods on the interfaces Client class and insert 2212 encode those actions in methods on the interfaces Client class and insert
2101 hooks to those actions in the "actions" attribute on that class, like so:: 2213 hooks to those actions in the "actions" attribute on that class, like so::
2102 2214
2103 actions = client.Class.actions + ( 2215 actions = client.Class.actions + (
2104 ('page1_submit', page1SubmitAction), 2216 ('page1_submit', 'page1SubmitAction'),
2105 ) 2217 )
2106 2218
2107 def page1SubmitAction(self): 2219 def page1SubmitAction(self):
2108 ''' Verify that the user has selected a category, and then move on 2220 ''' Verify that the user has selected a category, and then move on
2109 to page 2. 2221 to page 2.
2155 matches. 2267 matches.
2156 2268
2157 We also remove the redundant password fields from the ``user.item`` template. 2269 We also remove the redundant password fields from the ``user.item`` template.
2158 2270
2159 2271
2272 Adding a "vacation" flag to users for stopping nosy messages
2273 ------------------------------------------------------------
2274
2275 When users go on vacation and set up vacation email bouncing, you'll start to
2276 see a lot of messages come back through Roundup "Fred is on vacation". Not
2277 very useful, and relatively easy to stop.
2278
2279 1. add a "vacation" flag to your users::
2280
2281 user = Class(db, "user",
2282 username=String(), password=Password(),
2283 address=String(), realname=String(),
2284 phone=String(), organisation=String(),
2285 alternate_addresses=String(),
2286 roles=String(), queries=Multilink("query"),
2287 vacation=Boolean())
2288
2289 2. edit your detector ``nosyreactor.py`` so that the ``nosyreaction()``
2290 consists of::
2291
2292 def nosyreaction(db, cl, nodeid, oldvalues):
2293 # send a copy of all new messages to the nosy list
2294 for msgid in determineNewMessages(cl, nodeid, oldvalues):
2295 try:
2296 users = db.user
2297 messages = db.msg
2298
2299 # figure the recipient ids
2300 sendto = []
2301 r = {}
2302 recipients = messages.get(msgid, 'recipients')
2303 for recipid in messages.get(msgid, 'recipients'):
2304 r[recipid] = 1
2305
2306 # figure the author's id, and indicate they've received the
2307 # message
2308 authid = messages.get(msgid, 'author')
2309
2310 # possibly send the message to the author, as long as they aren't
2311 # anonymous
2312 if (db.config.MESSAGES_TO_AUTHOR == 'yes' and
2313 users.get(authid, 'username') != 'anonymous'):
2314 sendto.append(authid)
2315 r[authid] = 1
2316
2317 # now figure the nosy people who weren't recipients
2318 nosy = cl.get(nodeid, 'nosy')
2319 for nosyid in nosy:
2320 # Don't send nosy mail to the anonymous user (that user
2321 # shouldn't appear in the nosy list, but just in case they
2322 # do...)
2323 if users.get(nosyid, 'username') == 'anonymous':
2324 continue
2325 # make sure they haven't seen the message already
2326 if not r.has_key(nosyid):
2327 # send it to them
2328 sendto.append(nosyid)
2329 recipients.append(nosyid)
2330
2331 # generate a change note
2332 if oldvalues:
2333 note = cl.generateChangeNote(nodeid, oldvalues)
2334 else:
2335 note = cl.generateCreateNote(nodeid)
2336
2337 # we have new recipients
2338 if sendto:
2339 # filter out the people on vacation
2340 sendto = [i for i in sendto if not users.get(i, 'vacation', 0)]
2341
2342 # map userids to addresses
2343 sendto = [users.get(i, 'address') for i in sendto]
2344
2345 # update the message's recipients list
2346 messages.set(msgid, recipients=recipients)
2347
2348 # send the message
2349 cl.send_message(nodeid, msgid, note, sendto)
2350 except roundupdb.MessageSendError, message:
2351 raise roundupdb.DetectorError, message
2352
2353 Note that this is the standard nosy reaction code, with the small addition
2354 of::
2355
2356 # filter out the people on vacation
2357 sendto = [i for i in sendto if not users.get(i, 'vacation', 0)]
2358
2359 which filters out the users that have the vacation flag set to true.
2360
2361
2160 ------------------- 2362 -------------------
2161 2363
2162 Back to `Table of Contents`_ 2364 Back to `Table of Contents`_
2163 2365
2164 .. _`Table of Contents`: index.html 2366 .. _`Table of Contents`: index.html
2165 2367 .. _`design documentation`: design.html
2368

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