|
2 | 2 | Customising Roundup |
3 | 3 | =================== |
4 | 4 |
|
5 | | -:Version: $Revision: 1.58 $ |
| 5 | +:Version: $Revision: 1.59 $ |
6 | 6 |
|
7 | 7 | .. This document borrows from the ZopeBook section on ZPT. The original is at: |
8 | 8 | http://www.zope.org/Documentation/Books/ZopeBook/current/ZPT.stx |
@@ -2579,6 +2579,184 @@ able to give a summary of the total time spent on a particular issue. |
2579 | 2579 | example - then you'll need to restart that to pick up the code changes. |
2580 | 2580 | When that's done, you'll be able to use the new time logging interface. |
2581 | 2581 |
|
| 2582 | +Using a UN*X passwd file as the user database |
| 2583 | +--------------------------------------------- |
| 2584 | + |
| 2585 | +On some systems, the primary store of users is the UN*X passwd file. It holds |
| 2586 | +information on users such as their username, real name, password and primary |
| 2587 | +user group. |
| 2588 | + |
| 2589 | +Roundup can use this store as its primary source of user information, but it |
| 2590 | +needs additional information too - email address(es), roundup Roles, vacation |
| 2591 | +flags, roundup hyperdb item ids, etc. Also, "retired" users must still exist |
| 2592 | +in the user database, unlike some passwd files in which the users are removed |
| 2593 | +when they no longer have access to a system. |
| 2594 | + |
| 2595 | +To make use of the passwd file, we therefore synchronise between the two user |
| 2596 | +stores. We also use the passwd file to validate the user logins, as described |
| 2597 | +in the previous example, `using an external password validation source`_. We |
| 2598 | +keep the users lists in sync using a fairly simple script that runs once a |
| 2599 | +day, or several times an hour if more immediate access is needed. In short, it: |
| 2600 | + |
| 2601 | +1. parses the passwd file, finding usernames, passwords and real names, |
| 2602 | +2. compares that list to the current roundup user list: |
| 2603 | + a. entries no longer in the passwd file are *retired* |
| 2604 | + b. entries with mismatching real names are *updated* |
| 2605 | + a. entries only exist in the passwd file are *created* |
| 2606 | +3. send an email to administrators to let them know what's been done. |
| 2607 | + |
| 2608 | +The retiring and updating are simple operations, requiring only a call to |
| 2609 | +``retire()`` or ``set()``. The creation operation requires more information |
| 2610 | +though - the user's email address and their roundup Roles. We're going to |
| 2611 | +assume that the user's email address is the same as their login name, so we |
| 2612 | +just append the domain name to that. The Roles are determined using the |
| 2613 | +passwd group identifier - mapping their UN*X group to an appropriate set of |
| 2614 | +Roles. |
| 2615 | + |
| 2616 | +The script to perform all this, broken up into its main components, is as |
| 2617 | +follows. Firstly, we import the necessary modules and open the tracker we're |
| 2618 | +to work on:: |
| 2619 | + |
| 2620 | + import sys, os, smtplib |
| 2621 | + from roundup import instance, date |
| 2622 | + |
| 2623 | + # open the tracker |
| 2624 | + tracker_home = sys.argv[1] |
| 2625 | + tracker = instance.open(tracker_home) |
| 2626 | + |
| 2627 | +Next we read in the *passwd* file from the tracker home:: |
| 2628 | + |
| 2629 | + # read in the users |
| 2630 | + file = os.path.join(tracker_home, 'users.passwd') |
| 2631 | + users = [x.strip().split(':') for x in open(file).readlines()] |
| 2632 | + |
| 2633 | +Handle special users (those to ignore in the file, and those who don't appear |
| 2634 | +in the file):: |
| 2635 | + |
| 2636 | + # users to not keep ever, pre-load with the users I know aren't |
| 2637 | + # "real" users |
| 2638 | + ignore = ['ekmmon', 'bfast', 'csrmail'] |
| 2639 | + |
| 2640 | + # users to keep - pre-load with the roundup-specific users |
| 2641 | + keep = ['comment_pool', 'network_pool', 'admin', 'dev-team', 'cs_pool', |
| 2642 | + 'anonymous', 'system_pool', 'automated'] |
| 2643 | + |
| 2644 | +Now we map the UN*X group numbers to the Roles that users should have:: |
| 2645 | + |
| 2646 | + roles = { |
| 2647 | + '501': 'User,Tech', # tech |
| 2648 | + '502': 'User', # finance |
| 2649 | + '503': 'User,CSR', # customer service reps |
| 2650 | + '504': 'User', # sales |
| 2651 | + '505': 'User', # marketing |
| 2652 | + } |
| 2653 | + |
| 2654 | +Now we do all the work. Note that the body of the script (where we have the |
| 2655 | +tracker database open) is wrapped in a ``try`` / ``finally`` clause, so that |
| 2656 | +we always close the database cleanly when we're finished. So, we now do all |
| 2657 | +the work:: |
| 2658 | + |
| 2659 | + # open the database |
| 2660 | + db = tracker.open('admin') |
| 2661 | + try: |
| 2662 | + # store away messages to send to the tracker admins |
| 2663 | + msg = [] |
| 2664 | + |
| 2665 | + # loop over the users list read in from the passwd file |
| 2666 | + for user,passw,uid,gid,real,home,shell in users: |
| 2667 | + if user in ignore: |
| 2668 | + # this user shouldn't appear in our tracker |
| 2669 | + continue |
| 2670 | + keep.append(user) |
| 2671 | + try: |
| 2672 | + # see if the user exists in the tracker |
| 2673 | + uid = db.user.lookup(user) |
| 2674 | + |
| 2675 | + # yes, they do - now check the real name for correctness |
| 2676 | + if real != db.user.get(uid, 'realname'): |
| 2677 | + db.user.set(uid, realname=real) |
| 2678 | + msg.append('FIX %s - %s'%(user, real)) |
| 2679 | + except KeyError: |
| 2680 | + # nope, the user doesn't exist |
| 2681 | + db.user.create(username=user, realname=real, |
| 2682 | + address='% [email protected]'%user, roles=roles[gid]) |
| 2683 | + msg.append('ADD %s - %s (%s)'%(user, real, roles[gid])) |
| 2684 | + |
| 2685 | + # now check that all the users in the tracker are also in our "keep" |
| 2686 | + # list - retire those who aren't |
| 2687 | + for uid in db.user.list(): |
| 2688 | + user = db.user.get(uid, 'username') |
| 2689 | + if user not in keep: |
| 2690 | + db.user.retire(uid) |
| 2691 | + msg.append('RET %s'%user) |
| 2692 | + |
| 2693 | + # if we did work, then send email to the tracker admins |
| 2694 | + if msg: |
| 2695 | + # create the email |
| 2696 | + msg = '''Subject: %s user database maintenance |
| 2697 | + |
| 2698 | + %s |
| 2699 | + '''%(db.config.TRACKER_NAME, '\n'.join(msg)) |
| 2700 | + |
| 2701 | + # send the email |
| 2702 | + smtp = smtplib.SMTP(db.config.MAILHOST) |
| 2703 | + addr = db.config.ADMIN_EMAIL |
| 2704 | + smtp.sendmail(addr, addr, msg) |
| 2705 | + |
| 2706 | + # now we're done - commit the changes |
| 2707 | + db.commit() |
| 2708 | + finally: |
| 2709 | + # always close the database cleanly |
| 2710 | + db.close() |
| 2711 | + |
| 2712 | +And that's it! |
| 2713 | + |
| 2714 | + |
| 2715 | +Enabling display of either message summaries or the entire messages |
| 2716 | +------------------------------------------------------------------- |
| 2717 | + |
| 2718 | +This is pretty simple - all we need to do is copy the code from the example |
| 2719 | +`displaying entire message contents in the issue display`_ into our template |
| 2720 | +alongside the summary display, and then introduce a switch that shows either |
| 2721 | +one or the other. We'll use a new form variable, ``:whole_messages`` to |
| 2722 | +achieve this:: |
| 2723 | + |
| 2724 | + <table class="messages" tal:condition="context/messages"> |
| 2725 | + <tal:block tal:condition="not:request/form/:whole_messages/value | python:0"> |
| 2726 | + <tr><th colspan=3 class="header">Messages</th> |
| 2727 | + <th colspan=2 class="header"> |
| 2728 | + <a href="?:whole_messages=yes">show entire messages</a> |
| 2729 | + </th> |
| 2730 | + </tr> |
| 2731 | + <tr tal:repeat="msg context/messages"> |
| 2732 | + <td><a tal:attributes="href string:msg${msg/id}" |
| 2733 | + tal:content="string:msg${msg/id}"></a></td> |
| 2734 | + <td tal:content="msg/author">author</td> |
| 2735 | + <td nowrap tal:content="msg/date/pretty">date</td> |
| 2736 | + <td tal:content="msg/summary">summary</td> |
| 2737 | + <td> |
| 2738 | + <a tal:attributes="href string:?:remove:messages=${msg/id}&:action=edit">remove</a> |
| 2739 | + </td> |
| 2740 | + </tr> |
| 2741 | + </tal:block> |
| 2742 | + |
| 2743 | + <tal:block tal:condition="request/form/:whole_messages/value | python:0"> |
| 2744 | + <tr><th colspan=2 class="header">Messages</th> |
| 2745 | + <th class="header"><a href="?:whole_messages=">show only summaries</a></th> |
| 2746 | + </tr> |
| 2747 | + <tal:block tal:repeat="msg context/messages"> |
| 2748 | + <tr> |
| 2749 | + <th tal:content="msg/author">author</th> |
| 2750 | + <th nowrap tal:content="msg/date/pretty">date</th> |
| 2751 | + <th style="text-align: right"> |
| 2752 | + (<a tal:attributes="href string:?:remove:messages=${msg/id}&:action=edit">remove</a>) |
| 2753 | + </th> |
| 2754 | + </tr> |
| 2755 | + <tr><td colspan=3 tal:content="msg/content"></td></tr> |
| 2756 | + </tal:block> |
| 2757 | + </tal:block> |
| 2758 | + </table> |
| 2759 | + |
2582 | 2760 |
|
2583 | 2761 | ------------------- |
2584 | 2762 |
|
|
0 commit comments