comparison doc/customizing.txt @ 3131:96086801bd61 maint-0.8

merge from HEAD
author Richard Jones <richard@users.sourceforge.net>
date Sat, 12 Feb 2005 00:54:36 +0000
parents f0051e4bc8b7
children 5ce9c9a1399d
comparison
equal deleted inserted replaced
3128:f0051e4bc8b7 3131:96086801bd61
1 =================== 1 ===================
2 Customising Roundup 2 Customising Roundup
3 =================== 3 ===================
4 4
5 :Version: $Revision: 1.161.2.7 $ 5 :Version: $Revision: 1.161.2.8 $
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::
555 ============================================ 555 ============================================
556 .. _detectors: 556 .. _detectors:
557 557
558 Detectors are initialised every time you open your tracker database, so 558 Detectors are initialised every time you open your tracker database, so
559 you're free to add and remove them any time, even after the database is 559 you're free to add and remove them any time, even after the database is
560 initialised via the "roundup-admin initialise" command. 560 initialised via the ``roundup-admin initialise`` command.
561 561
562 The detectors in your tracker fire *before* (**auditors**) and *after* 562 The detectors in your tracker fire *before* (**auditors**) and *after*
563 (**reactors**) changes to the contents of your database. They are Python 563 (**reactors**) changes to the contents of your database. They are Python
564 modules that sit in your tracker's ``detectors`` directory. You will 564 modules that sit in your tracker's ``detectors`` directory. You will
565 have some installed by default - have a look. You can write new 565 have some installed by default - have a look. You can write new
921 921
922 **properties** 922 **properties**
923 A sequence of property names that are the only properties to apply the 923 A sequence of property names that are the only properties to apply the
924 new Permission to (eg. ``... klass='user', properties=('name', 924 new Permission to (eg. ``... klass='user', properties=('name',
925 'email') ...``) 925 'email') ...``)
926 **code** 926 **check**
927 A function to be execute which returns boolean determining whether the 927 A function to be execute which returns boolean determining whether the
928 Permission is allowed. The function has the signature ``check(db, userid, 928 Permission is allowed. The function has the signature ``check(db, userid,
929 itemid)`` where ``db`` is a handle on the open database, ``userid`` is 929 itemid)`` where ``db`` is a handle on the open database, ``userid`` is
930 the user attempting access and ``itemid`` is the specific item being 930 the user attempting access and ``itemid`` is the specific item being
931 accessed. 931 accessed.
2585 In both cases, the value is a valid charset name (eg. ``utf-8`` or 2585 In both cases, the value is a valid charset name (eg. ``utf-8`` or
2586 ``kio8-r``). 2586 ``kio8-r``).
2587 2587
2588 Inside Roundup, all strings are stored and processed in utf-8. 2588 Inside Roundup, all strings are stored and processed in utf-8.
2589 Unfortunately, some older browsers do not work properly with 2589 Unfortunately, some older browsers do not work properly with
2590 utf8-encoded pages (e.g. Netscape Navigator 4 displays wrong 2590 utf-8-encoded pages (e.g. Netscape Navigator 4 displays wrong
2591 characters in form fields). This version allows to change 2591 characters in form fields). This version allows one to change
2592 the character set for http transfers. To do so, you may add 2592 the character set for http transfers. To do so, you may add
2593 the following code to your ``page.html`` template:: 2593 the following code to your ``page.html`` template::
2594 2594
2595 <tal:block define="uri string:${request/base}${request/env/PATH_INFO}"> 2595 <tal:block define="uri string:${request/base}${request/env/PATH_INFO}">
2596 <a tal:attributes="href python:request.indexargs_href(uri, 2596 <a tal:attributes="href python:request.indexargs_url(uri,
2597 {'@charset':'utf-8'})">utf-8</a> 2597 {'@charset':'utf-8'})">utf-8</a>
2598 <a tal:attributes="href python:request.indexargs_href(uri, 2598 <a tal:attributes="href python:request.indexargs_url(uri,
2599 {'@charset':'koi8-r'})">koi8-r</a> 2599 {'@charset':'koi8-r'})">koi8-r</a>
2600 </tal:block> 2600 </tal:block>
2601 2601
2602 (substitute ``koi8-r`` with appropriate charset for your language). 2602 (substitute ``koi8-r`` with appropriate charset for your language).
2603 Charset preference is kept in the browser cookie ``roundup_charset``. 2603 Charset preference is kept in the browser cookie ``roundup_charset``.
2604 2604
2605 Lines ``meta http-equiv`` added to the tracker templates in version 0.6.0 2605 ``meta http-equiv`` lines added to the tracker templates in version 0.6.0
2606 should be changed to include actual character set name:: 2606 should be changed to include actual character set name::
2607 2607
2608 <meta http-equiv="Content-Type" 2608 <meta http-equiv="Content-Type"
2609 tal:attributes="content string:text/html;; charset=${request/client/charset}" 2609 tal:attributes="content string:text/html;; charset=${request/client/charset}"
2610 /> 2610 />
2675 2675
2676 2676
2677 Introduction 2677 Introduction
2678 :::::::::::: 2678 ::::::::::::
2679 2679
2680 To make the classic schema of roundup useful as a TODO tracking system 2680 To make the classic schema of Roundup useful as a TODO tracking system
2681 for a group of systems administrators, it needed an extra data field per 2681 for a group of systems administrators, it needs an extra data field per
2682 issue: a category. 2682 issue: a category.
2683 2683
2684 This would let sysadmins quickly list all TODOs in their particular area 2684 This would let sysadmins quickly list all TODOs in their particular area
2685 of interest without having to do complex queries, and without relying on 2685 of interest without having to do complex queries, and without relying on
2686 the spelling capabilities of other sysadmins (a losing proposition at 2686 the spelling capabilities of other sysadmins (a losing proposition at
2690 Adding a field to the database 2690 Adding a field to the database
2691 :::::::::::::::::::::::::::::: 2691 ::::::::::::::::::::::::::::::
2692 2692
2693 This is the easiest part of the change. The category would just be a 2693 This is the easiest part of the change. The category would just be a
2694 plain string, nothing fancy. To change what is in the database you need 2694 plain string, nothing fancy. To change what is in the database you need
2695 to add some lines to the ``open()`` function in ``schema.py``. Under the 2695 to add some lines to the ``schema.py`` file of your tracker instance.
2696 comment:: 2696 Under the comment::
2697 2697
2698 # add any additional database schema configuration here 2698 # add any additional database schema configuration here
2699 2699
2700 add:: 2700 add::
2701 2701
2712 This also means that there can only be one category with a given name. 2712 This also means that there can only be one category with a given name.
2713 2713
2714 Adding the above lines allows us to create categories, but they're not 2714 Adding the above lines allows us to create categories, but they're not
2715 tied to the issues that we are going to be creating. It's just a list of 2715 tied to the issues that we are going to be creating. It's just a list of
2716 categories off on its own, which isn't much use. We need to link it in 2716 categories off on its own, which isn't much use. We need to link it in
2717 with the issues. To do that, find the lines in the ``open()`` function 2717 with the issues. To do that, find the lines
2718 in ``schema.py`` which set up the "issue" class, and then add a link to 2718 in ``schema.py`` which set up the "issue" class, and then add a link to
2719 the category:: 2719 the category::
2720 2720
2721 issue = IssueClass(db, "issue", ... , 2721 issue = IssueClass(db, "issue", ... ,
2722 category=Multilink("category"), ... ) 2722 category=Multilink("category"), ... )
2730 2730
2731 2731
2732 Populating the new category class 2732 Populating the new category class
2733 ::::::::::::::::::::::::::::::::: 2733 :::::::::::::::::::::::::::::::::
2734 2734
2735 If you haven't initialised the database with the roundup-admin 2735 If you haven't initialised the database with the ``roundup-admin``
2736 "initialise" command, then you can add the following to the tracker 2736 "initialise" command, then you can add the following to the tracker
2737 ``schema.py`` in the ``init()`` function under the comment:: 2737 ``initial_data.py`` under the comment::
2738 2738
2739 # add any additional database create steps here - but only if you 2739 # add any additional database creation steps here - but only if you
2740 # haven't initialised the database with the admin "initialise" command 2740 # haven't initialised the database with the admin "initialise" command
2741 2741
2742 Add:: 2742 Add::
2743 2743
2744 category = db.getclass('category') 2744 category = db.getclass('category')
2771 doesn't suit us, as we want any user to be able to create new categories 2771 doesn't suit us, as we want any user to be able to create new categories
2772 as required, and obviously everyone needs to be able to view the 2772 as required, and obviously everyone needs to be able to view the
2773 categories of issues for it to be useful. 2773 categories of issues for it to be useful.
2774 2774
2775 We therefore need to change the security of the category objects. This 2775 We therefore need to change the security of the category objects. This
2776 is also done in the ``open()`` function of ``schema.py``. 2776 is also done in ``schema.py``.
2777 2777
2778 There are currently two loops which set up permissions and then assign 2778 There are currently two loops which set up permissions and then assign
2779 them to various roles. Simply add the new "category" to both lists:: 2779 them to various roles. Simply add the new "category" to both lists::
2780 2780
2781 # Assign the access and edit permissions for issue, file and message 2781 # Assign the access and edit permissions for issue, file and message
2784 p = db.security.getPermission('View', cl) 2784 p = db.security.getPermission('View', cl)
2785 db.security.addPermissionToRole('User', 'View', cl) 2785 db.security.addPermissionToRole('User', 'View', cl)
2786 db.security.addPermissionToRole('User', 'Edit', cl) 2786 db.security.addPermissionToRole('User', 'Edit', cl)
2787 db.security.addPermissionToRole('User', 'Create', cl) 2787 db.security.addPermissionToRole('User', 'Create', cl)
2788 2788
2789 These lines assign the View and Edit Permissions to the "User" role, so 2789 These lines assign the "View" and "Edit" Permissions to the "User" role,
2790 that normal users can view and edit "category" objects. 2790 so that normal users can view and edit "category" objects.
2791 2791
2792 This is all the work that needs to be done for the database. It will 2792 This is all the work that needs to be done for the database. It will
2793 store categories, and let users view and edit them. Now on to the 2793 store categories, and let users view and edit them. Now on to the
2794 interface stuff. 2794 interface stuff.
2795 2795
2798 :::::::::::::::::::::::::::::::: 2798 ::::::::::::::::::::::::::::::::
2799 2799
2800 We need to give the users the ability to create new categories, and the 2800 We need to give the users the ability to create new categories, and the
2801 place to put the link to this functionality is in the left hand function 2801 place to put the link to this functionality is in the left hand function
2802 bar, under the "Issues" area. The file that defines how this area looks 2802 bar, under the "Issues" area. The file that defines how this area looks
2803 is ``html/page``, which is what we are going to be editing next. 2803 is ``html/page.html``, which is what we are going to be editing next.
2804 2804
2805 If you look at this file you can see that it contains a lot of 2805 If you look at this file you can see that it contains a lot of
2806 "classblock" sections which are chunks of HTML that will be included or 2806 "classblock" sections which are chunks of HTML that will be included or
2807 excluded in the output depending on whether the condition in the 2807 excluded in the output depending on whether the condition in the
2808 classblock is met. Under the end of the classblock for issue is where we 2808 classblock is met. We are going to add the category code at the end of
2809 are going to add the category code:: 2809 the classblock for the *issue* class::
2810 2810
2811 <p class="classblock" 2811 <p class="classblock"
2812 tal:condition="python:request.user.hasPermission('View', 'category')"> 2812 tal:condition="python:request.user.hasPermission('View', 'category')">
2813 <b>Categories</b><br> 2813 <b>Categories</b><br>
2814 <a tal:condition="python:request.user.hasPermission('Edit', 'category')" 2814 <a tal:condition="python:request.user.hasPermission('Edit', 'category')"
2829 2829
2830 Note that if you have permission to *view* but not to *edit* categories, 2830 Note that if you have permission to *view* but not to *edit* categories,
2831 then all you will see is a "Categories" header with nothing underneath 2831 then all you will see is a "Categories" header with nothing underneath
2832 it. This is obviously not very good interface design, but will do for 2832 it. This is obviously not very good interface design, but will do for
2833 now. I just claim that it is so I can add more links in this section 2833 now. I just claim that it is so I can add more links in this section
2834 later on. However to fix the problem you could change the condition in 2834 later on. However, to fix the problem you could change the condition in
2835 the classblock statement, so that only users with "Edit" permission 2835 the classblock statement, so that only users with "Edit" permission
2836 would see the "Categories" stuff. 2836 would see the "Categories" stuff.
2837 2837
2838 2838
2839 Setting up a page to edit categories 2839 Setting up a page to edit categories
2846 The link was for the *item* template of the *category* object. This 2846 The link was for the *item* template of the *category* object. This
2847 translates into Roundup looking for a file called ``category.item.html`` 2847 translates into Roundup looking for a file called ``category.item.html``
2848 in the ``html`` tracker directory. This is the file that we are going to 2848 in the ``html`` tracker directory. This is the file that we are going to
2849 write now. 2849 write now.
2850 2850
2851 First we add an info tag in a comment which doesn't affect the outcome 2851 First, we add an info tag in a comment which doesn't affect the outcome
2852 of the code at all, but is useful for debugging. If you load a page in a 2852 of the code at all, but is useful for debugging. If you load a page in a
2853 browser and look at the page source, you can see which sections come 2853 browser and look at the page source, you can see which sections come
2854 from which files by looking for these comments:: 2854 from which files by looking for these comments::
2855 2855
2856 <!-- category.item --> 2856 <!-- category.item -->
2886 2886
2887 <table class="form"> 2887 <table class="form">
2888 <tr><th class="header" colspan="2">Category</th></tr> 2888 <tr><th class="header" colspan="2">Category</th></tr>
2889 2889
2890 Next, we need the field into which the user is going to enter the new 2890 Next, we need the field into which the user is going to enter the new
2891 category. The "context.name.field(size=60)" bit tells Roundup to 2891 category. The ``context.name.field(size=60)`` bit tells Roundup to
2892 generate a normal HTML field of size 60, and the contents of that field 2892 generate a normal HTML field of size 60, and the contents of that field
2893 will be the "name" variable of the current context (which is 2893 will be the "name" variable of the current context (namely "category").
2894 "category"). The upshot of this is that when the user types something in 2894 The upshot of this is that when the user types something in
2895 to the form, a new category will be created with that name:: 2895 to the form, a new category will be created with that name::
2896 2896
2897 <tr> 2897 <tr>
2898 <th>Name</th> 2898 <th>Name</th>
2899 <td tal:content="structure python:context.name.field(size=60)"> 2899 <td tal:content="structure python:context.name.field(size=60)">
2963 that is pointless unless we can assign categories to issues. Just like 2963 that is pointless unless we can assign categories to issues. Just like
2964 the ``html/category.item.html`` file was used to define how to add a new 2964 the ``html/category.item.html`` file was used to define how to add a new
2965 category, the ``html/issue.item.html`` is used to define how a new issue 2965 category, the ``html/issue.item.html`` is used to define how a new issue
2966 is created. 2966 is created.
2967 2967
2968 Just like ``category.issue.html`` this file defines a form which has a 2968 Just like ``category.issue.html``, this file defines a form which has a
2969 table to lay things out. It doesn't matter where in the table we add new 2969 table to lay things out. It doesn't matter where in the table we add new
2970 stuff, it is entirely up to your sense of aesthetics:: 2970 stuff, it is entirely up to your sense of aesthetics::
2971 2971
2972 <th>Category</th> 2972 <th>Category</th>
2973 <td><span tal:replace="structure context/category/field" /> 2973 <td><span tal:replace="structure context/category/field" />
2985 2985
2986 2986
2987 Searching on categories 2987 Searching on categories
2988 ::::::::::::::::::::::: 2988 :::::::::::::::::::::::
2989 2989
2990 We can add categories, and create issues with categories. The next 2990 Now we can add categories, and create issues with categories. The next
2991 obvious thing that we would like to be able to do, would be to search 2991 obvious thing that we would like to be able to do, would be to search
2992 for issues based on their category, so that, for example, anyone working 2992 for issues based on their category, so that, for example, anyone working
2993 on the web server could look at all issues in the category "Web". 2993 on the web server could look at all issues in the category "Web".
2994 2994
2995 If you look for "Search Issues" in the 'html/page.html' file, you will 2995 If you look for "Search Issues" in the ``html/page.html`` file, you will
2996 find that it looks something like 2996 find that it looks something like
2997 ``<a href="issue?@template=search">Search Issues</a>``. This shows us 2997 ``<a href="issue?@template=search">Search Issues</a>``. This shows us
2998 that when you click on "Search Issues" it will be looking for a 2998 that when you click on "Search Issues" it will be looking for a
2999 ``issue.search.html`` file to display. So that is the file that we will 2999 ``issue.search.html`` file to display. So that is the file that we will
3000 change. 3000 change.
3001 3001
3002 If you look at this file it should be starting to seem familiar, although it 3002 If you look at this file it should begin to seem familiar, although it
3003 does use some new macros. You can add the new category search code anywhere you 3003 does use some new macros. You can add the new category search code anywhere you
3004 like within that form:: 3004 like within that form::
3005 3005
3006 <tr tal:define="name string:category; 3006 <tr tal:define="name string:category;
3007 db_klass string:category; 3007 db_klass string:category;
3011 <td metal:use-macro="column_input"></td> 3011 <td metal:use-macro="column_input"></td>
3012 <td metal:use-macro="sort_input"></td> 3012 <td metal:use-macro="sort_input"></td>
3013 <td metal:use-macro="group_input"></td> 3013 <td metal:use-macro="group_input"></td>
3014 </tr> 3014 </tr>
3015 3015
3016 The definitions in the <tr> opening tag are used by the macros: 3016 The definitions in the ``<tr>`` opening tag are used by the macros:
3017 3017
3018 - search_select expands to a drop-down box with all categories using db_klass 3018 - ``search_select`` expands to a drop-down box with all categories using
3019 and db_content. 3019 ``db_klass`` and ``db_content``.
3020 - column_input expands to a checkbox for selecting what columns should be 3020 - ``column_input`` expands to a checkbox for selecting what columns
3021 displayed. 3021 should be displayed.
3022 - sort_input expands to a radio button for selecting what property should be 3022 - ``sort_input`` expands to a radio button for selecting what property
3023 sorted on. 3023 should be sorted on.
3024 - group_input expands to a radio button for selecting what property should be 3024 - ``group_input`` expands to a radio button for selecting what property
3025 group on. 3025 should be grouped on.
3026 3026
3027 The category search code above would expand to the following:: 3027 The category search code above would expand to the following::
3028 3028
3029 <tr> 3029 <tr>
3030 <th>Category:</th> 3030 <th>Category:</th>
3075 The condition is the same as above: only display the condition when the 3075 The condition is the same as above: only display the condition when the
3076 user hasn't asked for it to be hidden. The next part is to set the 3076 user hasn't asked for it to be hidden. The next part is to set the
3077 content of the cell to be the category part of "i" - the current issue. 3077 content of the cell to be the category part of "i" - the current issue.
3078 3078
3079 Finally we have to edit ``html/page.html`` again. This time, we need to 3079 Finally we have to edit ``html/page.html`` again. This time, we need to
3080 tell it that when the user clicks on "Unasigned Issues" or "All Issues", 3080 tell it that when the user clicks on "Unassigned Issues" or "All Issues",
3081 the category column should be included in the resulting list. If you 3081 the category column should be included in the resulting list. If you
3082 scroll down the page file, you can see the links with lots of options. 3082 scroll down the page file, you can see the links with lots of options.
3083 The option that we are interested in is the ``:columns=`` one which 3083 The option that we are interested in is the ``:columns=`` one which
3084 tells roundup which fields of the issue to display. Simply add 3084 tells roundup which fields of the issue to display. Simply add
3085 "category" to that list and it all should work. 3085 "category" to that list and it all should work.
3108 3108
3109 the "times" property is the new link to the "timelog" class. 3109 the "times" property is the new link to the "timelog" class.
3110 3110
3111 3. We'll need to let people add in times to the issue, so in the web 3111 3. We'll need to let people add in times to the issue, so in the web
3112 interface we'll have a new entry field. This is a special field 3112 interface we'll have a new entry field. This is a special field
3113 because unlike the other fields in the issue.item template, it 3113 because unlike the other fields in the ``issue.item`` template, it
3114 affects a different item (a timelog item) and not the template's 3114 affects a different item (a timelog item) and not the template's
3115 item, an issue. We have a special syntax for form fields that affect 3115 item (an issue). We have a special syntax for form fields that affect
3116 items other than the template default item (see the cgi 3116 items other than the template default item (see the cgi
3117 documentation on `special form variables`_). In particular, we add a 3117 documentation on `special form variables`_). In particular, we add a
3118 field to capture a new timelog item's perdiod:: 3118 field to capture a new timelog item's period::
3119 3119
3120 <tr> 3120 <tr>
3121 <th>Time Log</th> 3121 <th>Time Log</th>
3122 <td colspan=3><input type="text" name="timelog-1@period" /> 3122 <td colspan=3><input type="text" name="timelog-1@period" />
3123 <br />(enter as '3y 1m 4d 2:40:02' or parts thereof) 3123 <br />(enter as '3y 1m 4d 2:40:02' or parts thereof)
3132 3132
3133 On submission, the "-1" timelog item will be created and assigned a 3133 On submission, the "-1" timelog item will be created and assigned a
3134 real item id. The "times" property of the issue will have the new id 3134 real item id. The "times" property of the issue will have the new id
3135 added to it. 3135 added to it.
3136 3136
3137 4. We want to display a total of the time log times that have been 3137 4. We want to display a total of the timelog times that have been
3138 accumulated for an issue. To do this, we'll need to actually write 3138 accumulated for an issue. To do this, we'll need to actually write
3139 some Python code, since it's beyond the scope of PageTemplates to 3139 some Python code, since it's beyond the scope of PageTemplates to
3140 perform such calculations. We do this by adding a module ``timespent.py`` 3140 perform such calculations. We do this by adding a module ``timespent.py``
3141 to the ``extensions`` directory in our tracker:: 3141 to the ``extensions`` directory in our tracker. The contents of this
3142 file is as follows::
3142 3143
3143 def totalTimeSpent(times): 3144 def totalTimeSpent(times):
3144 ''' Call me with a list of timelog items (which have an 3145 ''' Call me with a list of timelog items (which have an
3145 Interval "period" property) 3146 Interval "period" property)
3146 ''' 3147 '''
3153 instance.registerUtil('totalTimeSpent', totalTimeSpent) 3154 instance.registerUtil('totalTimeSpent', totalTimeSpent)
3154 3155
3155 We will now be able to access the ``totalTimeSpent`` function via the 3156 We will now be able to access the ``totalTimeSpent`` function via the
3156 ``utils`` variable in our templates, as shown in the next step. 3157 ``utils`` variable in our templates, as shown in the next step.
3157 3158
3158 5. Display the time log for an issue:: 3159 5. Display the timelog for an issue::
3159 3160
3160 <table class="otherinfo" tal:condition="context/times"> 3161 <table class="otherinfo" tal:condition="context/times">
3161 <tr><th colspan="3" class="header">Time Log 3162 <tr><th colspan="3" class="header">Time Log
3162 <tal:block 3163 <tal:block
3163 tal:replace="python:utils.totalTimeSpent(context.times)" /> 3164 tal:replace="python:utils.totalTimeSpent(context.times)" />
3174 use of the ``totalTimeSpent`` method which will total up the times 3175 use of the ``totalTimeSpent`` method which will total up the times
3175 for the issue and return a new Interval. That will be automatically 3176 for the issue and return a new Interval. That will be automatically
3176 displayed in the template as text like "+ 1y 2:40" (1 year, 2 hours 3177 displayed in the template as text like "+ 1y 2:40" (1 year, 2 hours
3177 and 40 minutes). 3178 and 40 minutes).
3178 3179
3179 8. If you're using a persistent web server - roundup-server or 3180 8. If you're using a persistent web server - ``roundup-server`` or
3180 mod_python for example - then you'll need to restart that to pick up 3181 ``mod_python`` for example - then you'll need to restart that to pick up
3181 the code changes. When that's done, you'll be able to use the new 3182 the code changes. When that's done, you'll be able to use the new
3182 time logging interface. 3183 time logging interface.
3183 3184
3184 3185
3185 Tracking different types of issues 3186 Tracking different types of issues
3194 this is obvious, but sometimes it's better to actually sit down for a 3195 this is obvious, but sometimes it's better to actually sit down for a
3195 while and think about the schema you're going to implement. 3196 while and think about the schema you're going to implement.
3196 3197
3197 2. Add the new issue class to your tracker's ``schema.py`` - in this 3198 2. Add the new issue class to your tracker's ``schema.py`` - in this
3198 example, we're adding a "system support" class. Just after the "issue" 3199 example, we're adding a "system support" class. Just after the "issue"
3199 class definition in the "open" function, add:: 3200 class definition, add::
3200 3201
3201 support = IssueClass(db, "support", 3202 support = IssueClass(db, "support",
3202 assignedto=Link("user"), topic=Multilink("keyword"), 3203 assignedto=Link("user"), topic=Multilink("keyword"),
3203 status=Link("status"), deadline=Date(), 3204 status=Link("status"), deadline=Date(),
3204 affects=Multilink("system")) 3205 affects=Multilink("system"))
3205 3206
3206 3. Copy the existing "issue.*" (item, search and index) templates in the 3207 3. Copy the existing ``issue.*`` (item, search and index) templates in the
3207 tracker's "html" to "support.*". Edit them so they use the properties 3208 tracker's ``html`` to ``support.*``. Edit them so they use the properties
3208 defined in the "support" class. Be sure to check for hidden form 3209 defined in the ``support`` class. Be sure to check for hidden form
3209 variables like "required" to make sure they have the correct set of 3210 variables like "required" to make sure they have the correct set of
3210 required properties. 3211 required properties.
3211 3212
3212 4. Edit the modules in the "detectors", adding lines to their "init" 3213 4. Edit the modules in the ``detectors``, adding lines to their ``init``
3213 functions where appropriate. Look for "audit" and "react" registrations 3214 functions where appropriate. Look for ``audit`` and ``react`` registrations
3214 on the "issue" class, and duplicate them for "support". 3215 on the ``issue`` class, and duplicate them for ``support``.
3215 3216
3216 5. Create a new sidebar box for the new support class. Duplicate the 3217 5. Create a new sidebar box for the new support class. Duplicate the
3217 existing issues one, changing the "issue" class name to "support". 3218 existing issues one, changing the ``issue`` class name to ``support``.
3218 3219
3219 6. Re-start your tracker and start using the new "support" class. 3220 6. Re-start your tracker and start using the new ``support`` class.
3220 3221
3221 3222
3222 Optionally, you might want to restrict the users able to access this new 3223 Optionally, you might want to restrict the users able to access this new
3223 class to just the users with a new "SysAdmin" Role. To do this, we add 3224 class to just the users with a new "SysAdmin" Role. To do this, we add
3224 some security declarations:: 3225 some security declarations::
3229 3230
3230 You would then (as an "admin" user) edit the details of the appropriate 3231 You would then (as an "admin" user) edit the details of the appropriate
3231 users, and add "SysAdmin" to their Roles list. 3232 users, and add "SysAdmin" to their Roles list.
3232 3233
3233 Alternatively, you might want to change the Edit/View permissions granted 3234 Alternatively, you might want to change the Edit/View permissions granted
3234 for the "issue" class so that it's only available to users with the "System" 3235 for the ``issue`` class so that it's only available to users with the "System"
3235 or "Developer" Role, and then the new class you're adding is available to 3236 or "Developer" Role, and then the new class you're adding is available to
3236 all with the "User" Role. 3237 all with the "User" Role.
3237 3238
3238 3239
3239 Using External User Databases 3240 Using External User Databases
3253 3254
3254 Each user of Roundup must still have their information stored in the Roundup 3255 Each user of Roundup must still have their information stored in the Roundup
3255 database - we just use the passwd file to check their password. To do this, we 3256 database - we just use the passwd file to check their password. To do this, we
3256 need to override the standard ``verifyPassword`` method defined in 3257 need to override the standard ``verifyPassword`` method defined in
3257 ``roundup.cgi.actions.LoginAction`` and register the new class. The 3258 ``roundup.cgi.actions.LoginAction`` and register the new class. The
3258 following is added as ``externapassword.py`` in the tracker ``extensions`` 3259 following is added as ``externalpassword.py`` in the tracker ``extensions``
3259 directory:: 3260 directory::
3260 3261
3261 from roundup.cgi.actions import LoginAction 3262 from roundup.cgi.actions import LoginAction
3262 3263
3263 class ExternalPasswordLoginAction(LoginAction): 3264 class ExternalPasswordLoginAction(LoginAction):
3302 which the users are removed when they no longer have access to a system. 3303 which the users are removed when they no longer have access to a system.
3303 3304
3304 To make use of the passwd file, we therefore synchronise between the two 3305 To make use of the passwd file, we therefore synchronise between the two
3305 user stores. We also use the passwd file to validate the user logins, as 3306 user stores. We also use the passwd file to validate the user logins, as
3306 described in the previous example, `using an external password 3307 described in the previous example, `using an external password
3307 validation source`_. We keep the users lists in sync using a fairly 3308 validation source`_. We keep the user lists in sync using a fairly
3308 simple script that runs once a day, or several times an hour if more 3309 simple script that runs once a day, or several times an hour if more
3309 immediate access is needed. In short, it: 3310 immediate access is needed. In short, it:
3310 3311
3311 1. parses the passwd file, finding usernames, passwords and real names, 3312 1. parses the passwd file, finding usernames, passwords and real names,
3312 2. compares that list to the current roundup user list: 3313 2. compares that list to the current roundup user list:
3317 3318
3318 3. send an email to administrators to let them know what's been done. 3319 3. send an email to administrators to let them know what's been done.
3319 3320
3320 The retiring and updating are simple operations, requiring only a call 3321 The retiring and updating are simple operations, requiring only a call
3321 to ``retire()`` or ``set()``. The creation operation requires more 3322 to ``retire()`` or ``set()``. The creation operation requires more
3322 information though - the user's email address and their roundup Roles. 3323 information though - the user's email address and their Roundup Roles.
3323 We're going to assume that the user's email address is the same as their 3324 We're going to assume that the user's email address is the same as their
3324 login name, so we just append the domain name to that. The Roles are 3325 login name, so we just append the domain name to that. The Roles are
3325 determined using the passwd group identifier - mapping their UN*X group 3326 determined using the passwd group identifier - mapping their UN*X group
3326 to an appropriate set of Roles. 3327 to an appropriate set of Roles.
3327 3328
3433 once an hour / day (or on demand if you can work that into your LDAP store 3434 once an hour / day (or on demand if you can work that into your LDAP store
3434 workflow). See the example `Using a UN*X passwd file as the user database`_ 3435 workflow). See the example `Using a UN*X passwd file as the user database`_
3435 for more information about doing this. 3436 for more information about doing this.
3436 3437
3437 To authenticate off the LDAP store (rather than using the passwords in the 3438 To authenticate off the LDAP store (rather than using the passwords in the
3438 roundup user database) you'd use the same python-ldap module inside an 3439 Roundup user database) you'd use the same python-ldap module inside an
3439 extension to the cgi interface. You'd do this by overriding the method called 3440 extension to the cgi interface. You'd do this by overriding the method called
3440 "verifyPassword" on the LoginAction class in your tracker's interfaces.py 3441 ``verifyPassword`` on the ``LoginAction`` class in your tracker's
3441 module (see `using an external password validation source`_). The method is 3442 ``extensions`` directory (see `using an external password validation
3442 implemented by default as:: 3443 source`_). The method is implemented by default as::
3443 3444
3444 def verifyPassword(self, userid, password): 3445 def verifyPassword(self, userid, password):
3445 ''' Verify the password that the user has supplied 3446 ''' Verify the password that the user has supplied
3446 ''' 3447 '''
3447 stored = self.db.user.get(self.userid, 'password') 3448 stored = self.db.user.get(self.userid, 'password')
3562 which filters out the users that have the vacation flag set to true. 3563 which filters out the users that have the vacation flag set to true.
3563 3564
3564 Adding in state transition control 3565 Adding in state transition control
3565 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 3566 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3566 3567
3567 Sometimes tracker admins want to control the states that users may move 3568 Sometimes tracker admins want to control the states to which users may
3568 issues to. You can do this by following these steps: 3569 move issues. You can do this by following these steps:
3569 3570
3570 1. make "status" a required variable. This is achieved by adding the 3571 1. make "status" a required variable. This is achieved by adding the
3571 following to the top of the form in the ``issue.item.html`` 3572 following to the top of the form in the ``issue.item.html``
3572 template:: 3573 template::
3573 3574
3574 <input type="hidden" name="@required" value="status"> 3575 <input type="hidden" name="@required" value="status">
3575 3576
3576 this will force users to select a status. 3577 This will force users to select a status.
3577 3578
3578 2. add a Multilink property to the status class:: 3579 2. add a Multilink property to the status class::
3579 3580
3580 stat = Class(db, "status", ... , transitions=Multilink('status'), 3581 stat = Class(db, "status", ... , transitions=Multilink('status'),
3581 ...) 3582 ...)
3582 3583
3583 and then edit the statuses already created, either: 3584 and then edit the statuses already created, either:
3584 3585
3585 a. through the web using the class list -> status class editor, or 3586 a. through the web using the class list -> status class editor, or
3586 b. using the roundup-admin "set" command. 3587 b. using the ``roundup-admin`` "set" command.
3587 3588
3588 3. add an auditor module ``checktransition.py`` in your tracker's 3589 3. add an auditor module ``checktransition.py`` in your tracker's
3589 ``detectors`` directory, for example:: 3590 ``detectors`` directory, for example::
3590 3591
3591 def checktransition(db, cl, nodeid, newvalues): 3592 def checktransition(db, cl, nodeid, newvalues):
3638 3639
3639 We needed the ability to mark certain issues as "blockers" - that is, 3640 We needed the ability to mark certain issues as "blockers" - that is,
3640 they can't be resolved until another issue (the blocker) they rely on is 3641 they can't be resolved until another issue (the blocker) they rely on is
3641 resolved. To achieve this: 3642 resolved. To achieve this:
3642 3643
3643 1. Create a new property on the issue Class, 3644 1. Create a new property on the ``issue`` class:
3644 ``blockers=Multilink("issue")``. Edit your tracker's schema.py file. 3645 ``blockers=Multilink("issue")``. To do this, edit the definition of
3645 Where the "issue" class is defined, something like:: 3646 this class in your tracker's ``schema.py`` file. Change this::
3646 3647
3647 issue = IssueClass(db, "issue", 3648 issue = IssueClass(db, "issue",
3648 assignedto=Link("user"), topic=Multilink("keyword"), 3649 assignedto=Link("user"), topic=Multilink("keyword"),
3649 priority=Link("priority"), status=Link("status")) 3650 priority=Link("priority"), status=Link("status"))
3650 3651
3651 add the blockers entry like so:: 3652 to this, adding the blockers entry::
3652 3653
3653 issue = IssueClass(db, "issue", 3654 issue = IssueClass(db, "issue",
3654 blockers=Multilink("issue"), 3655 blockers=Multilink("issue"),
3655 assignedto=Link("user"), topic=Multilink("keyword"), 3656 assignedto=Link("user"), topic=Multilink("keyword"),
3656 priority=Link("priority"), status=Link("status")) 3657 priority=Link("priority"), status=Link("status"))
3657 3658
3658 2. Add the new "blockers" property to the issue.item edit page, using 3659 2. Add the new ``blockers`` property to the ``issue.item.html`` edit
3659 something like:: 3660 page, using something like::
3660 3661
3661 <th>Waiting On</th> 3662 <th>Waiting On</th>
3662 <td> 3663 <td>
3663 <span tal:replace="structure python:context.blockers.field(showid=1, 3664 <span tal:replace="structure python:context.blockers.field(showid=1,
3664 size=20)" /> 3665 size=20)" />
3672 You'll need to fiddle with your item page layout to find an 3673 You'll need to fiddle with your item page layout to find an
3673 appropriate place to put it - I'll leave that fun part up to you. 3674 appropriate place to put it - I'll leave that fun part up to you.
3674 Just make sure it appears in the first table, possibly somewhere near 3675 Just make sure it appears in the first table, possibly somewhere near
3675 the "superseders" field. 3676 the "superseders" field.
3676 3677
3677 3. Create a new detector module (attached) which enforces the rules: 3678 3. Create a new detector module (see below) which enforces the rules:
3678 3679
3679 - issues may not be resolved if they have blockers 3680 - issues may not be resolved if they have blockers
3680 - when a blocker is resolved, it's removed from issues it blocks 3681 - when a blocker is resolved, it's removed from issues it blocks
3681 3682
3682 The contents of the detector should be something like this:: 3683 The contents of the detector should be something like this::
3750 URLs so they filter out issues with any blockers. You do this by 3751 URLs so they filter out issues with any blockers. You do this by
3751 adding an additional filter on "blockers" for the value "-1". For 3752 adding an additional filter on "blockers" for the value "-1". For
3752 example, the existing "Show All" link in the "page" template (in the 3753 example, the existing "Show All" link in the "page" template (in the
3753 tracker's "html" directory) looks like this:: 3754 tracker's "html" directory) looks like this::
3754 3755
3755 <a href="issue?:sort=-activity&:group=priority&:filter=status& 3756 <a href="issue?@sort=-activity&@group=priority&@filter=status&
3756 :columns=id,activity,title,creator,assignedto,status& 3757 @columns=id,activity,title,creator,assignedto,status&
3757 status=-1,1,2,3,4,5,6,7">Show All</a><br> 3758 status=-1,1,2,3,4,5,6,7">Show All</a><br>
3758 3759
3759 modify it to add the "blockers" info to the URL (note, both the 3760 modify it to add the "blockers" info to the URL (note, both the
3760 ":filter" *and* "blockers" values must be specified):: 3761 "@filter" *and* "blockers" values must be specified)@@
3761 3762
3762 <a href="issue?:sort=-activity&:group=priority&:filter=status,blockers& 3763 <a href="issue?@sort=-activity&@group=priority&@filter=status,blockers&
3763 blockers=-1&:columns=id,activity,title,creator,assignedto,status& 3764 blockers=-1&@columns=id,activity,title,creator,assignedto,status&
3764 status=-1,1,2,3,4,5,6,7">Show All</a><br> 3765 status=-1,1,2,3,4,5,6,7">Show All</a><br>
3765 3766
3766 The above examples are line-wrapped on the trailing & and should 3767 The above examples are line-wrapped on the trailing & and should
3767 be unwrapped. 3768 be unwrapped.
3768 3769
3773 another issue's "blockers" property. 3774 another issue's "blockers" property.
3774 3775
3775 Add users to the nosy list based on the topic 3776 Add users to the nosy list based on the topic
3776 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 3777 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3777 3778
3778 We need the ability to automatically add users to the nosy list based 3779 Let's say we need the ability to automatically add users to the nosy
3779 on the occurence of a topic. Every user should be allowed to edit his 3780 list based
3780 own list of topics for which he wants to be added to the nosy list. 3781 on the occurance of a topic. Every user should be allowed to edit their
3781 3782 own list of topics for which they want to be added to the nosy list.
3782 Below will be showed that such a change can be performed with only 3783
3783 minimal understanding of the roundup system, but with clever use 3784 Below, we'll show that this change can be done with minimal
3784 of Copy and Paste. 3785 understanding of the Roundup system, using only copy and paste.
3785 3786
3786 This requires three changes to the tracker: a change in the database to 3787 This requires three changes to the tracker: a change in the database to
3787 allow per-user recording of the lists of topics for which he wants to 3788 allow per-user recording of the lists of topics for which he wants to
3788 be put on the nosy list, a change in the user view allowing to edit 3789 be put on the nosy list, a change in the user view allowing them to edit
3789 this list of topics, and addition of an auditor which updates the nosy 3790 this list of topics, and addition of an auditor which updates the nosy
3790 list when a topic is set. 3791 list when a topic is set.
3791 3792
3792 Adding the nosy topic list 3793 Adding the nosy topic list
3793 :::::::::::::::::::::::::: 3794 ::::::::::::::::::::::::::
3794 3795
3795 The change in the database to make is that for any user there should be 3796 The change to make in the database, is that for any user there should be
3796 a list of topics for which he wants to be put on the nosy list. Adding 3797 a list of topics for which he wants to be put on the nosy list. Adding
3797 a ``Multilink`` of ``keyword`` seem to fullfill this (note that within 3798 a ``Multilink`` of ``keyword`` seems to fullfill this (note that within
3798 the code topics are called ``keywords``.) As such, all what has to be 3799 the code, topics are called ``keywords``.) As such, all that has to be
3799 done is to add a new field to the definition of ``user`` within the 3800 done is to add a new field to the definition of ``user`` within the
3800 file ``schema.py``. We will call this new field ``nosy_keywords``, and 3801 file ``schema.py``. We will call this new field ``nosy_keywords``, and
3801 the updated definition of user will be:: 3802 the updated definition of user will be::
3802 3803
3803 user = Class(db, "user", 3804 user = Class(db, "user",
3813 :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 3814 ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
3814 3815
3815 We want any user to be able to change the list of topics for which 3816 We want any user to be able to change the list of topics for which
3816 he will by default be added to the nosy list. We choose to add this 3817 he will by default be added to the nosy list. We choose to add this
3817 to the user view, as is generated by the file ``html/user.item.html``. 3818 to the user view, as is generated by the file ``html/user.item.html``.
3818 We easily can 3819 We can easily
3819 see that the topic field in the issue view has very similar editting 3820 see that the topic field in the issue view has very similar editing
3820 requirements as our nosy topics, both being a list of topics. As 3821 requirements as our nosy topics, both being lists of topics. As
3821 such, we search for Topics in ``issue.item.html``, and extract the 3822 such, we look for Topics in ``issue.item.html``, and extract the
3822 associated parts from there. We add this to ``user.item.html`` at the 3823 associated parts from there. We add this to ``user.item.html`` at the
3823 bottom of the list of viewed items (i.e. just below the 'Alternate 3824 bottom of the list of viewed items (i.e. just below the 'Alternate
3824 E-mail addresses' in the classic template):: 3825 E-mail addresses' in the classic template)::
3825 3826
3826 <tr> 3827 <tr>
3833 3834
3834 3835
3835 Addition of an auditor to update the nosy list 3836 Addition of an auditor to update the nosy list
3836 :::::::::::::::::::::::::::::::::::::::::::::: 3837 ::::::::::::::::::::::::::::::::::::::::::::::
3837 3838
3838 The more difficult part is the addition of the logic to actually 3839 The more difficult part is the logic to add
3839 at the users to the nosy list when it is required. 3840 the users to the nosy list when required.
3840 The choice is made to perform this action when the topics on an 3841 We choose to perform this action whenever the topics on an
3841 item are set, including when an item is created. 3842 item are set (this includes the creation of items).
3842 Here we choose to start out with a copy of the 3843 Here we choose to start out with a copy of the
3843 ``detectors/nosyreaction.py`` detector, which we copy to the file 3844 ``detectors/nosyreaction.py`` detector, which we copy to the file
3844 ``detectors/nosy_keyword_reaction.py``. 3845 ``detectors/nosy_keyword_reaction.py``.
3845 This looks like a good start as it also adds users 3846 This looks like a good start as it also adds users
3846 to the nosy list. A look through the code reveals that the 3847 to the nosy list. A look through the code reveals that the
3847 ``nosyreaction`` function actually is sending the e-mail, which 3848 ``nosyreaction`` function actually sends the e-mail.
3848 we do not need. As such, we can change the init function to:: 3849 We don't need this. Therefore, we can change the ``init`` function to::
3849 3850
3850 def init(db): 3851 def init(db):
3851 db.issue.audit('create', update_kw_nosy) 3852 db.issue.audit('create', update_kw_nosy)
3852 db.issue.audit('set', update_kw_nosy) 3853 db.issue.audit('set', update_kw_nosy)
3853 3854
3854 After that we rename the ``updatenosy`` function to ``update_kw_nosy``. 3855 After that, we rename the ``updatenosy`` function to ``update_kw_nosy``.
3855 The first two blocks of code in that function relate to settings 3856 The first two blocks of code in that function relate to setting
3856 ``current`` to a combination of the old and new nosy lists. This 3857 ``current`` to a combination of the old and new nosy lists. This
3857 functionality is left in the new auditor. The following block of 3858 functionality is left in the new auditor. The following block of
3858 code, which in ``updatenosy`` handled adding the assignedto user(s) 3859 code, which handled adding the assignedto user(s) to the nosy list in
3859 to the nosy list, should be replaced by a block of code to add the 3860 ``updatenosy``, should be replaced by a block of code to add the
3860 interested users to the nosy list. We choose here to loop over all 3861 interested users to the nosy list. We choose here to loop over all
3861 new topics, than loop over all users, 3862 new topics, than looping over all users,
3862 and assign the user to the nosy list when the topic in the user's 3863 and assign the user to the nosy list when the topic occurs in the user's
3863 nosy_keywords. The next part in ``updatenosy``, adding the author 3864 ``nosy_keywords``. The next part in ``updatenosy`` -- adding the author
3864 and/or recipients of a message to the nosy list, obviously is not 3865 and/or recipients of a message to the nosy list -- is obviously not
3865 relevant here and thus is deleted from the new auditor. The last 3866 relevant here and is thus deleted from the new auditor. The last
3866 part, copying the new nosy list to newvalues, does not have to be changed. 3867 part, copying the new nosy list to ``newvalues``, can stay as is.
3867 This brings the following function:: 3868 This results in the following function::
3868 3869
3869 def update_kw_nosy(db, cl, nodeid, newvalues): 3870 def update_kw_nosy(db, cl, nodeid, newvalues):
3870 '''Update the nosy list for changes to the topics 3871 '''Update the nosy list for changes to the topics
3871 ''' 3872 '''
3872 # nodeid will be None if this is a new node 3873 # nodeid will be None if this is a new node
3908 current[user_id] = 1 3909 current[user_id] = 1
3909 3910
3910 # that's it, save off the new nosy list 3911 # that's it, save off the new nosy list
3911 newvalues['nosy'] = current.keys() 3912 newvalues['nosy'] = current.keys()
3912 3913
3913 and these two function are the only ones needed in the file. 3914 These two function are the only ones needed in the file.
3914 3915
3915 TODO: update this example to use the find() Class method. 3916 TODO: update this example to use the ``find()`` Class method.
3916 3917
3917 Caveats 3918 Caveats
3918 ::::::: 3919 :::::::
3919 3920
3920 A few problems with the design here can be noted: 3921 A few problems with the design here can be noted:
3921 3922
3922 Multiple additions 3923 Multiple additions
3923 When a user, after automatic selection, is manually removed 3924 When a user, after automatic selection, is manually removed
3924 from the nosy list, he again is added to the nosy list when the 3925 from the nosy list, he is added to the nosy list again when the
3925 topic list of the issue is updated. A better design might be 3926 topic list of the issue is updated. A better design might be
3926 to only check which topics are new compared to the old list 3927 to only check which topics are new compared to the old list
3927 of topics, and only add users when they have indicated 3928 of topics, and only add users when they have indicated
3928 interest on a new topic. 3929 interest on a new topic.
3929 3930
3930 The code could also be changed to only trigger on the create() event, 3931 The code could also be changed to only trigger on the ``create()``
3931 rather than also on the set() event, thus only setting the nosy list 3932 event, rather than also on the ``set()`` event, thus only setting
3932 when the issue is created. 3933 the nosy list when the issue is created.
3933 3934
3934 Scalability 3935 Scalability
3935 In the auditor there is a loop over all users. For a site with 3936 In the auditor, there is a loop over all users. For a site with
3936 only few users this will pose no serious problem, however, with 3937 only few users this will pose no serious problem; however, with
3937 many users this will be a serious performance bottleneck. 3938 many users this will be a serious performance bottleneck.
3938 A way out will be to link from the topics to the users which 3939 A way out would be to link from the topics to the users who
3939 selected these topics a nosy topics. This will eliminate the 3940 selected these topics as nosy topics. This will eliminate the
3940 loop over all users. 3941 loop over all users.
3941 3942
3942 Changes to Security and Permissions 3943 Changes to Security and Permissions
3943 ----------------------------------- 3944 -----------------------------------
3944 3945
3957 3958
3958 3. Then assign the new Permission to your "Developer" Role:: 3959 3. Then assign the new Permission to your "Developer" Role::
3959 3960
3960 db.security.addPermissionToRole('Developer', p) 3961 db.security.addPermissionToRole('Developer', p)
3961 3962
3962 4. In the issue item edit page ("html/issue.item.html" in your tracker 3963 4. In the issue item edit page (``html/issue.item.html`` in your tracker
3963 directory), use the new Permission in restricting the "assignedto" 3964 directory), use the new Permission in restricting the "assignedto"
3964 list:: 3965 list::
3965 3966
3966 <select name="assignedto"> 3967 <select name="assignedto">
3967 <option value="-1">- no selection -</option> 3968 <option value="-1">- no selection -</option>
3974 tal:content="user/realname"></option> 3975 tal:content="user/realname"></option>
3975 </tal:block> 3976 </tal:block>
3976 </select> 3977 </select>
3977 3978
3978 For extra security, you may wish to setup an auditor to enforce the 3979 For extra security, you may wish to setup an auditor to enforce the
3979 Permission requirement (install this as "assignedtoFixer.py" in your 3980 Permission requirement (install this as ``assignedtoFixer.py`` in your
3980 tracker "detectors" directory):: 3981 tracker ``detectors`` directory)::
3981 3982
3982 def assignedtoMustBeFixer(db, cl, nodeid, newvalues): 3983 def assignedtoMustBeFixer(db, cl, nodeid, newvalues):
3983 ''' Ensure the assignedto value in newvalues is a used with the 3984 ''' Ensure the assignedto value in newvalues is used with the
3984 Fixer Permission 3985 Fixer Permission
3985 ''' 3986 '''
3986 if not newvalues.has_key('assignedto'): 3987 if not newvalues.has_key('assignedto'):
3987 # don't care 3988 # don't care
3988 return 3989 return
4001 4002
4002 4003
4003 Users may only edit their issues 4004 Users may only edit their issues
4004 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4005 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4005 4006
4006 Users registering themselves are granted Provisional access - meaning they 4007 In this case, users registering themselves are granted Provisional
4008 access, meaning they
4007 have access to edit the issues they submit, but not others. We create a new 4009 have access to edit the issues they submit, but not others. We create a new
4008 Role called "Provisional User" which is granted to newly-registered users, 4010 Role called "Provisional User" which is granted to newly-registered users,
4009 and has limited access. One of the Permissions they have is the new "Edit 4011 and has limited access. One of the Permissions they have is the new "Edit
4010 Own" on issues (regular users have "Edit".) 4012 Own" on issues (regular users have "Edit".)
4011 4013
4023 db.security.addPermissionToRole('Provisional User', 'Create', 'issue') 4025 db.security.addPermissionToRole('Provisional User', 'Create', 'issue')
4024 def own_issue(db, userid, itemid): 4026 def own_issue(db, userid, itemid):
4025 '''Determine whether the userid matches the creator of the issue.''' 4027 '''Determine whether the userid matches the creator of the issue.'''
4026 return userid == db.issue.get(itemid, 'creator') 4028 return userid == db.issue.get(itemid, 'creator')
4027 p = db.security.addPermission(name='Edit', klass='issue', 4029 p = db.security.addPermission(name='Edit', klass='issue',
4028 code=own_issue, description='Can only edit own issues') 4030 check=own_issue, description='Can only edit own issues')
4029 db.security.addPermissionToRole('Provisional User', p) 4031 db.security.addPermissionToRole('Provisional User', p)
4030 p = db.security.addPermission(name='View', klass='issue', 4032 p = db.security.addPermission(name='View', klass='issue',
4031 code=own_issue, description='Can only view own issues') 4033 check=own_issue, description='Can only view own issues')
4032 db.security.addPermissionToRole('Provisional User', p) 4034 db.security.addPermissionToRole('Provisional User', p)
4033 4035
4034 # Assign the Permissions for issue-related classes 4036 # Assign the Permissions for issue-related classes
4035 for cl in 'file', 'msg', 'query', 'keyword': 4037 for cl in 'file', 'msg', 'query', 'keyword':
4036 db.security.addPermissionToRole('Provisional User', 'View', cl) 4038 db.security.addPermissionToRole('Provisional User', 'View', cl)
4042 # and give the new users access to the web and email interface 4044 # and give the new users access to the web and email interface
4043 db.security.addPermissionToRole('Provisional User', 'Web Access') 4045 db.security.addPermissionToRole('Provisional User', 'Web Access')
4044 db.security.addPermissionToRole('Provisional User', 'Email Access') 4046 db.security.addPermissionToRole('Provisional User', 'Email Access')
4045 4047
4046 4048
4047 Then in the ``config.ini`` we change the Role assigned to newly-registered 4049 Then, in ``config.ini``, we change the Role assigned to newly-registered
4048 users, replacing the existing ``'User'`` values:: 4050 users, replacing the existing ``'User'`` values::
4049 4051
4050 [main] 4052 [main]
4051 ... 4053 ...
4052 new_web_user_roles = 'Provisional User' 4054 new_web_user_roles = 'Provisional User'
4057 --------------------------------- 4059 ---------------------------------
4058 4060
4059 Adding action links to the index page 4061 Adding action links to the index page
4060 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4062 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4061 4063
4062 Add a column to the item.index.html template. 4064 Add a column to the ``item.index.html`` template.
4063 4065
4064 Resolving the issue:: 4066 Resolving the issue::
4065 4067
4066 <a tal:attributes="href 4068 <a tal:attributes="href
4067 string:issue${i/id}?:status=resolved&:action=edit">resolve</a> 4069 string:issue${i/id}?:status=resolved&:action=edit">resolve</a>
4069 "Take" the issue:: 4071 "Take" the issue::
4070 4072
4071 <a tal:attributes="href 4073 <a tal:attributes="href
4072 string:issue${i/id}?:assignedto=${request/user/id}&:action=edit">take</a> 4074 string:issue${i/id}?:assignedto=${request/user/id}&:action=edit">take</a>
4073 4075
4074 ... and so on 4076 ... and so on.
4075 4077
4076 Colouring the rows in the issue index according to priority 4078 Colouring the rows in the issue index according to priority
4077 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4079 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4078 4080
4079 A simple ``tal:attributes`` statement will do the bulk of the work here. In 4081 A simple ``tal:attributes`` statement will do the bulk of the work here. In
4080 the ``issue.index.html`` template, add to the ``<tr>`` that displays the 4082 the ``issue.index.html`` template, add this to the ``<tr>`` that
4081 actual rows of data:: 4083 displays the rows of data::
4082 4084
4083 <tr tal:attributes="class string:priority-${i/priority/plain}"> 4085 <tr tal:attributes="class string:priority-${i/priority/plain}">
4084 4086
4085 and then in your stylesheet (``style.css``) specify the colouring for the 4087 and then in your stylesheet (``style.css``) specify the colouring for the
4086 different priorities, like:: 4088 different priorities, as follows::
4087 4089
4088 tr.priority-critical td { 4090 tr.priority-critical td {
4089 background-color: red; 4091 background-color: red;
4090 } 4092 }
4091 4093
4125 <td tal:condition="request/show/status" 4127 <td tal:condition="request/show/status"
4126 tal:content="structure i/status/field">&nbsp;</td> 4128 tal:content="structure i/status/field">&nbsp;</td>
4127 4129
4128 this will result in an edit field for the status property. 4130 this will result in an edit field for the status property.
4129 4131
4130 3. after the ``tal:block`` which lists the actual index items (marked by 4132 3. after the ``tal:block`` which lists the index items (marked by
4131 ``tal:repeat="i batch"``) add a new table row:: 4133 ``tal:repeat="i batch"``) add a new table row::
4132 4134
4133 <tr> 4135 <tr>
4134 <td tal:attributes="colspan python:len(request.columns)"> 4136 <td tal:attributes="colspan python:len(request.columns)">
4135 <input type="submit" value=" Save Changes "> 4137 <input type="submit" value=" Save Changes ">
4137 <tal:block replace="structure request/indexargs_form" /> 4139 <tal:block replace="structure request/indexargs_form" />
4138 </td> 4140 </td>
4139 </tr> 4141 </tr>
4140 4142
4141 which gives us a submit button, indicates that we are performing an edit 4143 which gives us a submit button, indicates that we are performing an edit
4142 on any changed statuses and the final block will make sure that the 4144 on any changed statuses. The final ``tal:block`` will make sure that the
4143 current index view parameters (filtering, columns, etc) will be used in 4145 current index view parameters (filtering, columns, etc) will be used in
4144 rendering the next page (the results of the editing). 4146 rendering the next page (the results of the editing).
4145 4147
4146 4148
4147 Displaying only message summaries in the issue display 4149 Displaying only message summaries in the issue display
4148 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4150 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4149 4151
4150 Alter the issue.item template section for messages to:: 4152 Alter the ``issue.item`` template section for messages to::
4151 4153
4152 <table class="messages" tal:condition="context/messages"> 4154 <table class="messages" tal:condition="context/messages">
4153 <tr><th colspan="5" class="header">Messages</th></tr> 4155 <tr><th colspan="5" class="header">Messages</th></tr>
4154 <tr tal:repeat="msg context/messages"> 4156 <tr tal:repeat="msg context/messages">
4155 <td><a tal:attributes="href string:msg${msg/id}" 4157 <td><a tal:attributes="href string:msg${msg/id}"
4169 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4171 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4170 4172
4171 This is pretty simple - all we need to do is copy the code from the 4173 This is pretty simple - all we need to do is copy the code from the
4172 example `displaying only message summaries in the issue display`_ into 4174 example `displaying only message summaries in the issue display`_ into
4173 our template alongside the summary display, and then introduce a switch 4175 our template alongside the summary display, and then introduce a switch
4174 that shows either one or the other. We'll use a new form variable, 4176 that shows either the one or the other. We'll use a new form variable,
4175 ``@whole_messages`` to achieve this:: 4177 ``@whole_messages`` to achieve this::
4176 4178
4177 <table class="messages" tal:condition="context/messages"> 4179 <table class="messages" tal:condition="context/messages">
4178 <tal:block tal:condition="not:request/form/@whole_messages/value | python:0"> 4180 <tal:block tal:condition="not:request/form/@whole_messages/value | python:0">
4179 <tr><th colspan="3" class="header">Messages</th> 4181 <tr><th colspan="3" class="header">Messages</th>
4246 . 4248 .
4247 . 4249 .
4248 . 4250 .
4249 </form> 4251 </form>
4250 4252
4251 Note that later in the form, I test the value of "cat" include form 4253 Note that later in the form, I use the value of "cat" to decide which
4252 elements that are appropriate. For example:: 4254 form elements should be displayed. For example::
4253 4255
4254 <tal:block tal:condition="python:cat in '6 10 13 14 15 16 17'.split()"> 4256 <tal:block tal:condition="python:cat in '6 10 13 14 15 16 17'.split()">
4255 <tr> 4257 <tr>
4256 <th>Operating System</th> 4258 <th>Operating System</th>
4257 <td tal:content="structure context/os/field"></td> 4259 <td tal:content="structure context/os/field"></td>

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