Mercurial > p > roundup > code
comparison doc/customizing.txt @ 3130:7308c3c5a943
docs editing from Jean Jordaan
| author | Richard Jones <richard@users.sourceforge.net> |
|---|---|
| date | Sat, 12 Feb 2005 00:47:33 +0000 |
| parents | 021b131bd816 |
| children | 1b15e9eeb592 |
comparison
equal
deleted
inserted
replaced
| 3127:021b131bd816 | 3130:7308c3c5a943 |
|---|---|
| 1 =================== | 1 =================== |
| 2 Customising Roundup | 2 Customising Roundup |
| 3 =================== | 3 =================== |
| 4 | 4 |
| 5 :Version: $Revision: 1.171 $ | 5 :Version: $Revision: 1.172 $ |
| 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. |
| 2588 In both cases, the value is a valid charset name (eg. ``utf-8`` or | 2588 In both cases, the value is a valid charset name (eg. ``utf-8`` or |
| 2589 ``kio8-r``). | 2589 ``kio8-r``). |
| 2590 | 2590 |
| 2591 Inside Roundup, all strings are stored and processed in utf-8. | 2591 Inside Roundup, all strings are stored and processed in utf-8. |
| 2592 Unfortunately, some older browsers do not work properly with | 2592 Unfortunately, some older browsers do not work properly with |
| 2593 utf8-encoded pages (e.g. Netscape Navigator 4 displays wrong | 2593 utf-8-encoded pages (e.g. Netscape Navigator 4 displays wrong |
| 2594 characters in form fields). This version allows to change | 2594 characters in form fields). This version allows one to change |
| 2595 the character set for http transfers. To do so, you may add | 2595 the character set for http transfers. To do so, you may add |
| 2596 the following code to your ``page.html`` template:: | 2596 the following code to your ``page.html`` template:: |
| 2597 | 2597 |
| 2598 <tal:block define="uri string:${request/base}${request/env/PATH_INFO}"> | 2598 <tal:block define="uri string:${request/base}${request/env/PATH_INFO}"> |
| 2599 <a tal:attributes="href python:request.indexargs_href(uri, | 2599 <a tal:attributes="href python:request.indexargs_url(uri, |
| 2600 {'@charset':'utf-8'})">utf-8</a> | 2600 {'@charset':'utf-8'})">utf-8</a> |
| 2601 <a tal:attributes="href python:request.indexargs_href(uri, | 2601 <a tal:attributes="href python:request.indexargs_url(uri, |
| 2602 {'@charset':'koi8-r'})">koi8-r</a> | 2602 {'@charset':'koi8-r'})">koi8-r</a> |
| 2603 </tal:block> | 2603 </tal:block> |
| 2604 | 2604 |
| 2605 (substitute ``koi8-r`` with appropriate charset for your language). | 2605 (substitute ``koi8-r`` with appropriate charset for your language). |
| 2606 Charset preference is kept in the browser cookie ``roundup_charset``. | 2606 Charset preference is kept in the browser cookie ``roundup_charset``. |
| 2607 | 2607 |
| 2608 Lines ``meta http-equiv`` added to the tracker templates in version 0.6.0 | 2608 ``meta http-equiv`` lines added to the tracker templates in version 0.6.0 |
| 2609 should be changed to include actual character set name:: | 2609 should be changed to include actual character set name:: |
| 2610 | 2610 |
| 2611 <meta http-equiv="Content-Type" | 2611 <meta http-equiv="Content-Type" |
| 2612 tal:attributes="content string:text/html;; charset=${request/client/charset}" | 2612 tal:attributes="content string:text/html;; charset=${request/client/charset}" |
| 2613 /> | 2613 /> |
| 2678 | 2678 |
| 2679 | 2679 |
| 2680 Introduction | 2680 Introduction |
| 2681 :::::::::::: | 2681 :::::::::::: |
| 2682 | 2682 |
| 2683 To make the classic schema of roundup useful as a TODO tracking system | 2683 To make the classic schema of Roundup useful as a TODO tracking system |
| 2684 for a group of systems administrators, it needed an extra data field per | 2684 for a group of systems administrators, it needs an extra data field per |
| 2685 issue: a category. | 2685 issue: a category. |
| 2686 | 2686 |
| 2687 This would let sysadmins quickly list all TODOs in their particular area | 2687 This would let sysadmins quickly list all TODOs in their particular area |
| 2688 of interest without having to do complex queries, and without relying on | 2688 of interest without having to do complex queries, and without relying on |
| 2689 the spelling capabilities of other sysadmins (a losing proposition at | 2689 the spelling capabilities of other sysadmins (a losing proposition at |
| 2693 Adding a field to the database | 2693 Adding a field to the database |
| 2694 :::::::::::::::::::::::::::::: | 2694 :::::::::::::::::::::::::::::: |
| 2695 | 2695 |
| 2696 This is the easiest part of the change. The category would just be a | 2696 This is the easiest part of the change. The category would just be a |
| 2697 plain string, nothing fancy. To change what is in the database you need | 2697 plain string, nothing fancy. To change what is in the database you need |
| 2698 to add some lines to the ``open()`` function in ``schema.py``. Under the | 2698 to add some lines to the ``schema.py`` file of your tracker instance. |
| 2699 comment:: | 2699 Under the comment:: |
| 2700 | 2700 |
| 2701 # add any additional database schema configuration here | 2701 # add any additional database schema configuration here |
| 2702 | 2702 |
| 2703 add:: | 2703 add:: |
| 2704 | 2704 |
| 2715 This also means that there can only be one category with a given name. | 2715 This also means that there can only be one category with a given name. |
| 2716 | 2716 |
| 2717 Adding the above lines allows us to create categories, but they're not | 2717 Adding the above lines allows us to create categories, but they're not |
| 2718 tied to the issues that we are going to be creating. It's just a list of | 2718 tied to the issues that we are going to be creating. It's just a list of |
| 2719 categories off on its own, which isn't much use. We need to link it in | 2719 categories off on its own, which isn't much use. We need to link it in |
| 2720 with the issues. To do that, find the lines in the ``open()`` function | 2720 with the issues. To do that, find the lines |
| 2721 in ``schema.py`` which set up the "issue" class, and then add a link to | 2721 in ``schema.py`` which set up the "issue" class, and then add a link to |
| 2722 the category:: | 2722 the category:: |
| 2723 | 2723 |
| 2724 issue = IssueClass(db, "issue", ... , | 2724 issue = IssueClass(db, "issue", ... , |
| 2725 category=Multilink("category"), ... ) | 2725 category=Multilink("category"), ... ) |
| 2733 | 2733 |
| 2734 | 2734 |
| 2735 Populating the new category class | 2735 Populating the new category class |
| 2736 ::::::::::::::::::::::::::::::::: | 2736 ::::::::::::::::::::::::::::::::: |
| 2737 | 2737 |
| 2738 If you haven't initialised the database with the roundup-admin | 2738 If you haven't initialised the database with the ``roundup-admin`` |
| 2739 "initialise" command, then you can add the following to the tracker | 2739 "initialise" command, then you can add the following to the tracker |
| 2740 ``schema.py`` in the ``init()`` function under the comment:: | 2740 ``initial_data.py`` under the comment:: |
| 2741 | 2741 |
| 2742 # add any additional database create steps here - but only if you | 2742 # add any additional database creation steps here - but only if you |
| 2743 # haven't initialised the database with the admin "initialise" command | 2743 # haven't initialised the database with the admin "initialise" command |
| 2744 | 2744 |
| 2745 Add:: | 2745 Add:: |
| 2746 | 2746 |
| 2747 category = db.getclass('category') | 2747 category = db.getclass('category') |
| 2774 doesn't suit us, as we want any user to be able to create new categories | 2774 doesn't suit us, as we want any user to be able to create new categories |
| 2775 as required, and obviously everyone needs to be able to view the | 2775 as required, and obviously everyone needs to be able to view the |
| 2776 categories of issues for it to be useful. | 2776 categories of issues for it to be useful. |
| 2777 | 2777 |
| 2778 We therefore need to change the security of the category objects. This | 2778 We therefore need to change the security of the category objects. This |
| 2779 is also done in the ``open()`` function of ``schema.py``. | 2779 is also done in ``schema.py``. |
| 2780 | 2780 |
| 2781 There are currently two loops which set up permissions and then assign | 2781 There are currently two loops which set up permissions and then assign |
| 2782 them to various roles. Simply add the new "category" to both lists:: | 2782 them to various roles. Simply add the new "category" to both lists:: |
| 2783 | 2783 |
| 2784 # Assign the access and edit permissions for issue, file and message | 2784 # Assign the access and edit permissions for issue, file and message |
| 2787 p = db.security.getPermission('View', cl) | 2787 p = db.security.getPermission('View', cl) |
| 2788 db.security.addPermissionToRole('User', 'View', cl) | 2788 db.security.addPermissionToRole('User', 'View', cl) |
| 2789 db.security.addPermissionToRole('User', 'Edit', cl) | 2789 db.security.addPermissionToRole('User', 'Edit', cl) |
| 2790 db.security.addPermissionToRole('User', 'Create', cl) | 2790 db.security.addPermissionToRole('User', 'Create', cl) |
| 2791 | 2791 |
| 2792 These lines assign the View and Edit Permissions to the "User" role, so | 2792 These lines assign the "View" and "Edit" Permissions to the "User" role, |
| 2793 that normal users can view and edit "category" objects. | 2793 so that normal users can view and edit "category" objects. |
| 2794 | 2794 |
| 2795 This is all the work that needs to be done for the database. It will | 2795 This is all the work that needs to be done for the database. It will |
| 2796 store categories, and let users view and edit them. Now on to the | 2796 store categories, and let users view and edit them. Now on to the |
| 2797 interface stuff. | 2797 interface stuff. |
| 2798 | 2798 |
| 2801 :::::::::::::::::::::::::::::::: | 2801 :::::::::::::::::::::::::::::::: |
| 2802 | 2802 |
| 2803 We need to give the users the ability to create new categories, and the | 2803 We need to give the users the ability to create new categories, and the |
| 2804 place to put the link to this functionality is in the left hand function | 2804 place to put the link to this functionality is in the left hand function |
| 2805 bar, under the "Issues" area. The file that defines how this area looks | 2805 bar, under the "Issues" area. The file that defines how this area looks |
| 2806 is ``html/page``, which is what we are going to be editing next. | 2806 is ``html/page.html``, which is what we are going to be editing next. |
| 2807 | 2807 |
| 2808 If you look at this file you can see that it contains a lot of | 2808 If you look at this file you can see that it contains a lot of |
| 2809 "classblock" sections which are chunks of HTML that will be included or | 2809 "classblock" sections which are chunks of HTML that will be included or |
| 2810 excluded in the output depending on whether the condition in the | 2810 excluded in the output depending on whether the condition in the |
| 2811 classblock is met. Under the end of the classblock for issue is where we | 2811 classblock is met. We are going to add the category code at the end of |
| 2812 are going to add the category code:: | 2812 the classblock for the *issue* class:: |
| 2813 | 2813 |
| 2814 <p class="classblock" | 2814 <p class="classblock" |
| 2815 tal:condition="python:request.user.hasPermission('View', 'category')"> | 2815 tal:condition="python:request.user.hasPermission('View', 'category')"> |
| 2816 <b>Categories</b><br> | 2816 <b>Categories</b><br> |
| 2817 <a tal:condition="python:request.user.hasPermission('Edit', 'category')" | 2817 <a tal:condition="python:request.user.hasPermission('Edit', 'category')" |
| 2832 | 2832 |
| 2833 Note that if you have permission to *view* but not to *edit* categories, | 2833 Note that if you have permission to *view* but not to *edit* categories, |
| 2834 then all you will see is a "Categories" header with nothing underneath | 2834 then all you will see is a "Categories" header with nothing underneath |
| 2835 it. This is obviously not very good interface design, but will do for | 2835 it. This is obviously not very good interface design, but will do for |
| 2836 now. I just claim that it is so I can add more links in this section | 2836 now. I just claim that it is so I can add more links in this section |
| 2837 later on. However to fix the problem you could change the condition in | 2837 later on. However, to fix the problem you could change the condition in |
| 2838 the classblock statement, so that only users with "Edit" permission | 2838 the classblock statement, so that only users with "Edit" permission |
| 2839 would see the "Categories" stuff. | 2839 would see the "Categories" stuff. |
| 2840 | 2840 |
| 2841 | 2841 |
| 2842 Setting up a page to edit categories | 2842 Setting up a page to edit categories |
| 2849 The link was for the *item* template of the *category* object. This | 2849 The link was for the *item* template of the *category* object. This |
| 2850 translates into Roundup looking for a file called ``category.item.html`` | 2850 translates into Roundup looking for a file called ``category.item.html`` |
| 2851 in the ``html`` tracker directory. This is the file that we are going to | 2851 in the ``html`` tracker directory. This is the file that we are going to |
| 2852 write now. | 2852 write now. |
| 2853 | 2853 |
| 2854 First we add an info tag in a comment which doesn't affect the outcome | 2854 First, we add an info tag in a comment which doesn't affect the outcome |
| 2855 of the code at all, but is useful for debugging. If you load a page in a | 2855 of the code at all, but is useful for debugging. If you load a page in a |
| 2856 browser and look at the page source, you can see which sections come | 2856 browser and look at the page source, you can see which sections come |
| 2857 from which files by looking for these comments:: | 2857 from which files by looking for these comments:: |
| 2858 | 2858 |
| 2859 <!-- category.item --> | 2859 <!-- category.item --> |
| 2889 | 2889 |
| 2890 <table class="form"> | 2890 <table class="form"> |
| 2891 <tr><th class="header" colspan="2">Category</th></tr> | 2891 <tr><th class="header" colspan="2">Category</th></tr> |
| 2892 | 2892 |
| 2893 Next, we need the field into which the user is going to enter the new | 2893 Next, we need the field into which the user is going to enter the new |
| 2894 category. The "context.name.field(size=60)" bit tells Roundup to | 2894 category. The ``context.name.field(size=60)`` bit tells Roundup to |
| 2895 generate a normal HTML field of size 60, and the contents of that field | 2895 generate a normal HTML field of size 60, and the contents of that field |
| 2896 will be the "name" variable of the current context (which is | 2896 will be the "name" variable of the current context (namely "category"). |
| 2897 "category"). The upshot of this is that when the user types something in | 2897 The upshot of this is that when the user types something in |
| 2898 to the form, a new category will be created with that name:: | 2898 to the form, a new category will be created with that name:: |
| 2899 | 2899 |
| 2900 <tr> | 2900 <tr> |
| 2901 <th>Name</th> | 2901 <th>Name</th> |
| 2902 <td tal:content="structure python:context.name.field(size=60)"> | 2902 <td tal:content="structure python:context.name.field(size=60)"> |
| 2966 that is pointless unless we can assign categories to issues. Just like | 2966 that is pointless unless we can assign categories to issues. Just like |
| 2967 the ``html/category.item.html`` file was used to define how to add a new | 2967 the ``html/category.item.html`` file was used to define how to add a new |
| 2968 category, the ``html/issue.item.html`` is used to define how a new issue | 2968 category, the ``html/issue.item.html`` is used to define how a new issue |
| 2969 is created. | 2969 is created. |
| 2970 | 2970 |
| 2971 Just like ``category.issue.html`` this file defines a form which has a | 2971 Just like ``category.issue.html``, this file defines a form which has a |
| 2972 table to lay things out. It doesn't matter where in the table we add new | 2972 table to lay things out. It doesn't matter where in the table we add new |
| 2973 stuff, it is entirely up to your sense of aesthetics:: | 2973 stuff, it is entirely up to your sense of aesthetics:: |
| 2974 | 2974 |
| 2975 <th>Category</th> | 2975 <th>Category</th> |
| 2976 <td><span tal:replace="structure context/category/field" /> | 2976 <td><span tal:replace="structure context/category/field" /> |
| 2988 | 2988 |
| 2989 | 2989 |
| 2990 Searching on categories | 2990 Searching on categories |
| 2991 ::::::::::::::::::::::: | 2991 ::::::::::::::::::::::: |
| 2992 | 2992 |
| 2993 We can add categories, and create issues with categories. The next | 2993 Now we can add categories, and create issues with categories. The next |
| 2994 obvious thing that we would like to be able to do, would be to search | 2994 obvious thing that we would like to be able to do, would be to search |
| 2995 for issues based on their category, so that, for example, anyone working | 2995 for issues based on their category, so that, for example, anyone working |
| 2996 on the web server could look at all issues in the category "Web". | 2996 on the web server could look at all issues in the category "Web". |
| 2997 | 2997 |
| 2998 If you look for "Search Issues" in the 'html/page.html' file, you will | 2998 If you look for "Search Issues" in the ``html/page.html`` file, you will |
| 2999 find that it looks something like | 2999 find that it looks something like |
| 3000 ``<a href="issue?@template=search">Search Issues</a>``. This shows us | 3000 ``<a href="issue?@template=search">Search Issues</a>``. This shows us |
| 3001 that when you click on "Search Issues" it will be looking for a | 3001 that when you click on "Search Issues" it will be looking for a |
| 3002 ``issue.search.html`` file to display. So that is the file that we will | 3002 ``issue.search.html`` file to display. So that is the file that we will |
| 3003 change. | 3003 change. |
| 3004 | 3004 |
| 3005 If you look at this file it should be starting to seem familiar, although it | 3005 If you look at this file it should begin to seem familiar, although it |
| 3006 does use some new macros. You can add the new category search code anywhere you | 3006 does use some new macros. You can add the new category search code anywhere you |
| 3007 like within that form:: | 3007 like within that form:: |
| 3008 | 3008 |
| 3009 <tr tal:define="name string:category; | 3009 <tr tal:define="name string:category; |
| 3010 db_klass string:category; | 3010 db_klass string:category; |
| 3014 <td metal:use-macro="column_input"></td> | 3014 <td metal:use-macro="column_input"></td> |
| 3015 <td metal:use-macro="sort_input"></td> | 3015 <td metal:use-macro="sort_input"></td> |
| 3016 <td metal:use-macro="group_input"></td> | 3016 <td metal:use-macro="group_input"></td> |
| 3017 </tr> | 3017 </tr> |
| 3018 | 3018 |
| 3019 The definitions in the <tr> opening tag are used by the macros: | 3019 The definitions in the ``<tr>`` opening tag are used by the macros: |
| 3020 | 3020 |
| 3021 - search_select expands to a drop-down box with all categories using db_klass | 3021 - ``search_select`` expands to a drop-down box with all categories using |
| 3022 and db_content. | 3022 ``db_klass`` and ``db_content``. |
| 3023 - column_input expands to a checkbox for selecting what columns should be | 3023 - ``column_input`` expands to a checkbox for selecting what columns |
| 3024 displayed. | 3024 should be displayed. |
| 3025 - sort_input expands to a radio button for selecting what property should be | 3025 - ``sort_input`` expands to a radio button for selecting what property |
| 3026 sorted on. | 3026 should be sorted on. |
| 3027 - group_input expands to a radio button for selecting what property should be | 3027 - ``group_input`` expands to a radio button for selecting what property |
| 3028 group on. | 3028 should be grouped on. |
| 3029 | 3029 |
| 3030 The category search code above would expand to the following:: | 3030 The category search code above would expand to the following:: |
| 3031 | 3031 |
| 3032 <tr> | 3032 <tr> |
| 3033 <th>Category:</th> | 3033 <th>Category:</th> |
| 3078 The condition is the same as above: only display the condition when the | 3078 The condition is the same as above: only display the condition when the |
| 3079 user hasn't asked for it to be hidden. The next part is to set the | 3079 user hasn't asked for it to be hidden. The next part is to set the |
| 3080 content of the cell to be the category part of "i" - the current issue. | 3080 content of the cell to be the category part of "i" - the current issue. |
| 3081 | 3081 |
| 3082 Finally we have to edit ``html/page.html`` again. This time, we need to | 3082 Finally we have to edit ``html/page.html`` again. This time, we need to |
| 3083 tell it that when the user clicks on "Unasigned Issues" or "All Issues", | 3083 tell it that when the user clicks on "Unassigned Issues" or "All Issues", |
| 3084 the category column should be included in the resulting list. If you | 3084 the category column should be included in the resulting list. If you |
| 3085 scroll down the page file, you can see the links with lots of options. | 3085 scroll down the page file, you can see the links with lots of options. |
| 3086 The option that we are interested in is the ``:columns=`` one which | 3086 The option that we are interested in is the ``:columns=`` one which |
| 3087 tells roundup which fields of the issue to display. Simply add | 3087 tells roundup which fields of the issue to display. Simply add |
| 3088 "category" to that list and it all should work. | 3088 "category" to that list and it all should work. |
| 3111 | 3111 |
| 3112 the "times" property is the new link to the "timelog" class. | 3112 the "times" property is the new link to the "timelog" class. |
| 3113 | 3113 |
| 3114 3. We'll need to let people add in times to the issue, so in the web | 3114 3. We'll need to let people add in times to the issue, so in the web |
| 3115 interface we'll have a new entry field. This is a special field | 3115 interface we'll have a new entry field. This is a special field |
| 3116 because unlike the other fields in the issue.item template, it | 3116 because unlike the other fields in the ``issue.item`` template, it |
| 3117 affects a different item (a timelog item) and not the template's | 3117 affects a different item (a timelog item) and not the template's |
| 3118 item, an issue. We have a special syntax for form fields that affect | 3118 item (an issue). We have a special syntax for form fields that affect |
| 3119 items other than the template default item (see the cgi | 3119 items other than the template default item (see the cgi |
| 3120 documentation on `special form variables`_). In particular, we add a | 3120 documentation on `special form variables`_). In particular, we add a |
| 3121 field to capture a new timelog item's perdiod:: | 3121 field to capture a new timelog item's period:: |
| 3122 | 3122 |
| 3123 <tr> | 3123 <tr> |
| 3124 <th>Time Log</th> | 3124 <th>Time Log</th> |
| 3125 <td colspan=3><input type="text" name="timelog-1@period" /> | 3125 <td colspan=3><input type="text" name="timelog-1@period" /> |
| 3126 <br />(enter as '3y 1m 4d 2:40:02' or parts thereof) | 3126 <br />(enter as '3y 1m 4d 2:40:02' or parts thereof) |
| 3135 | 3135 |
| 3136 On submission, the "-1" timelog item will be created and assigned a | 3136 On submission, the "-1" timelog item will be created and assigned a |
| 3137 real item id. The "times" property of the issue will have the new id | 3137 real item id. The "times" property of the issue will have the new id |
| 3138 added to it. | 3138 added to it. |
| 3139 | 3139 |
| 3140 4. We want to display a total of the time log times that have been | 3140 4. We want to display a total of the timelog times that have been |
| 3141 accumulated for an issue. To do this, we'll need to actually write | 3141 accumulated for an issue. To do this, we'll need to actually write |
| 3142 some Python code, since it's beyond the scope of PageTemplates to | 3142 some Python code, since it's beyond the scope of PageTemplates to |
| 3143 perform such calculations. We do this by adding a module ``timespent.py`` | 3143 perform such calculations. We do this by adding a module ``timespent.py`` |
| 3144 to the ``extensions`` directory in our tracker:: | 3144 to the ``extensions`` directory in our tracker. The contents of this |
| 3145 file is as follows:: | |
| 3145 | 3146 |
| 3146 def totalTimeSpent(times): | 3147 def totalTimeSpent(times): |
| 3147 ''' Call me with a list of timelog items (which have an | 3148 ''' Call me with a list of timelog items (which have an |
| 3148 Interval "period" property) | 3149 Interval "period" property) |
| 3149 ''' | 3150 ''' |
| 3156 instance.registerUtil('totalTimeSpent', totalTimeSpent) | 3157 instance.registerUtil('totalTimeSpent', totalTimeSpent) |
| 3157 | 3158 |
| 3158 We will now be able to access the ``totalTimeSpent`` function via the | 3159 We will now be able to access the ``totalTimeSpent`` function via the |
| 3159 ``utils`` variable in our templates, as shown in the next step. | 3160 ``utils`` variable in our templates, as shown in the next step. |
| 3160 | 3161 |
| 3161 5. Display the time log for an issue:: | 3162 5. Display the timelog for an issue:: |
| 3162 | 3163 |
| 3163 <table class="otherinfo" tal:condition="context/times"> | 3164 <table class="otherinfo" tal:condition="context/times"> |
| 3164 <tr><th colspan="3" class="header">Time Log | 3165 <tr><th colspan="3" class="header">Time Log |
| 3165 <tal:block | 3166 <tal:block |
| 3166 tal:replace="python:utils.totalTimeSpent(context.times)" /> | 3167 tal:replace="python:utils.totalTimeSpent(context.times)" /> |
| 3177 use of the ``totalTimeSpent`` method which will total up the times | 3178 use of the ``totalTimeSpent`` method which will total up the times |
| 3178 for the issue and return a new Interval. That will be automatically | 3179 for the issue and return a new Interval. That will be automatically |
| 3179 displayed in the template as text like "+ 1y 2:40" (1 year, 2 hours | 3180 displayed in the template as text like "+ 1y 2:40" (1 year, 2 hours |
| 3180 and 40 minutes). | 3181 and 40 minutes). |
| 3181 | 3182 |
| 3182 8. If you're using a persistent web server - roundup-server or | 3183 8. If you're using a persistent web server - ``roundup-server`` or |
| 3183 mod_python for example - then you'll need to restart that to pick up | 3184 ``mod_python`` for example - then you'll need to restart that to pick up |
| 3184 the code changes. When that's done, you'll be able to use the new | 3185 the code changes. When that's done, you'll be able to use the new |
| 3185 time logging interface. | 3186 time logging interface. |
| 3186 | 3187 |
| 3187 | 3188 |
| 3188 Tracking different types of issues | 3189 Tracking different types of issues |
| 3197 this is obvious, but sometimes it's better to actually sit down for a | 3198 this is obvious, but sometimes it's better to actually sit down for a |
| 3198 while and think about the schema you're going to implement. | 3199 while and think about the schema you're going to implement. |
| 3199 | 3200 |
| 3200 2. Add the new issue class to your tracker's ``schema.py`` - in this | 3201 2. Add the new issue class to your tracker's ``schema.py`` - in this |
| 3201 example, we're adding a "system support" class. Just after the "issue" | 3202 example, we're adding a "system support" class. Just after the "issue" |
| 3202 class definition in the "open" function, add:: | 3203 class definition, add:: |
| 3203 | 3204 |
| 3204 support = IssueClass(db, "support", | 3205 support = IssueClass(db, "support", |
| 3205 assignedto=Link("user"), topic=Multilink("keyword"), | 3206 assignedto=Link("user"), topic=Multilink("keyword"), |
| 3206 status=Link("status"), deadline=Date(), | 3207 status=Link("status"), deadline=Date(), |
| 3207 affects=Multilink("system")) | 3208 affects=Multilink("system")) |
| 3208 | 3209 |
| 3209 3. Copy the existing "issue.*" (item, search and index) templates in the | 3210 3. Copy the existing ``issue.*`` (item, search and index) templates in the |
| 3210 tracker's "html" to "support.*". Edit them so they use the properties | 3211 tracker's ``html`` to ``support.*``. Edit them so they use the properties |
| 3211 defined in the "support" class. Be sure to check for hidden form | 3212 defined in the ``support`` class. Be sure to check for hidden form |
| 3212 variables like "required" to make sure they have the correct set of | 3213 variables like "required" to make sure they have the correct set of |
| 3213 required properties. | 3214 required properties. |
| 3214 | 3215 |
| 3215 4. Edit the modules in the "detectors", adding lines to their "init" | 3216 4. Edit the modules in the ``detectors``, adding lines to their ``init`` |
| 3216 functions where appropriate. Look for "audit" and "react" registrations | 3217 functions where appropriate. Look for ``audit`` and ``react`` registrations |
| 3217 on the "issue" class, and duplicate them for "support". | 3218 on the ``issue`` class, and duplicate them for ``support``. |
| 3218 | 3219 |
| 3219 5. Create a new sidebar box for the new support class. Duplicate the | 3220 5. Create a new sidebar box for the new support class. Duplicate the |
| 3220 existing issues one, changing the "issue" class name to "support". | 3221 existing issues one, changing the ``issue`` class name to ``support``. |
| 3221 | 3222 |
| 3222 6. Re-start your tracker and start using the new "support" class. | 3223 6. Re-start your tracker and start using the new ``support`` class. |
| 3223 | 3224 |
| 3224 | 3225 |
| 3225 Optionally, you might want to restrict the users able to access this new | 3226 Optionally, you might want to restrict the users able to access this new |
| 3226 class to just the users with a new "SysAdmin" Role. To do this, we add | 3227 class to just the users with a new "SysAdmin" Role. To do this, we add |
| 3227 some security declarations:: | 3228 some security declarations:: |
| 3232 | 3233 |
| 3233 You would then (as an "admin" user) edit the details of the appropriate | 3234 You would then (as an "admin" user) edit the details of the appropriate |
| 3234 users, and add "SysAdmin" to their Roles list. | 3235 users, and add "SysAdmin" to their Roles list. |
| 3235 | 3236 |
| 3236 Alternatively, you might want to change the Edit/View permissions granted | 3237 Alternatively, you might want to change the Edit/View permissions granted |
| 3237 for the "issue" class so that it's only available to users with the "System" | 3238 for the ``issue`` class so that it's only available to users with the "System" |
| 3238 or "Developer" Role, and then the new class you're adding is available to | 3239 or "Developer" Role, and then the new class you're adding is available to |
| 3239 all with the "User" Role. | 3240 all with the "User" Role. |
| 3240 | 3241 |
| 3241 | 3242 |
| 3242 Using External User Databases | 3243 Using External User Databases |
| 3256 | 3257 |
| 3257 Each user of Roundup must still have their information stored in the Roundup | 3258 Each user of Roundup must still have their information stored in the Roundup |
| 3258 database - we just use the passwd file to check their password. To do this, we | 3259 database - we just use the passwd file to check their password. To do this, we |
| 3259 need to override the standard ``verifyPassword`` method defined in | 3260 need to override the standard ``verifyPassword`` method defined in |
| 3260 ``roundup.cgi.actions.LoginAction`` and register the new class. The | 3261 ``roundup.cgi.actions.LoginAction`` and register the new class. The |
| 3261 following is added as ``externapassword.py`` in the tracker ``extensions`` | 3262 following is added as ``externalpassword.py`` in the tracker ``extensions`` |
| 3262 directory:: | 3263 directory:: |
| 3263 | 3264 |
| 3264 from roundup.cgi.actions import LoginAction | 3265 from roundup.cgi.actions import LoginAction |
| 3265 | 3266 |
| 3266 class ExternalPasswordLoginAction(LoginAction): | 3267 class ExternalPasswordLoginAction(LoginAction): |
| 3305 which the users are removed when they no longer have access to a system. | 3306 which the users are removed when they no longer have access to a system. |
| 3306 | 3307 |
| 3307 To make use of the passwd file, we therefore synchronise between the two | 3308 To make use of the passwd file, we therefore synchronise between the two |
| 3308 user stores. We also use the passwd file to validate the user logins, as | 3309 user stores. We also use the passwd file to validate the user logins, as |
| 3309 described in the previous example, `using an external password | 3310 described in the previous example, `using an external password |
| 3310 validation source`_. We keep the users lists in sync using a fairly | 3311 validation source`_. We keep the user lists in sync using a fairly |
| 3311 simple script that runs once a day, or several times an hour if more | 3312 simple script that runs once a day, or several times an hour if more |
| 3312 immediate access is needed. In short, it: | 3313 immediate access is needed. In short, it: |
| 3313 | 3314 |
| 3314 1. parses the passwd file, finding usernames, passwords and real names, | 3315 1. parses the passwd file, finding usernames, passwords and real names, |
| 3315 2. compares that list to the current roundup user list: | 3316 2. compares that list to the current roundup user list: |
| 3320 | 3321 |
| 3321 3. send an email to administrators to let them know what's been done. | 3322 3. send an email to administrators to let them know what's been done. |
| 3322 | 3323 |
| 3323 The retiring and updating are simple operations, requiring only a call | 3324 The retiring and updating are simple operations, requiring only a call |
| 3324 to ``retire()`` or ``set()``. The creation operation requires more | 3325 to ``retire()`` or ``set()``. The creation operation requires more |
| 3325 information though - the user's email address and their roundup Roles. | 3326 information though - the user's email address and their Roundup Roles. |
| 3326 We're going to assume that the user's email address is the same as their | 3327 We're going to assume that the user's email address is the same as their |
| 3327 login name, so we just append the domain name to that. The Roles are | 3328 login name, so we just append the domain name to that. The Roles are |
| 3328 determined using the passwd group identifier - mapping their UN*X group | 3329 determined using the passwd group identifier - mapping their UN*X group |
| 3329 to an appropriate set of Roles. | 3330 to an appropriate set of Roles. |
| 3330 | 3331 |
| 3436 once an hour / day (or on demand if you can work that into your LDAP store | 3437 once an hour / day (or on demand if you can work that into your LDAP store |
| 3437 workflow). See the example `Using a UN*X passwd file as the user database`_ | 3438 workflow). See the example `Using a UN*X passwd file as the user database`_ |
| 3438 for more information about doing this. | 3439 for more information about doing this. |
| 3439 | 3440 |
| 3440 To authenticate off the LDAP store (rather than using the passwords in the | 3441 To authenticate off the LDAP store (rather than using the passwords in the |
| 3441 roundup user database) you'd use the same python-ldap module inside an | 3442 Roundup user database) you'd use the same python-ldap module inside an |
| 3442 extension to the cgi interface. You'd do this by overriding the method called | 3443 extension to the cgi interface. You'd do this by overriding the method called |
| 3443 "verifyPassword" on the LoginAction class in your tracker's interfaces.py | 3444 ``verifyPassword`` on the ``LoginAction`` class in your tracker's |
| 3444 module (see `using an external password validation source`_). The method is | 3445 ``extensions`` directory (see `using an external password validation |
| 3445 implemented by default as:: | 3446 source`_). The method is implemented by default as:: |
| 3446 | 3447 |
| 3447 def verifyPassword(self, userid, password): | 3448 def verifyPassword(self, userid, password): |
| 3448 ''' Verify the password that the user has supplied | 3449 ''' Verify the password that the user has supplied |
| 3449 ''' | 3450 ''' |
| 3450 stored = self.db.user.get(self.userid, 'password') | 3451 stored = self.db.user.get(self.userid, 'password') |
| 3565 which filters out the users that have the vacation flag set to true. | 3566 which filters out the users that have the vacation flag set to true. |
| 3566 | 3567 |
| 3567 Adding in state transition control | 3568 Adding in state transition control |
| 3568 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | 3569 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 3569 | 3570 |
| 3570 Sometimes tracker admins want to control the states that users may move | 3571 Sometimes tracker admins want to control the states to which users may |
| 3571 issues to. You can do this by following these steps: | 3572 move issues. You can do this by following these steps: |
| 3572 | 3573 |
| 3573 1. make "status" a required variable. This is achieved by adding the | 3574 1. make "status" a required variable. This is achieved by adding the |
| 3574 following to the top of the form in the ``issue.item.html`` | 3575 following to the top of the form in the ``issue.item.html`` |
| 3575 template:: | 3576 template:: |
| 3576 | 3577 |
| 3577 <input type="hidden" name="@required" value="status"> | 3578 <input type="hidden" name="@required" value="status"> |
| 3578 | 3579 |
| 3579 this will force users to select a status. | 3580 This will force users to select a status. |
| 3580 | 3581 |
| 3581 2. add a Multilink property to the status class:: | 3582 2. add a Multilink property to the status class:: |
| 3582 | 3583 |
| 3583 stat = Class(db, "status", ... , transitions=Multilink('status'), | 3584 stat = Class(db, "status", ... , transitions=Multilink('status'), |
| 3584 ...) | 3585 ...) |
| 3585 | 3586 |
| 3586 and then edit the statuses already created, either: | 3587 and then edit the statuses already created, either: |
| 3587 | 3588 |
| 3588 a. through the web using the class list -> status class editor, or | 3589 a. through the web using the class list -> status class editor, or |
| 3589 b. using the roundup-admin "set" command. | 3590 b. using the ``roundup-admin`` "set" command. |
| 3590 | 3591 |
| 3591 3. add an auditor module ``checktransition.py`` in your tracker's | 3592 3. add an auditor module ``checktransition.py`` in your tracker's |
| 3592 ``detectors`` directory, for example:: | 3593 ``detectors`` directory, for example:: |
| 3593 | 3594 |
| 3594 def checktransition(db, cl, nodeid, newvalues): | 3595 def checktransition(db, cl, nodeid, newvalues): |
| 3641 | 3642 |
| 3642 We needed the ability to mark certain issues as "blockers" - that is, | 3643 We needed the ability to mark certain issues as "blockers" - that is, |
| 3643 they can't be resolved until another issue (the blocker) they rely on is | 3644 they can't be resolved until another issue (the blocker) they rely on is |
| 3644 resolved. To achieve this: | 3645 resolved. To achieve this: |
| 3645 | 3646 |
| 3646 1. Create a new property on the issue Class, | 3647 1. Create a new property on the ``issue`` class: |
| 3647 ``blockers=Multilink("issue")``. Edit your tracker's schema.py file. | 3648 ``blockers=Multilink("issue")``. To do this, edit the definition of |
| 3648 Where the "issue" class is defined, something like:: | 3649 this class in your tracker's ``schema.py`` file. Change this:: |
| 3649 | 3650 |
| 3650 issue = IssueClass(db, "issue", | 3651 issue = IssueClass(db, "issue", |
| 3651 assignedto=Link("user"), topic=Multilink("keyword"), | 3652 assignedto=Link("user"), topic=Multilink("keyword"), |
| 3652 priority=Link("priority"), status=Link("status")) | 3653 priority=Link("priority"), status=Link("status")) |
| 3653 | 3654 |
| 3654 add the blockers entry like so:: | 3655 to this, adding the blockers entry:: |
| 3655 | 3656 |
| 3656 issue = IssueClass(db, "issue", | 3657 issue = IssueClass(db, "issue", |
| 3657 blockers=Multilink("issue"), | 3658 blockers=Multilink("issue"), |
| 3658 assignedto=Link("user"), topic=Multilink("keyword"), | 3659 assignedto=Link("user"), topic=Multilink("keyword"), |
| 3659 priority=Link("priority"), status=Link("status")) | 3660 priority=Link("priority"), status=Link("status")) |
| 3660 | 3661 |
| 3661 2. Add the new "blockers" property to the issue.item edit page, using | 3662 2. Add the new ``blockers`` property to the ``issue.item.html`` edit |
| 3662 something like:: | 3663 page, using something like:: |
| 3663 | 3664 |
| 3664 <th>Waiting On</th> | 3665 <th>Waiting On</th> |
| 3665 <td> | 3666 <td> |
| 3666 <span tal:replace="structure python:context.blockers.field(showid=1, | 3667 <span tal:replace="structure python:context.blockers.field(showid=1, |
| 3667 size=20)" /> | 3668 size=20)" /> |
| 3675 You'll need to fiddle with your item page layout to find an | 3676 You'll need to fiddle with your item page layout to find an |
| 3676 appropriate place to put it - I'll leave that fun part up to you. | 3677 appropriate place to put it - I'll leave that fun part up to you. |
| 3677 Just make sure it appears in the first table, possibly somewhere near | 3678 Just make sure it appears in the first table, possibly somewhere near |
| 3678 the "superseders" field. | 3679 the "superseders" field. |
| 3679 | 3680 |
| 3680 3. Create a new detector module (attached) which enforces the rules: | 3681 3. Create a new detector module (see below) which enforces the rules: |
| 3681 | 3682 |
| 3682 - issues may not be resolved if they have blockers | 3683 - issues may not be resolved if they have blockers |
| 3683 - when a blocker is resolved, it's removed from issues it blocks | 3684 - when a blocker is resolved, it's removed from issues it blocks |
| 3684 | 3685 |
| 3685 The contents of the detector should be something like this:: | 3686 The contents of the detector should be something like this:: |
| 3753 URLs so they filter out issues with any blockers. You do this by | 3754 URLs so they filter out issues with any blockers. You do this by |
| 3754 adding an additional filter on "blockers" for the value "-1". For | 3755 adding an additional filter on "blockers" for the value "-1". For |
| 3755 example, the existing "Show All" link in the "page" template (in the | 3756 example, the existing "Show All" link in the "page" template (in the |
| 3756 tracker's "html" directory) looks like this:: | 3757 tracker's "html" directory) looks like this:: |
| 3757 | 3758 |
| 3758 <a href="issue?:sort=-activity&:group=priority&:filter=status& | 3759 <a href="issue?@sort=-activity&@group=priority&@filter=status& |
| 3759 :columns=id,activity,title,creator,assignedto,status& | 3760 @columns=id,activity,title,creator,assignedto,status& |
| 3760 status=-1,1,2,3,4,5,6,7">Show All</a><br> | 3761 status=-1,1,2,3,4,5,6,7">Show All</a><br> |
| 3761 | 3762 |
| 3762 modify it to add the "blockers" info to the URL (note, both the | 3763 modify it to add the "blockers" info to the URL (note, both the |
| 3763 ":filter" *and* "blockers" values must be specified):: | 3764 "@filter" *and* "blockers" values must be specified)@@ |
| 3764 | 3765 |
| 3765 <a href="issue?:sort=-activity&:group=priority&:filter=status,blockers& | 3766 <a href="issue?@sort=-activity&@group=priority&@filter=status,blockers& |
| 3766 blockers=-1&:columns=id,activity,title,creator,assignedto,status& | 3767 blockers=-1&@columns=id,activity,title,creator,assignedto,status& |
| 3767 status=-1,1,2,3,4,5,6,7">Show All</a><br> | 3768 status=-1,1,2,3,4,5,6,7">Show All</a><br> |
| 3768 | 3769 |
| 3769 The above examples are line-wrapped on the trailing & and should | 3770 The above examples are line-wrapped on the trailing & and should |
| 3770 be unwrapped. | 3771 be unwrapped. |
| 3771 | 3772 |
| 3776 another issue's "blockers" property. | 3777 another issue's "blockers" property. |
| 3777 | 3778 |
| 3778 Add users to the nosy list based on the topic | 3779 Add users to the nosy list based on the topic |
| 3779 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | 3780 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 3780 | 3781 |
| 3781 We need the ability to automatically add users to the nosy list based | 3782 Let's say we need the ability to automatically add users to the nosy |
| 3782 on the occurence of a topic. Every user should be allowed to edit his | 3783 list based |
| 3783 own list of topics for which he wants to be added to the nosy list. | 3784 on the occurance of a topic. Every user should be allowed to edit their |
| 3784 | 3785 own list of topics for which they want to be added to the nosy list. |
| 3785 Below will be showed that such a change can be performed with only | 3786 |
| 3786 minimal understanding of the roundup system, but with clever use | 3787 Below, we'll show that this change can be done with minimal |
| 3787 of Copy and Paste. | 3788 understanding of the Roundup system, using only copy and paste. |
| 3788 | 3789 |
| 3789 This requires three changes to the tracker: a change in the database to | 3790 This requires three changes to the tracker: a change in the database to |
| 3790 allow per-user recording of the lists of topics for which he wants to | 3791 allow per-user recording of the lists of topics for which he wants to |
| 3791 be put on the nosy list, a change in the user view allowing to edit | 3792 be put on the nosy list, a change in the user view allowing them to edit |
| 3792 this list of topics, and addition of an auditor which updates the nosy | 3793 this list of topics, and addition of an auditor which updates the nosy |
| 3793 list when a topic is set. | 3794 list when a topic is set. |
| 3794 | 3795 |
| 3795 Adding the nosy topic list | 3796 Adding the nosy topic list |
| 3796 :::::::::::::::::::::::::: | 3797 :::::::::::::::::::::::::: |
| 3797 | 3798 |
| 3798 The change in the database to make is that for any user there should be | 3799 The change to make in the database, is that for any user there should be |
| 3799 a list of topics for which he wants to be put on the nosy list. Adding | 3800 a list of topics for which he wants to be put on the nosy list. Adding |
| 3800 a ``Multilink`` of ``keyword`` seem to fullfill this (note that within | 3801 a ``Multilink`` of ``keyword`` seems to fullfill this (note that within |
| 3801 the code topics are called ``keywords``.) As such, all what has to be | 3802 the code, topics are called ``keywords``.) As such, all that has to be |
| 3802 done is to add a new field to the definition of ``user`` within the | 3803 done is to add a new field to the definition of ``user`` within the |
| 3803 file ``schema.py``. We will call this new field ``nosy_keywords``, and | 3804 file ``schema.py``. We will call this new field ``nosy_keywords``, and |
| 3804 the updated definition of user will be:: | 3805 the updated definition of user will be:: |
| 3805 | 3806 |
| 3806 user = Class(db, "user", | 3807 user = Class(db, "user", |
| 3816 :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: | 3817 :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: |
| 3817 | 3818 |
| 3818 We want any user to be able to change the list of topics for which | 3819 We want any user to be able to change the list of topics for which |
| 3819 he will by default be added to the nosy list. We choose to add this | 3820 he will by default be added to the nosy list. We choose to add this |
| 3820 to the user view, as is generated by the file ``html/user.item.html``. | 3821 to the user view, as is generated by the file ``html/user.item.html``. |
| 3821 We easily can | 3822 We can easily |
| 3822 see that the topic field in the issue view has very similar editting | 3823 see that the topic field in the issue view has very similar editing |
| 3823 requirements as our nosy topics, both being a list of topics. As | 3824 requirements as our nosy topics, both being lists of topics. As |
| 3824 such, we search for Topics in ``issue.item.html``, and extract the | 3825 such, we look for Topics in ``issue.item.html``, and extract the |
| 3825 associated parts from there. We add this to ``user.item.html`` at the | 3826 associated parts from there. We add this to ``user.item.html`` at the |
| 3826 bottom of the list of viewed items (i.e. just below the 'Alternate | 3827 bottom of the list of viewed items (i.e. just below the 'Alternate |
| 3827 E-mail addresses' in the classic template):: | 3828 E-mail addresses' in the classic template):: |
| 3828 | 3829 |
| 3829 <tr> | 3830 <tr> |
| 3836 | 3837 |
| 3837 | 3838 |
| 3838 Addition of an auditor to update the nosy list | 3839 Addition of an auditor to update the nosy list |
| 3839 :::::::::::::::::::::::::::::::::::::::::::::: | 3840 :::::::::::::::::::::::::::::::::::::::::::::: |
| 3840 | 3841 |
| 3841 The more difficult part is the addition of the logic to actually | 3842 The more difficult part is the logic to add |
| 3842 at the users to the nosy list when it is required. | 3843 the users to the nosy list when required. |
| 3843 The choice is made to perform this action when the topics on an | 3844 We choose to perform this action whenever the topics on an |
| 3844 item are set, including when an item is created. | 3845 item are set (this includes the creation of items). |
| 3845 Here we choose to start out with a copy of the | 3846 Here we choose to start out with a copy of the |
| 3846 ``detectors/nosyreaction.py`` detector, which we copy to the file | 3847 ``detectors/nosyreaction.py`` detector, which we copy to the file |
| 3847 ``detectors/nosy_keyword_reaction.py``. | 3848 ``detectors/nosy_keyword_reaction.py``. |
| 3848 This looks like a good start as it also adds users | 3849 This looks like a good start as it also adds users |
| 3849 to the nosy list. A look through the code reveals that the | 3850 to the nosy list. A look through the code reveals that the |
| 3850 ``nosyreaction`` function actually is sending the e-mail, which | 3851 ``nosyreaction`` function actually sends the e-mail. |
| 3851 we do not need. As such, we can change the init function to:: | 3852 We don't need this. Therefore, we can change the ``init`` function to:: |
| 3852 | 3853 |
| 3853 def init(db): | 3854 def init(db): |
| 3854 db.issue.audit('create', update_kw_nosy) | 3855 db.issue.audit('create', update_kw_nosy) |
| 3855 db.issue.audit('set', update_kw_nosy) | 3856 db.issue.audit('set', update_kw_nosy) |
| 3856 | 3857 |
| 3857 After that we rename the ``updatenosy`` function to ``update_kw_nosy``. | 3858 After that, we rename the ``updatenosy`` function to ``update_kw_nosy``. |
| 3858 The first two blocks of code in that function relate to settings | 3859 The first two blocks of code in that function relate to setting |
| 3859 ``current`` to a combination of the old and new nosy lists. This | 3860 ``current`` to a combination of the old and new nosy lists. This |
| 3860 functionality is left in the new auditor. The following block of | 3861 functionality is left in the new auditor. The following block of |
| 3861 code, which in ``updatenosy`` handled adding the assignedto user(s) | 3862 code, which handled adding the assignedto user(s) to the nosy list in |
| 3862 to the nosy list, should be replaced by a block of code to add the | 3863 ``updatenosy``, should be replaced by a block of code to add the |
| 3863 interested users to the nosy list. We choose here to loop over all | 3864 interested users to the nosy list. We choose here to loop over all |
| 3864 new topics, than loop over all users, | 3865 new topics, than looping over all users, |
| 3865 and assign the user to the nosy list when the topic in the user's | 3866 and assign the user to the nosy list when the topic occurs in the user's |
| 3866 nosy_keywords. The next part in ``updatenosy``, adding the author | 3867 ``nosy_keywords``. The next part in ``updatenosy`` -- adding the author |
| 3867 and/or recipients of a message to the nosy list, obviously is not | 3868 and/or recipients of a message to the nosy list -- is obviously not |
| 3868 relevant here and thus is deleted from the new auditor. The last | 3869 relevant here and is thus deleted from the new auditor. The last |
| 3869 part, copying the new nosy list to newvalues, does not have to be changed. | 3870 part, copying the new nosy list to ``newvalues``, can stay as is. |
| 3870 This brings the following function:: | 3871 This results in the following function:: |
| 3871 | 3872 |
| 3872 def update_kw_nosy(db, cl, nodeid, newvalues): | 3873 def update_kw_nosy(db, cl, nodeid, newvalues): |
| 3873 '''Update the nosy list for changes to the topics | 3874 '''Update the nosy list for changes to the topics |
| 3874 ''' | 3875 ''' |
| 3875 # nodeid will be None if this is a new node | 3876 # nodeid will be None if this is a new node |
| 3911 current[user_id] = 1 | 3912 current[user_id] = 1 |
| 3912 | 3913 |
| 3913 # that's it, save off the new nosy list | 3914 # that's it, save off the new nosy list |
| 3914 newvalues['nosy'] = current.keys() | 3915 newvalues['nosy'] = current.keys() |
| 3915 | 3916 |
| 3916 and these two function are the only ones needed in the file. | 3917 These two function are the only ones needed in the file. |
| 3917 | 3918 |
| 3918 TODO: update this example to use the find() Class method. | 3919 TODO: update this example to use the ``find()`` Class method. |
| 3919 | 3920 |
| 3920 Caveats | 3921 Caveats |
| 3921 ::::::: | 3922 ::::::: |
| 3922 | 3923 |
| 3923 A few problems with the design here can be noted: | 3924 A few problems with the design here can be noted: |
| 3924 | 3925 |
| 3925 Multiple additions | 3926 Multiple additions |
| 3926 When a user, after automatic selection, is manually removed | 3927 When a user, after automatic selection, is manually removed |
| 3927 from the nosy list, he again is added to the nosy list when the | 3928 from the nosy list, he is added to the nosy list again when the |
| 3928 topic list of the issue is updated. A better design might be | 3929 topic list of the issue is updated. A better design might be |
| 3929 to only check which topics are new compared to the old list | 3930 to only check which topics are new compared to the old list |
| 3930 of topics, and only add users when they have indicated | 3931 of topics, and only add users when they have indicated |
| 3931 interest on a new topic. | 3932 interest on a new topic. |
| 3932 | 3933 |
| 3933 The code could also be changed to only trigger on the create() event, | 3934 The code could also be changed to only trigger on the ``create()`` |
| 3934 rather than also on the set() event, thus only setting the nosy list | 3935 event, rather than also on the ``set()`` event, thus only setting |
| 3935 when the issue is created. | 3936 the nosy list when the issue is created. |
| 3936 | 3937 |
| 3937 Scalability | 3938 Scalability |
| 3938 In the auditor there is a loop over all users. For a site with | 3939 In the auditor, there is a loop over all users. For a site with |
| 3939 only few users this will pose no serious problem, however, with | 3940 only few users this will pose no serious problem; however, with |
| 3940 many users this will be a serious performance bottleneck. | 3941 many users this will be a serious performance bottleneck. |
| 3941 A way out will be to link from the topics to the users which | 3942 A way out would be to link from the topics to the users who |
| 3942 selected these topics a nosy topics. This will eliminate the | 3943 selected these topics as nosy topics. This will eliminate the |
| 3943 loop over all users. | 3944 loop over all users. |
| 3944 | 3945 |
| 3945 Changes to Security and Permissions | 3946 Changes to Security and Permissions |
| 3946 ----------------------------------- | 3947 ----------------------------------- |
| 3947 | 3948 |
| 3960 | 3961 |
| 3961 3. Then assign the new Permission to your "Developer" Role:: | 3962 3. Then assign the new Permission to your "Developer" Role:: |
| 3962 | 3963 |
| 3963 db.security.addPermissionToRole('Developer', p) | 3964 db.security.addPermissionToRole('Developer', p) |
| 3964 | 3965 |
| 3965 4. In the issue item edit page ("html/issue.item.html" in your tracker | 3966 4. In the issue item edit page (``html/issue.item.html`` in your tracker |
| 3966 directory), use the new Permission in restricting the "assignedto" | 3967 directory), use the new Permission in restricting the "assignedto" |
| 3967 list:: | 3968 list:: |
| 3968 | 3969 |
| 3969 <select name="assignedto"> | 3970 <select name="assignedto"> |
| 3970 <option value="-1">- no selection -</option> | 3971 <option value="-1">- no selection -</option> |
| 3977 tal:content="user/realname"></option> | 3978 tal:content="user/realname"></option> |
| 3978 </tal:block> | 3979 </tal:block> |
| 3979 </select> | 3980 </select> |
| 3980 | 3981 |
| 3981 For extra security, you may wish to setup an auditor to enforce the | 3982 For extra security, you may wish to setup an auditor to enforce the |
| 3982 Permission requirement (install this as "assignedtoFixer.py" in your | 3983 Permission requirement (install this as ``assignedtoFixer.py`` in your |
| 3983 tracker "detectors" directory):: | 3984 tracker ``detectors`` directory):: |
| 3984 | 3985 |
| 3985 def assignedtoMustBeFixer(db, cl, nodeid, newvalues): | 3986 def assignedtoMustBeFixer(db, cl, nodeid, newvalues): |
| 3986 ''' Ensure the assignedto value in newvalues is a used with the | 3987 ''' Ensure the assignedto value in newvalues is used with the |
| 3987 Fixer Permission | 3988 Fixer Permission |
| 3988 ''' | 3989 ''' |
| 3989 if not newvalues.has_key('assignedto'): | 3990 if not newvalues.has_key('assignedto'): |
| 3990 # don't care | 3991 # don't care |
| 3991 return | 3992 return |
| 4004 | 4005 |
| 4005 | 4006 |
| 4006 Users may only edit their issues | 4007 Users may only edit their issues |
| 4007 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | 4008 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 4008 | 4009 |
| 4009 Users registering themselves are granted Provisional access - meaning they | 4010 In this case, users registering themselves are granted Provisional |
| 4011 access, meaning they | |
| 4010 have access to edit the issues they submit, but not others. We create a new | 4012 have access to edit the issues they submit, but not others. We create a new |
| 4011 Role called "Provisional User" which is granted to newly-registered users, | 4013 Role called "Provisional User" which is granted to newly-registered users, |
| 4012 and has limited access. One of the Permissions they have is the new "Edit | 4014 and has limited access. One of the Permissions they have is the new "Edit |
| 4013 Own" on issues (regular users have "Edit".) | 4015 Own" on issues (regular users have "Edit".) |
| 4014 | 4016 |
| 4026 db.security.addPermissionToRole('Provisional User', 'Create', 'issue') | 4028 db.security.addPermissionToRole('Provisional User', 'Create', 'issue') |
| 4027 def own_issue(db, userid, itemid): | 4029 def own_issue(db, userid, itemid): |
| 4028 '''Determine whether the userid matches the creator of the issue.''' | 4030 '''Determine whether the userid matches the creator of the issue.''' |
| 4029 return userid == db.issue.get(itemid, 'creator') | 4031 return userid == db.issue.get(itemid, 'creator') |
| 4030 p = db.security.addPermission(name='Edit', klass='issue', | 4032 p = db.security.addPermission(name='Edit', klass='issue', |
| 4031 code=own_issue, description='Can only edit own issues') | 4033 check=own_issue, description='Can only edit own issues') |
| 4032 db.security.addPermissionToRole('Provisional User', p) | 4034 db.security.addPermissionToRole('Provisional User', p) |
| 4033 p = db.security.addPermission(name='View', klass='issue', | 4035 p = db.security.addPermission(name='View', klass='issue', |
| 4034 code=own_issue, description='Can only view own issues') | 4036 check=own_issue, description='Can only view own issues') |
| 4035 db.security.addPermissionToRole('Provisional User', p) | 4037 db.security.addPermissionToRole('Provisional User', p) |
| 4036 | 4038 |
| 4037 # Assign the Permissions for issue-related classes | 4039 # Assign the Permissions for issue-related classes |
| 4038 for cl in 'file', 'msg', 'query', 'keyword': | 4040 for cl in 'file', 'msg', 'query', 'keyword': |
| 4039 db.security.addPermissionToRole('Provisional User', 'View', cl) | 4041 db.security.addPermissionToRole('Provisional User', 'View', cl) |
| 4045 # and give the new users access to the web and email interface | 4047 # and give the new users access to the web and email interface |
| 4046 db.security.addPermissionToRole('Provisional User', 'Web Access') | 4048 db.security.addPermissionToRole('Provisional User', 'Web Access') |
| 4047 db.security.addPermissionToRole('Provisional User', 'Email Access') | 4049 db.security.addPermissionToRole('Provisional User', 'Email Access') |
| 4048 | 4050 |
| 4049 | 4051 |
| 4050 Then in the ``config.ini`` we change the Role assigned to newly-registered | 4052 Then, in ``config.ini``, we change the Role assigned to newly-registered |
| 4051 users, replacing the existing ``'User'`` values:: | 4053 users, replacing the existing ``'User'`` values:: |
| 4052 | 4054 |
| 4053 [main] | 4055 [main] |
| 4054 ... | 4056 ... |
| 4055 new_web_user_roles = 'Provisional User' | 4057 new_web_user_roles = 'Provisional User' |
| 4080 --------------------------------- | 4082 --------------------------------- |
| 4081 | 4083 |
| 4082 Adding action links to the index page | 4084 Adding action links to the index page |
| 4083 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | 4085 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 4084 | 4086 |
| 4085 Add a column to the item.index.html template. | 4087 Add a column to the ``item.index.html`` template. |
| 4086 | 4088 |
| 4087 Resolving the issue:: | 4089 Resolving the issue:: |
| 4088 | 4090 |
| 4089 <a tal:attributes="href | 4091 <a tal:attributes="href |
| 4090 string:issue${i/id}?:status=resolved&:action=edit">resolve</a> | 4092 string:issue${i/id}?:status=resolved&:action=edit">resolve</a> |
| 4092 "Take" the issue:: | 4094 "Take" the issue:: |
| 4093 | 4095 |
| 4094 <a tal:attributes="href | 4096 <a tal:attributes="href |
| 4095 string:issue${i/id}?:assignedto=${request/user/id}&:action=edit">take</a> | 4097 string:issue${i/id}?:assignedto=${request/user/id}&:action=edit">take</a> |
| 4096 | 4098 |
| 4097 ... and so on | 4099 ... and so on. |
| 4098 | 4100 |
| 4099 Colouring the rows in the issue index according to priority | 4101 Colouring the rows in the issue index according to priority |
| 4100 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | 4102 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 4101 | 4103 |
| 4102 A simple ``tal:attributes`` statement will do the bulk of the work here. In | 4104 A simple ``tal:attributes`` statement will do the bulk of the work here. In |
| 4103 the ``issue.index.html`` template, add to the ``<tr>`` that displays the | 4105 the ``issue.index.html`` template, add this to the ``<tr>`` that |
| 4104 actual rows of data:: | 4106 displays the rows of data:: |
| 4105 | 4107 |
| 4106 <tr tal:attributes="class string:priority-${i/priority/plain}"> | 4108 <tr tal:attributes="class string:priority-${i/priority/plain}"> |
| 4107 | 4109 |
| 4108 and then in your stylesheet (``style.css``) specify the colouring for the | 4110 and then in your stylesheet (``style.css``) specify the colouring for the |
| 4109 different priorities, like:: | 4111 different priorities, as follows:: |
| 4110 | 4112 |
| 4111 tr.priority-critical td { | 4113 tr.priority-critical td { |
| 4112 background-color: red; | 4114 background-color: red; |
| 4113 } | 4115 } |
| 4114 | 4116 |
| 4148 <td tal:condition="request/show/status" | 4150 <td tal:condition="request/show/status" |
| 4149 tal:content="structure i/status/field"> </td> | 4151 tal:content="structure i/status/field"> </td> |
| 4150 | 4152 |
| 4151 this will result in an edit field for the status property. | 4153 this will result in an edit field for the status property. |
| 4152 | 4154 |
| 4153 3. after the ``tal:block`` which lists the actual index items (marked by | 4155 3. after the ``tal:block`` which lists the index items (marked by |
| 4154 ``tal:repeat="i batch"``) add a new table row:: | 4156 ``tal:repeat="i batch"``) add a new table row:: |
| 4155 | 4157 |
| 4156 <tr> | 4158 <tr> |
| 4157 <td tal:attributes="colspan python:len(request.columns)"> | 4159 <td tal:attributes="colspan python:len(request.columns)"> |
| 4158 <input type="submit" value=" Save Changes "> | 4160 <input type="submit" value=" Save Changes "> |
| 4160 <tal:block replace="structure request/indexargs_form" /> | 4162 <tal:block replace="structure request/indexargs_form" /> |
| 4161 </td> | 4163 </td> |
| 4162 </tr> | 4164 </tr> |
| 4163 | 4165 |
| 4164 which gives us a submit button, indicates that we are performing an edit | 4166 which gives us a submit button, indicates that we are performing an edit |
| 4165 on any changed statuses and the final block will make sure that the | 4167 on any changed statuses. The final ``tal:block`` will make sure that the |
| 4166 current index view parameters (filtering, columns, etc) will be used in | 4168 current index view parameters (filtering, columns, etc) will be used in |
| 4167 rendering the next page (the results of the editing). | 4169 rendering the next page (the results of the editing). |
| 4168 | 4170 |
| 4169 | 4171 |
| 4170 Displaying only message summaries in the issue display | 4172 Displaying only message summaries in the issue display |
| 4171 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | 4173 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 4172 | 4174 |
| 4173 Alter the issue.item template section for messages to:: | 4175 Alter the ``issue.item`` template section for messages to:: |
| 4174 | 4176 |
| 4175 <table class="messages" tal:condition="context/messages"> | 4177 <table class="messages" tal:condition="context/messages"> |
| 4176 <tr><th colspan="5" class="header">Messages</th></tr> | 4178 <tr><th colspan="5" class="header">Messages</th></tr> |
| 4177 <tr tal:repeat="msg context/messages"> | 4179 <tr tal:repeat="msg context/messages"> |
| 4178 <td><a tal:attributes="href string:msg${msg/id}" | 4180 <td><a tal:attributes="href string:msg${msg/id}" |
| 4192 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | 4194 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 4193 | 4195 |
| 4194 This is pretty simple - all we need to do is copy the code from the | 4196 This is pretty simple - all we need to do is copy the code from the |
| 4195 example `displaying only message summaries in the issue display`_ into | 4197 example `displaying only message summaries in the issue display`_ into |
| 4196 our template alongside the summary display, and then introduce a switch | 4198 our template alongside the summary display, and then introduce a switch |
| 4197 that shows either one or the other. We'll use a new form variable, | 4199 that shows either the one or the other. We'll use a new form variable, |
| 4198 ``@whole_messages`` to achieve this:: | 4200 ``@whole_messages`` to achieve this:: |
| 4199 | 4201 |
| 4200 <table class="messages" tal:condition="context/messages"> | 4202 <table class="messages" tal:condition="context/messages"> |
| 4201 <tal:block tal:condition="not:request/form/@whole_messages/value | python:0"> | 4203 <tal:block tal:condition="not:request/form/@whole_messages/value | python:0"> |
| 4202 <tr><th colspan="3" class="header">Messages</th> | 4204 <tr><th colspan="3" class="header">Messages</th> |
| 4269 . | 4271 . |
| 4270 . | 4272 . |
| 4271 . | 4273 . |
| 4272 </form> | 4274 </form> |
| 4273 | 4275 |
| 4274 Note that later in the form, I test the value of "cat" include form | 4276 Note that later in the form, I use the value of "cat" to decide which |
| 4275 elements that are appropriate. For example:: | 4277 form elements should be displayed. For example:: |
| 4276 | 4278 |
| 4277 <tal:block tal:condition="python:cat in '6 10 13 14 15 16 17'.split()"> | 4279 <tal:block tal:condition="python:cat in '6 10 13 14 15 16 17'.split()"> |
| 4278 <tr> | 4280 <tr> |
| 4279 <th>Operating System</th> | 4281 <th>Operating System</th> |
| 4280 <td tal:content="structure context/os/field"></td> | 4282 <td tal:content="structure context/os/field"></td> |
