[zopeorg-checkins] CVS: Products/TrackerBase - CHANGES.txt:1.1 LICENSE.txt:1.1 Noodging.py:1.1 README.txt:1.1 Tracker.zexp:1.1 TrackerBase.py:1.1 TrackerMethods.py:1.1 TrackerMisc.py:1.1 __init__.py:1.1 addTrackerForm.dtml:1.1 age.py:1.1 envsetup.py:1.1 issue.gif:1.1 item.gif:1.1 tracks.gif:1.1

Sidnei da Silva sidnei at x3ng.com.br
Fri May 30 11:17:55 EDT 2003


Update of /cvs-zopeorg/Products/TrackerBase
In directory cvs.zope.org:/tmp/cvs-serv19195/TrackerBase

Added Files:
	CHANGES.txt LICENSE.txt Noodging.py README.txt Tracker.zexp 
	TrackerBase.py TrackerMethods.py TrackerMisc.py __init__.py 
	addTrackerForm.dtml age.py envsetup.py issue.gif item.gif 
	tracks.gif 
Log Message:
Adding products needed for migration of NZO

=== Added File Products/TrackerBase/CHANGES.txt ===
21-Feb-2000 Mon 1.02+

        * TrackerMethods send_posting(): Refined header origin and
        destination addresses - still WAGs, tho:

          - Made the From a bit more self-explanatory, with the
            tracker-admin's address as the origin.

          - Using an empty '<>' for the destinations - seems to
            work...

        I have to consult the RFC to find out the right way (if there
        is one) to not provide destination addresses.


        Changed some more to deal with sites with large membership
        (editStateYikes).


        Removed WRAP=hard everywhere - it wraps URLs in the middle!!


=== Added File Products/TrackerBase/LICENSE.txt ===
##############################################################################
# 
# Zope Public License (ZPL) Version 1.0
# -------------------------------------
# 
# Copyright (c) Digital Creations.  All rights reserved.
# 
# This license has been certified as Open Source(tm).
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
# 
# 1. Redistributions in source code must retain the above copyright
#    notice, this list of conditions, and the following disclaimer.
# 
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions, and the following disclaimer in
#    the documentation and/or other materials provided with the
#    distribution.
# 
# 3. Digital Creations requests that attribution be given to Zope
#    in any manner possible. Zope includes a "Powered by Zope"
#    button that is installed by default. While it is not a license
#    violation to remove this button, it is requested that the
#    attribution remain. A significant investment has been put
#    into Zope, and this effort will continue if the Zope community
#    continues to grow. This is one way to assure that growth.
# 
# 4. All advertising materials and documentation mentioning
#    features derived from or use of this software must display
#    the following acknowledgement:
# 
#      "This product includes software developed by Digital Creations
#      for use in the Z Object Publishing Environment
#      (http://www.zope.org/)."
# 
#    In the event that the product being advertised includes an
#    intact Zope distribution (with copyright and license included)
#    then this clause is waived.
# 
# 5. Names associated with Zope or Digital Creations must not be used to
#    endorse or promote products derived from this software without
#    prior written permission from Digital Creations.
# 
# 6. Modified redistributions of any form whatsoever must retain
#    the following acknowledgment:
# 
#      "This product includes software developed by Digital Creations
#      for use in the Z Object Publishing Environment
#      (http://www.zope.org/)."
# 
#    Intact (re-)distributions of any official Zope release do not
#    require an external acknowledgement.
# 
# 7. Modifications are encouraged but must be packaged separately as
#    patches to official Zope releases.  Distributions that do not
#    clearly separate the patches from the original work must be clearly
#    labeled as unofficial distributions.  Modifications which do not
#    carry the name Zope may be packaged in any form, as long as they
#    conform to all of the clauses above.
# 
# 
# Disclaimer
# 
#   THIS SOFTWARE IS PROVIDED BY DIGITAL CREATIONS ``AS IS'' AND ANY
#   EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
#   IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
#   PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL DIGITAL CREATIONS OR ITS
#   CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
#   SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
#   LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
#   USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
#   ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
#   OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
#   OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
#   SUCH DAMAGE.
# 
# 
# This software consists of contributions made by Digital Creations and
# many individuals on behalf of Digital Creations.  Specific
# attributions are listed in the accompanying credits file.
# 
##############################################################################


=== Added File Products/TrackerBase/Noodging.py === (509/609 lines abridged)
"""Tracker rules processor mixin."""

from types import *
import time, whrandom

import Acquisition
from Persistence import Persistent
from DateTime import DateTime

DEBUG = 0

BIZDAYSTART = 9.0                       # 9:00 am
BIZDAYEND = 18.0                        # 6:00 pm

REGULAR_HOLIDAYS = {'Independence Day': '07/04',
                    'Christmas Day': '12/25',
                    'Thanksgiving': '11/25',
                    'New Years Day': '01/01',
                    'Easter': '04/04',
                    }

RULES_USER = '.rules.'

class TransactionAbort(Exception):
    pass

BadForm = "BadForm"           # String exception, needs to be caught elsewhere.

class Noodge(Acquisition.Implicit,
             Persistent,
             ):
    """Maintain and process the tracker's rule base.

    Rules are applied only to active issues - ie, those that are not
    in the completed, rejected, or deferred stage."""
    def __init__(self, rules=None):
        self.init_expr_tables()
        self.set_rules(rules)

    def set_rules(self, rules):
        self._rules = rules
    def init_expr_tables(self):
        # Map non-builtin conditions to method names.
        self._conditions = {'state': "state_condition",
                            'value': "issue_value",
                            }
        self._actions = {'notify': "notify_action",
                         }

    def execute_rules(self, issues=None):

[-=- -=- -=- 509 lines omitted -=- -=- -=-]

                if extremed == 0:
                    ztime = extremed = (ztime + shift).earliestTime()
                else: ztime = extremed = ztime + shift
            else:
                if extremed == 0:
                    ztime = extremed = (ztime + shift).latestTime()
                else: ztime = ztime + shift

    return ztime

class HoliDatesOff:
    """A cache for "regular" holidays, ie those determined by a month/day.

    '.add()' registers new month/day holidays.

    '.get()' takes a year and returns all the registered holiday dates, as
    DateTime objects, for that year - shifting any that fall on a weekend to
    the nearest subsequent non-holiday weekday.

    The HoliDatesOff class economizes on the DateTime object creation process
    by caching assortments for any encountered year.  The cache is invalidated
    any time a new date is entered."""
    def __init__(self, monthdaylist=[]):
        self._cache = {}                # {year: [dates]}
        self.monthdaylist = []
        self.add(monthdaylist)
    def add(self, dates):
        if type(dates) == type(""):
            dates = [dates]
        mdl = self.monthdaylist
        for i in dates:
            if i in mdl:
                continue
            mdl.append(i)
        self._cache = {}
    def get(self, year):
        year = str(year)
        if self._cache.has_key(year):
            print "from cache"
            return self._cache[year]
        got = []
        for i in self.monthdaylist:
            d = nearest_bizday(DateTime("%s/%s" % (year, i)), 1, got)
            got.append(DateTime(d.Date()))
        got = tuple(got)
        self._cache[year] = got
        return got

def getHolidayRegistry():
    return HoliDatesOff(REGULAR_HOLIDAYS.values())


=== Added File Products/TrackerBase/README.txt ===
ZCatalog

  Notes for Zope 2.0 (this will not work pre Zope 2).

  This is the TrackerBase product for the Zope.  It also contains the
  Tracker ZClass, in Tracker.zexp.

  Installation

    To install the tracker from CVS:

    o Checkout the product into the lib/python/Products folder of your 
      site, and use the Control_Panel "Restart" button to restart your
      Zope site.

      (Note that the 'forms' subdirectory contains all the dtml used
      in the Tracker ZClass - i like to have version control for all
      my code.  Until ZClasses provide that version control - which may
      not be as long as you think - i do it myself.)

    o Create a symlink in your site's Extensions directory (creating the
      dir if necessary) to
      ../lib/python/Products/TrackerBase/TrackerMethods.py

      If you're on a platform without symlinks, copy TrackerMethods.py 
      to the Extensions directory.  (You'll have to remember to update 
      the copies when the original file changes.)

    o Copy Tracker.zexp to your site's import dir (creating the dir if
      necessary)

    o From the Zope Control_Panel/Products folder, import 'Tracker.zexp'.

      (If you're updating an existing tracker install, you'll have to
      delete the Tracker product before doing the import.)

    You should now be able to create and use trackers in your zope.

    Follow the "help" and "details" links from the setup pages for
    help on configuring the tracker, and the TrackerHelp tab from
    anywhere in the tracker for an overview of the system. 

    (See also:

      http://www.zope.org/Members/klm/SoftwareCarpentry -- the
      Software Carpentry tracker submission, including similar help
      documentation, some design specs, a cover letter describing
      desired futures, etc.

      http://www.zope.org/Members/klm/Tracker -- The "Tracker tracker" 
      for bug reports, feature and documentation requests, and so
      forth.  This is the place to go to report tracker bugs - after
      first searching for issues where the bug is already reported, of 
      course...-)


=== Added File Products/TrackerBase/Tracker.zexp === (6078/6178 lines abridged)
ZEXP     
ùí      Ø((UApp.ProductqUProductqtqNt.}q(UidqUTrackerqU__ac_local_roles__q}qUklmq	]q
UOwnerqasU_objectsq(}q
(U	meta_typeqUZ ClassqUidqUTrackerqu}q(hUExternal MethodqhU
addTrackerqu}q(hUDTML MethodqhUaddTrackerFormqu}q(hUZope PermissionqhUaddTrackerPermqu}q(hUZope FactoryqhUfTrackerqu}q(U	meta_typeqUProduct Helpq Uidq!UHelpq"utUtitleq#U h(U     
ùîq$(UApp.Factoryq%UFactoryq&ttQU
addTrackerq'(U     
ùïq((U&Products.ExternalMethod.ExternalMethodq)UExternalMethodq*ttQh(U     
ùðq+(UApp.Permissionq,U
Permissionq-ttQh(U     
ùñq.(UOFS.DTMLMethodq/U
DTMLMethodq0ttQh(U     
ùòq1(UZClasses.ZClassq2UZClassq3ttQh"(U     jÏq4(UHelpSys.HelpSysq5UProductHelpq6ttQu.     
ùî      T((UApp.FactoryqUFactoryqtqNt.}q(UidqUfTrackerqU__ac_local_roles__q}qUklmq	]q
UOwnerqasUinitialqUaddTrackerFormq
U_permissionMapperqcAccessControl.PermissionMapping
PM
qNRq}qU_Use_Factories_PermissionqU_Add_Tracker_PermissionqsbU
permissionqUAdd TrackerqUtitleqUTracker FactoryqUobject_typeqUTrackerqu.     
ùï      ((U&Products.ExternalMethod.ExternalMethodqUExternalMethodqtqNt.}q(U	func_codeq(cApp.Extensions
FuncCode
qoq}q(Uco_varnamesq	(Uselfq
UidqUREQUESTqUinstq
UdestqUtrackerqUuserqUroleqUpsqtqUco_argcountqKubhU
addTrackerqU__ac_local_roles__q}qUklmq]qUOwnerqasUtitleqU U	_functionqU
addTrackerqU
func_defaultsq(NtqU_moduleq UTrackerMethodsq!u.     
ùð       –((UApp.PermissionqU
PermissionqtqNt.}q(UtitleqU UidqUaddTrackerPermqUnameqUAdd Trackerq	U__ac_local_roles__q
}qUklmq]q
UOwnerqasu.     
ùñ      —((UOFS.DTMLMethodqU
DTMLMethodqtqNt.}q(UtitleqU U__name__qUaddTrackerFormqU__ac_local_roles__q}q	Uklmq
]qUOwnerqasUglobalsq
}qUrawqTê  <!--#var standard_html_header-->

<!--#var manage_tabs-->

<h2>Add A Tracker</h2>

<form action="addTracker" method="POST">
    <table border="0">
        <tr>
            <th align="left" valign="top">Id</th>
            <td valign="top"><input type="text" size="40"
            name="id"> </td>
        </tr>
        <tr>
            <th align="left" valign="top">Title</th>
            <td valign="top"><input type="text" size="40"
            name="title"> </td>
        </tr>
        <tr>
            <td>&nbsp;</td>
            <td><br>
            <input type="submit" value="Add ...">

[-=- -=- -=- 6078 lines omitted -=- -=- -=-]

FuncCode
qoq}q(Uco_varnamesq(Uselfq	tq
Uco_argcountqKubUidqUindex_objectq
U__ac_local_roles__q}qUbrianq]qUOwnerqasUtitleqU U	_functionqUindex_objectqU
func_defaultsqNU_moduleqUZopeSitequ.     
ú      ’(cProducts.ExternalMethod.ExternalMethod
ExternalMethod
qNtq.}q(U	func_codeq(cApp.Extensions
FuncCode
qoq}q(Uco_varnamesq(Uselfq	Uftypeq
UfhostqUurlqUtypeq
UuriqUhostqUscript_nameqtqUco_argcountqKubUidqUurlqU__ac_local_roles__q}qUbrianq]qUOwnerqasUtitleqU U	_functionqUurlqU
func_defaultsq(curllib
splittype
qcurllib
splithost
qtq U_moduleq!UZopeSiteq"u.     
ú€      j(cProducts.ExternalMethod.ExternalMethod
ExternalMethod
qNtq.}q(U	func_codeq(cApp.Extensions
FuncCode
qoq}q(Uco_varnamesq(Uselfq	Uitemq
U	containerqUobjectqUstq
Uco_argcountqKubUidqUmanage_beforeDeleteqU__ac_local_roles__q}qUbrianq]qUOwnerqasUtitleqU U	_functionqUmanage_beforeDeleteqU
func_defaultsqNU_moduleqUZopeSitequ.     
ú      G(cOFS.DTMLMethod
DTMLMethod
qNtq.}q(UtitleqU U__name__qUwaaaqUglobalsq}qUrawq	UÚ<!--#var standard_html_header-->
<H2><!--#var title_or_id--> <!--#var document_title--></H2>
<P>This is the <!--#var document_id--> Document in 
the <!--#var title_and_id--> Folder.</P>
<!--#var standard_html_footer-->q
U_varsq}qu.     
ú‚      b(cProducts.ExternalMethod.ExternalMethod
ExternalMethod
qNtq.}q(U	func_codeq(cApp.Extensions
FuncCode
qoq}q(Uco_varnamesq(Uselfq	Uitemq
U	containerqUobjectqUstq
Uco_argcountqKubUidqUmanage_afterAddqU__ac_local_roles__q}qUbrianq]qUOwnerqasUtitleqU U	_functionqUmanage_afterAddqU
func_defaultsqNU_moduleqUZopeSitequ.     
úƒ      >(cProducts.ExternalMethod.ExternalMethod
ExternalMethod
qNtq.}q(U	func_codeq(cApp.Extensions
FuncCode
qoq}q(Uco_varnamesq(Uselfq	tq
Uco_argcountqKubUidqUunindex_objectq
U__ac_local_roles__q}qUbrianq]qUOwnerqasUtitleqU U
func_defaultsqNU	_functionqUunindex_objectqU_moduleqUZopeSitequ.     
ú„      Y(cProducts.ExternalMethod.ExternalMethod
ExternalMethod
qNtq.}q(U	func_codeq(cApp.Extensions
FuncCode
qoq}q(Uco_varnamesq(Uselfq	Uitemq
UobjectqUstqUco_argcountq
KubUidqUmanage_afterCloneqU__ac_local_roles__q}qUbrianq]qUOwnerqasUtitleqU U	_functionqUmanage_afterCloneqU
func_defaultsqNU_moduleqUZopeSitequ.     
ú…      b((U&Products.ExternalMethod.ExternalMethodqUExternalMethodqtqNt.}q(U	func_codeq(cApp.Extensions
FuncCode
qoq}q(Uco_varnamesq	(Uselfq
U
first_onlyqUusersqUuserq
UrolesqtqUco_argcountqKubUidqUcreatorqU__ac_local_roles__q}qUbrianq]qUOwnerqasUtitleqU U	_functionqUcreatorqU
func_defaultsq(K tqU_moduleqUZopeSitequ.ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ

=== Added File Products/TrackerBase/TrackerBase.py ===
"""Issue tracking system implementation classes."""

__version__ = '$Revision: 1.1 $'

import time
import string
from types import *
from string import atoi

from Noodging import Noodge, BadForm, TransactionAbort, age


import Acquisition
import AccessControl.Role
import OFS.SimpleItem
from Globals import HTMLFile, default__class_init__
from Persistence import Persistent
from IOBTree import BTree
    

# Setting to 1 leave an entry in audit history for each rules run.
# Setting to 2 also produces debug flags on stderr.
DEBUG = 0

class TrackerBase(Noodge,
                  Acquisition.Implicit,
                  Persistent,
                  AccessControl.Role.RoleManager,
                  ):
    """Issue tracking system central implementation."""

    meta_class = 'TrackerIssue'

    isPrincipiaFolderish = 1

    __ac_permissions__ = (
        ('Browse',
         ('__getitem__', '__getattr__',
          'owners', 'keys', 'items', 'values', 'has_key',
          'update', 'get', 'length', '__len__', '__del__',
          '__delitem__', '__setitem__',
          ), ['Anonymous']),
        ('Create Issues', ('add_issue'), ['Anonymous']),
        ('Configure Tracker', (), ['TrackerOwner']),
        ('Supporter', (), ['TrackerOwner', 'Supporter']),
        )

    IMATRACKER = 1

    def __init__(self, id):
        __traceback_info__ = ("TrackerBase __init__ in", `id`)
        self.id = id
        self._notifier = Notifier()
        Noodge.__init__(self)
        self._issues = BTree()

    def __repr__(self):
        return ("<%s %s (%d issues) at 0x%s>"
                % (self.__class__.__name__, `self.id`, self.length(),
                   hex(id(self))[2:]))

    def add_issue(self, issue):
        """Add a new issue, and return the generated id."""
        if self._issues:
            id = self._issues.keys()[-1] + 1
        else:
            id = 1
        self._issues[id] = issue
        issue.id = str(id)
        return id

    def keys(self):
        """Return all issue keys, regardless of viewability of value."""
        return self._issues.keys()
    def items(self):
        """Return all keys and issues which we are privileged to view."""
        got = []
        for k, v in self._issues.items():
            got.append((k, v.__of__(self)))
        return got
    def values(self):
        """Return all issues which we are privileged to view."""
        got = []
        for i in self.items():
            got.append(i[1])
        return got
    def has_key(self, id):
        """True if item exists for key, regardless of privilege to view it."""
        if type(id) == StringType:
            try:    id=atoi(id)
            except: raise AttributeError, id
        return self._issues.has_key(id)
    def get(self, id, failobj=None):
        """Like generic sequence get, including failover."""
        if type(id) == StringType:
            try:    id=atoi(id)
            except: raise AttributeError, id
        if self._issues.has_key(id):
            return self._issues[id]
        else:
            return failobj
    def length(self):
        """Number of all issues."""
        return self._issues.__len__()
    def __len__(self):
        """Number of all issues."""
        return self._issues.__len__()
##     def __del__(self):
##         """ """
##         for k in tuple(self.keys()):
##             del self._issues[k]
    def __delitem__(self, id):
        """ """
        if type(id) == StringType:
            try:    id=atoi(id)
            except: raise AttributeError, id
        del self._issues[id]
    def __setitem__(self, id, issue):
        """ """
        __traceback_info__ = ("TrackerBase.__setitem__", `id`, `issue`)
        if type(id) == StringType:
            try:    id=atoi(id)
            except: raise AttributeError, id
        self._issues[id] = issue

    def __getitem__(self, id):
        """ """
        if type(id) == StringType:
            try:    id=atoi(id)
            except: raise AttributeError, id
        __traceback_info__ = ("TrackerBase.__getitem__", `id`)
        return self._issues[id].__of__(self)
    __getattr__ = __getitem__

    def owners(self):
        # Modelled on Extensions/ZopeSite.py:getOwners()
        got = []
        for k, v in self.__ac_local_roles__.items():
            if 'Owner' in v:
                got.append(k)
        return got

default__class_init__(TrackerBase)

class TrackerIssueBase(Acquisition.Implicit,
                       Persistent,
                       AccessControl.Role.RoleManager,
                       ):
    """Implement Issue state-change auditing, item-containment, permissions.

    See audited_state, add_state, register_state_changes for auditing features.

    The ZClass inheritor does the rest..."""

    # If we need to enable access to unprotected attributes.
    __allow_access_to_unprotected_attributes__ = 1

    isPrincipiaFolderish = 1

    IMATRACKERISSUE = 1

    meta_class = 'TrackerIssueBase'

    # XXX Permissions are going to have to be worked out!
    __ac_permissions__ = (
        ('Public Issue Interaction',
         ['add_item', '__getitem__', '__getattr__',
          'owners', 'keys', 'items', 'values',
          'has_key', 'update', 'get', 'length', 'length', '__len__',
          '__delitem__', '__setitem__', 'history', 'numid',
          'add_state', 'register_state_changes', 'statecond',
          'promotes',  # XXX XXX Expediency, permissions broke
          ], ['Anonymous']),
        ('Privileged Issue Interaction',
         [],                            # ['promotes'],
         ['Requester', 'IssueOwner', 'Supporter', 'TrackerOwner'],
         ),
        ('Tracker Staff',
         [],                            # ['promotes'],
         ['IssueOwner', 'Supporter', 'TrackerOwner'],
         ),
        ('Issue Overrides',
         [],
         ['IssueOwner', 'TrackerOwner'],
         )
        )

    def __init__(self, REQUEST=None):
        self.id = (REQUEST and REQUEST.get('id', '')) or ''
        self._messages = BTree()
        self._promoted = []
        self.audited_state = AuditedState()
        self.state_tick = 1

    def __repr__(self):
        return ("<%s %s (%d messages, %d promoted) at 0x%s>"
                % (self.__class__.__name__, `self.id`,
                   self.length(), len(self.promotes()),
                   hex(id(self))[2:]))

    def add_item(self, item, user):
        """Add a new item, returning generated id and item acquired."""
        items = self._messages
        if items:
            id = items.keys()[-1] + 1
        else:
            id = 1
        item.id = str(id)
        items[id] = item
        self.audited_state.set('newitem', id, user)
        return (id, item.__of__(self))

    def numid(self):
        """Return the numeric version of the id."""
        if type(self.id) == StringType:
            return atoi(self.id)
        else:
            return self.id

    def promotes(self, itemid=None, user=None, demote=None):
        """Return list of promoted items, or promote/demote one.

        Without an itemid, return the list of promoted items.  With just
        the itemid and user, add the itemid to the list - or remove it, if
        demote is set."""

        if itemid is None:
            return self._promoted[:]

        if type(itemid) == StringType:
            try:    itemid=atoi(itemid)
            except: raise AttributeError, itemid

        if demote is None:
            pubs = self._promoted
            if itemid not in pubs:
                pubs.append(itemid)
                self._promoted = pubs
            self.audited_state.set('promote', itemid, user)
        else:
            pubs = self._promoted
            if itemid in pubs:
                pubs.remove(itemid)
                self._promoted = pubs
            self.audited_state.set('demote', itemid, user)

    def execute_rules(self):
        """Invoke tracker rules on just this issue."""
        return self.aq_parent.execute_rules(issues=[self])

    def statecond(self, var=None, value=None, priorcount=None):
        """Expose Noodge.state_condition() method to dtml."""
        return self.state_condition(self, var=var, value=value,
                                    priorcount=priorcount)

    def tick(self):
        """Tick used to identify "volatile" settings of current transaction."""
        return self.state_tick

    def increment_tick(self):
        """Increment ticker by which audited state volitility is recognized."""
        self.state_tick = self.state_tick + 1

    def add_state(self, additions, REQUEST=None):
        """Institute the state settings that are to be audited."""
        # Create the audit trail:
        user = (REQUEST and `REQUEST.AUTHENTICATED_USER`) or ''
        setstate = self.audited_state.set
        for varname, value in additions:
            setstate(varname, value, user)

    def register_state_changes(self, REQUEST=None):
        """Check for tracked-attribute changes and register any found.

        Call this whenever the state of the issue has changed."""
        state = self.audited_state
        user = (REQUEST and `REQUEST.AUTHENTICATED_USER`) or ''
        for a, v in state.items():
            if not hasattr(self, a):
                # Event state vars, like 'newitem', are ephemeral...
                continue
            current = getattr(self, a)
            if current != v:
                state.set(a, current, user)
        return self.execute_rules()

    def history(self):
        """History of audited items."""
        return self.audited_state.history()

    def __getitem__(self, id):
        """ """
        if type(id) == StringType:
            try:    id=atoi(id)
            except: raise AttributeError, id
        return self._messages[id].__of__(self)
    __getattr__ = __getitem__

    def keys(self, reverse=0):
        """ """
        got = list(self._messages.keys())
        if reverse: got.reverse()
        return got
    def items(self, reverse=0):
        """Return all keys and issues which we are privileged to view."""
        items = self._messages
        got = []
        for k, v in items.items():
            got.append((k, v.__of__(self)))
        if reverse:
            got.reverse()
        return got
    def values(self, reverse=0):
        """Return all items which we are privileged to view."""
        got = []
        for i in self.items(reverse=reverse):
            got.append(i[1])
        return got
    def has_key(self, id):
        """True if item exists for key, regardless of privilege to view it."""
        if type(id) == StringType:
            try:    id=atoi(id)
            except: raise AttributeError, id
        return self._messages.has_key(id)
    def get(self, id, failobj=None):
        """Like generic sequence get, including failover."""
        if type(id) == StringType:
            try:    id=atoi(id)
            except: raise AttributeError, id
        items = self._messages
        if items.has_key(id):
            return items[id].__of__(self)
        else:
            return failobj
    def length(self):
        """Number of message items."""
        return self._messages.__len__()
    def __len__(self):
        """Number of message items."""
        return self._messages.__len__()

##     def __del__(self):
##         """ """
##         for k in tuple(self._messages.keys()):
##             del self._messages[k]
##         # XXX any other cleanup...

    def __delitem__(self, id):
        """ """
        # XXX is there any special stuff to do for ZClass deletion?
        if type(id) == StringType:
            try:    id=atoi(id)
            except: raise AttributeError, id
        del self._messages[id]
    def __setitem__(self, id, issue):
        """ """
        __traceback_info__ = ("TrackerIssueBase.__setitem__", `id`, `issue`)
        if type(id) == StringType:
            try:    id=atoi(id)
            except: raise AttributeError, id
        self._messages[id] = issue

    def __getitem__(self, id):
        """ """
        if type(id) == StringType:
            try:    id=atoi(id)
            except: raise AttributeError, id
        __traceback_info__ = ("TrackerIssueBase.__getitem__", `id`)
        return self._messages[id].__of__(self)

    def owners(self):
        lr = self.__ac_local_roles__ or {}
        got = []
        for k, v in lr.items():
            if 'TrackerOwner' in v:
                got.append(k)
        return got
default__class_init__(TrackerIssueBase)

class AuditedState(Acquisition.Explicit):

    # If we need to enable access to unprotected attributes.
    __allow_access_to_unprotected_subobjects__ = 1

    def __init__(self):
        self._state = {}
        self._hist = []

    def set(self, varname, value, user):
        """Set current state and record old value in history."""
        self._state[varname] = value
        # self.tick() is got by acquisition.
        self._hist.insert(0, (varname, value, user, time.time(),
                              self.aq_parent.tick()))

    def get(self, varname, failobj=None):
        if not self._state.has_key(varname):
            return failobj
        return self._state[varname]

    def keys(self):
        return self._state.keys()
    def items(self):
        return self._state.items()

    def in_state(self, varname, value):
        return value == self.get(varname)
            
    def age(self):
        if not self._hist:
            return 0
        return age(self._hist[-1][-2], business=1)

    def history(self,
                varname=None, value=None, user=None, max=None, after=None,
                tick=None):
        """Return list of (name, val, oldval, user, time, tick) tuples.

        Order is more recent entries first.

        Optional parameters constrain the search.  varname, value, and user
        constrain entries to ones that contain that field.  (value and user
        can be lists of eligible candidates.) max specifies an upper limit
        to the number of entries returned.  after specifies a cutoff
        time.time(), entries logged before that time are excluded."""

        if varname != None and not self._state.has_key(varname):
            raise AttributeError, "Undefined varname '%s'" % varname
        got = []
        if varname == value == max == after == tick == None:
            __traceback_info__ = (`self`,)
            return self._hist[:]
        if value is not None and type(value) == StringType:
            user = [user]
        if user is not None and type(user) == StringType:
            user = [user]
        for n, v, u, t, i in self._hist:
            if after and t < after:
                break
            if varname and (n != varname):
                continue
            if value and (v not in value):
                continue
            if user and (u not in user):
                continue
            if tick:
                if i != tick:
                    return []
                return [(n, v, u, t, i)]
            got.append((n, v, u, t, i))
            if len(got) == max:
                break
        return got

class Notifier:
    def notify(self, issue, who, subj, msg):
        if DEBUG >= 2:
            Note("** Issue notify: %s, Notify: to: %s, subj:%s\nmsg: %s\n",
                 issue, who, subj, msg)

def sample():
    """Evolving test that returns a populated tracker."""
    tracker = TrackerBase('sample')
    tracker.abbrev = 'SAMP'
    issue = TrackerIssueBase()
    i = tracker.add_issue(issue)
    issue = tracker[i]
    audit = issue.audited_state
    user = 'test'
    class PseudoItem(Acquisition.Implicit):
        def __init__(self, title="<No title>", description="<No description>"):
            self.title = title
            self.description = description
            self.IMATRACKERITEM = 1
    item = PseudoItem("sample title", "sample description")
    issue.add_item(item, user)
    item = issue[1]
    additions = [('newitem', '0'),
                 ('stage', 'pending'),
                 ('punctuality', 'ok'),
                 ('correspondence', 'caught-up'),
                 ('priority', 'normal'),
                 ('urgency', 'normal'),
                 ('window', 'soon'),
                 ]
    class x:
        AUTHENTICATED_USER = 'test'
    issue.add_state(additions, x())
    return tracker, issue, item


=== Added File Products/TrackerBase/TrackerMethods.py === (2193/2293 lines abridged)
"""External methods for Tracker-related ZClasses and Products."""

#import Products.TrackerBase.TrackerMisc # DEBUG
#reload(Products.TrackerBase.TrackerMisc) # DEBUG

from Products.TrackerBase.TrackerMisc import *

import OFS.Image

import sys, time

TRACKER_DATA_VERSION = 1
ISSUE_DATA_VERSION = 1
ITEM_DATA_VERSION = 1

def addTracker(self, id, REQUEST=None):
    """ """
    # self = Factory - actually, FactoryDispatcher
    # Destination() = the folder in which the obj is being created.
    if not id:
        raise ValueError, "You must specify an id."
    inst = self.Tracker(id)

    dest = self.Destination()
    dest._setObject(id, inst)
    tracker = getattr(dest, id)
    tracker.DATA_VERSION = TRACKER_DATA_VERSION

    user = (REQUEST and `REQUEST.AUTHENTICATED_USER`) or ''
    tracker.manage_addLocalRoles(user, ['TrackerOwner'])

    for role in unique(ROLE_CATEGORIES['open']
                       + ROLE_CATEGORIES['dedicated']):
        if not tracker._has_user_defined_role(role):
            tracker._addRole(role)

    setTrackerRules(tracker)

    tracker.smtphost = socket.gethostname()
    tracker.smtpport = 25

    for ps in tracker.propertysheets:
        ps.manage_changeProperties(REQUEST)

    tracker.supporters = []
    tracker.clients = []
    tracker.peers = []
    tracker.open_subscribe = tracker.default_open_subscribe

    trackerInitCatalog(tracker)

[-=- -=- -=- 2193 lines omitted -=- -=- -=-]

##                 (INCLUDE, 'overdue', 'ownership')))),

        (   # Clear up ownership pending state when suitable.
            (CONDITION, (NOT, (STATE, 'stage', 'pending'))),
            ( (CONDITION, (CONTAINS, 'due', 'ownership')),
              (ACTIONS, (REMOVE, 'due', 'ownership'))),
            ( (CONDITION, (CONTAINS, 'overdue', 'ownership')),
              (ACTIONS, (REMOVE, 'overdue', 'ownership')))),

        (   # Clear up rsvp pending state when suitable.
            (CONDITION, (AND, (OR, (CONTAINS, 'due', 'rsvp'),
                                   (CONTAINS, 'overdue', 'rsvp')),
                              (NOT, (STATE, 'msgfromrole', 'Requester', 0)))),
            ( (CONDITION, (CONTAINS, 'due', 'rsvp')),
              (ACTIONS, (REMOVE, 'due', 'rsvp'))),
            ( (CONDITION, (CONTAINS, 'overdue', 'rsvp')),
              (ACTIONS, (REMOVE, 'overdue', 'rsvp')))),

        (   # Alert to languishing reply.
            (CONDITION,
             # The last correspondence is from the requester:
             (AND,
              (OR, ('<',
                    (STATE, 'msgfromrole', ['Requester', 'TrackerOwner'], 0),
                    (STATE, 'msgfromrole', 'Supporter', 0)),
               ('>', (STATE, 'msgfromrole', 'Requester', 0), 3*ADAY)))),
             # Send daily alerts to issue owner, after initial grace period:
            ( (CONDITION, (OR,
                             (NOT, (STATE, 'alert', 'rsvp due', 0)),
                             ('>', (STATE, 'alert', 'rsvp due', 0), 1*ADAY))),
              (ACTIONS,
               (SET, 'alert', 'rsvp due'),
               (NOTIFY, 'IssueOwner',
                "Issue %[var title]s %[var id]s correspondence pending",
                "Issue %[var issueIdentity]s requester is owed"
                " correspondence.")
               )),
            # This has been going on too long - alert the tracker owner once:
            ( (CONDITION, (AND,
                            ('>', (STATE, 'alert', 'rsvp due', 0), 7*ADAY),
                            (NOT, (STATE, 'alert', 'rsvp overdue', 0)))),
              (ACTIONS,
               (SET, 'alert', 'rsvp overdue'),
               (NOTIFY, 'TrackerOwner',
                "Issue %[var title]s %[var id]s correspondence WAY pending",
                "Issue %[var issueIdentity]s requester has been owed"
                " correspondence for more than 7 days."))))
        ]
    return standard



=== Added File Products/TrackerBase/TrackerMisc.py === (697/797 lines abridged)
# General stuff ==========================================================
        
import string
from DocumentTemplate.html_quote import html_quote
import smtplib, socket
from DateTime import DateTime
import urllib

from AccessControl.PermissionRole import rolesForPermissionOn
from Products.ZCatalog.ZCatalog import Catalog

RELEVANCE_DEBUG = 1
RULES_DEBUG = 0
EMBLEM_DEBUG = 1

# Privacy stuff ==========================================================
        
# Role categories - map abstract tracker role names to tracker-specific zope
# security roles.  Use tracker_role() to do the translation from tracker dtml.
INVOLVED_ROLES = ['IssueOwner', 'Requester', 'TrackerOwner']
STAFF_ROLES = ['TrackerOwner', 'IssueOwner', 'Supporter', 'Researcher']
DEDICATED_ROLES = STAFF_ROLES + ['TrackerClient', 'Subscriber', 'Requester']
OPEN_ROLES = DEDICATED_ROLES + ['Anonymous', 'Member']
# XXX SupporterHelp would be a good place for exposition about the categories.
#     Some details would be good for TrackerElementsHelp, as well - see
#     set_privacy for related notes.
ROLE_CATEGORIES = {
    # staff: View any items, become issue owner, and do other state changes.
    #        Not shown other-correspondence items, but has access to them,
    #        including private ones.
    'staff': STAFF_ROLES,
    # observer: View correspondence beyond summary, private issues.  Does not 
    #           enable followup privileges or access to private items.
    'observer': (['Subscriber'] + INVOLVED_ROLES),
    # involved: Like observer but can submit followups.
    'involved': INVOLVED_ROLES,
    # dedicated: Can enter dedicated tracker.
    'dedicated': DEDICATED_ROLES,
    # open: Can enter open tracker.
    'open': OPEN_ROLES,
                   }

def elementEmblem(self, issue_id=None, item_id=None, nolink=0, complete=0,
                  forcelink=0, REQUEST=None):
    """Return a terse link specifying a particular issue, item, or tracker.

    If REQUEST.AUTHENTICATED_USER is not authorized to view the target element
    (or no REQUEST was passed in), then only the textual, unlinked version of
    the emblem is returned.  This serves as the cue that the element is
    unviewable, and prevents the user from trying to prevent the element and

[-=- -=- -=- 697 lines omitted -=- -=- -=-]

    else:
        return (text[:prior], text[prior+1:])
    
def pedigree(self):
    """Investigation utility showing the base class hierachy of a class."""
    __traceback_info__ = (`self`)
    structure = pedigree_structure(self.__class__)
    got = indented(structure, depth=0)
    return ("<!--#var standard_html_header-->"
            + ("<h3>%s Instance Inherited Classes</h3>\n" % got[0])
            + "\n<em>('...' abbreviates already elaborated classes)</em>"
            + "\n<pre>\n" + string.join(got[1:], '\n') + "\n</pre>\n"
            + "<!--#var standard_html_footer-->")
    
def indented(nesting, depth=0):
    """Indicate list nesting with indentation instead of containment.

    Ellipses appreviate already elaborated classes, and single-element
    inheritance is shown on the same line as the inheritor."""
    got = []
    for i in nesting:
        if type(i) == type(''):
            got.append((depth * '  ') + i)
##         elif len(i) == 2 and len(i[1]) == 1:
##             got.append((depth+1) * '  ' + "%s %s" % (i[0], i[1][0]))
        else:
            got = got + indented(i, depth + 1)
    return got

def pedigree_structure(klass, already_seen=None):
    """Return nested lists reflecting inheritance structure of self."""
    __traceback_info__ = (`klass`)
    bases = klass.__bases__
    seen = ""
    if already_seen is None:
        already_seen = []
    elif bases and klass in already_seen:
        seen = " ..."

    if hasattr(klass, '__module__') and klass.__module__:
        got = ["%s (%s)%s" % (klass.__name__, klass.__module__, seen)]
    else:
        got = [klass.__name__ + seen]

    if not seen:
        already_seen.append(klass)
        for i in bases:
            got.append(pedigree_structure(i, already_seen))

    return got


=== Added File Products/TrackerBase/__init__.py ===
"""Tracker base-objects product."""

__version__='$Revision: 1.1 $'[11:-2]

import TrackerBase

def initialize(context):

##     perm='Add Whatever'

    context.registerBaseClass(TrackerBase.TrackerBase, "TrackerBase")
    context.registerBaseClass(TrackerBase.TrackerIssueBase,
                              "TrackerIssueBase")
##    context.registerBaseClass(TrackerBase.TrackerItemBase)


=== Added File Products/TrackerBase/addTrackerForm.dtml ===
<!--#var standard_html_header-->

<!--#var manage_tabs-->

<h2>Add A Tracker</h2>

<form action="addTracker" method="POST">
    <table border="0">
        <tr>
            <th align="left" valign="top">Id</th>
            <td valign="top"><input type="text" size="40"
            name="id"> </td>
        </tr>
        <tr>
            <th align="left" valign="top">Title</th>
            <td valign="top"><input type="text" size="40"
            name="title"> </td>
        </tr>
        <tr>
            <td>&nbsp;</td>
            <td><br>
            <input type="submit" value="Add ...">
    <font color="gray"> ... three configuration steps remaining ... </font>
	  </td>
        </tr>
    </table>
</form>
<!--#var standard_html_footer-->


=== Added File Products/TrackerBase/age.py ===
from DateTime import DateTime

BIZDAYSTART = 9.0                       # 9:00 am
BIZDAYEND = 18.0                        # 6:00 pm

def age(since, until=None, business=0, holidates=None,
        bizdaystart=BIZDAYSTART, bizdayend=BIZDAYEND):
    """Return age in decimal (float) number of days.

    Optional end time 'until', defaults to current moment.

    Optional flag 'business' means compute according to business time
    - excluding non-business hours, weekends, and specified holidays.

    Optional holidates is a list of DateTimes.Dates() format dates
    designating holidays to be excluded from business hours.
    \(Holidates falling on a weekend day are shifted to next
    non-exempted weekday before being exempted.)

    Optional bizdaystart is the start time of a business day, expressed
    *as a decimal number*: the integral hour, then the decimal portion
    thereof.

    Optional bizdayend is the companion day-end time for bizdaystart."""

    now = DateTime()
    if type(since) != type(now): since = DateTime(since)
    if not until: until = now
    elif type(until) != type(now): until = DateTime(until)

    if not business:
        return until - since

    # Business-hours computation onwards.

    # factor to normalize number of business hours/day:
    daylenfactor = 24/(bizdayend - bizdaystart)
    if daylenfactor < 0:
        raise 'DateTimeError', ('business day end (%s) is before start (%s)?? '
                                % (bizdayend, bizdaystart))

    # Shift the endpoints to nearest contained business days:
    since = nearest_bizday(since, forwards=1, holidates=holidates)
    until = nearest_bizday(until, forwards=0, holidates=holidates)
    if not since.lessThan(until):
        return 0
    # Nail down the amount of time in the head & tail days:
    head = tail = 0
    sincetod = since - since.earliestTime()
    if sincetod < (bizdaystart/24):
        # Increment the time to the start of the business day:
        since = since + ((bizdaystart/24) - sincetod)
        sincetod = since - since.earliestTime()
    untiltod = until - until.earliestTime()
    if untiltod > (bizdayend/24):
        # Decrement the time to the end of the business day:
        until = until - (untiltod - (bizdayend/24))
        untiltod = until - until.earliestTime()
    if not since.lessThan(until):
        return 0
    if since.Date() == until.Date():
        return (until - since) * daylenfactor
    else:
        subtotal = (((bizdayend/24 - sincetod) + (untiltod - bizdaystart/24))
                    * daylenfactor)

    # Ok, now we just need to deal with intervening days.
    since = since.latestTime()
    until = until.earliestTime()
    
    # Num of intervening days, sans head/tail
    interdays = int(until - since)

    if interdays == 0:
        return subtotal

    # Omit saturdays and sundays.
    dow_count = since.dow() + interdays
    weekend_days = dow_count/7 + (dow_count+1)/7
    interdays = interdays - weekend_days

    if holidates:
        # Omit intervening holidays.
        # Increment to the first non-weekend, non-considered day if necessary.
        exempted = []
        for htime in holidates:
            if since <= htime <= until:
                interdays = interdays - 1
                if interdays <= 0:
                    return subtotal

    return interdays + subtotal
    
def nearest_bizday(ztime, forwards, holidates):
    """Return the nearest business day to specified time.

    If we must shift forward, return the beginning of the soonest day.
    If backward, return the end of the most recent day.

    holidates are DateTime objects representing holiday dates to exclude."""

    # Each time around we concentrate on *either* the weekend *or* holiday...
    doingweekend = 0
    # ... so we can process the shifts consistently at the bottom.  Thus
    # we retraverse the loop after any shift to assess the newly found date.
    shift = 1
    # We only need to do the (relatively expensive) earliest/latest shifts
    # the first time - 'extremed' marks that. 
    extremed = 0
    while shift:
        shift = 0

        if not doingweekend:
            if ztime.dow() == 6:
                if forwards: shift = 2
                else: shift = -1
                doingweekend = 1
            elif ztime.dow() == 0:
                if forwards: shift = 1
                else: shift = -2
                doingweekend = 1
        else:
            doingweekend = 0

        if not doingweekend and holidates:
            thisDate = ztime.Date()
            for h in holidates:
                if thisDate == h.Date():
                    if forwards: shift = 1
                    else: shift = -1
        
        if shift:
            if shift > 0:
                if extremed == 0:
                    ztime = extremed = (ztime + shift).earliestTime()
                else: ztime = extremed = ztime + shift
            else:
                if extremed == 0:
                    ztime = extremed = (ztime + shift).latestTime()
                else: ztime = ztime + shift

    return ztime

class HoliDatesOff:
    """A cache for "regular" holidays, ie those determined by a month/day.

    '.add()' registers new month/day holidays.

    '.get()' takes a year and returns all the registered holiday dates, as
    DateTime objects, for that year - shifting any that fall on a weekend to
    the nearest subsequent non-holiday weekday.

    The HoliDatesOff class economizes on the DateTime object creation process
    by caching assortments for any encountered year.  The cache is invalidated
    any time a new date is entered."""
    def __init__(self, monthdaylist=[]):
        self._cache = {}                # {year: [dates]}
        self.monthdaylist = []
        self.add(monthdaylist)
    def add(self, dates):
        if type(dates) == type(""):
            dates = [dates]
        mdl = self.monthdaylist
        for i in dates:
            if i in mdl:
                continue
            mdl.append(i)
        self._cache = {}
    def get(self, year):
        year = str(year)
        if self._cache.has_key(year):
            print "from cache"
            return self._cache[year]
        got = []
        for i in self.monthdaylist:
            d = nearest_bizday(DateTime("%s/%s" % (year, i)), 1, got)
            got.append(DateTime(d.Date()))
        got = tuple(got)
        self._cache[year] = got
        return got

REGULAR_HOLIDAYS = {'Independence Day': '07/04',
                    'Christmas Day': '12/25',
                    'Thanksgiving': '11/25',
                    'New Years Day': '01/01',
                    'Easter': '04/04',
                    }

def getHolidayRegistry():
    return HoliDatesOff(REGULAR_HOLIDAYS.values())


=== Added File Products/TrackerBase/envsetup.py ===
"""Command sequence to prime for interaction with a sample tracker."""

try:
    envsetup
    envsetup.seek(0)
except NameError:
    envsetup = open('./envsetup.py', 'r')

import sys
sys.path.insert(0, '.')
sys.path.insert(0, '../..')
import ZODB
import TrackerBase, TrackerMethods, Noodging   # or
reload(TrackerBase); reload(TrackerMethods); reload(Noodging)
tracker, issue, item = TrackerBase.sample();
rules = TrackerMethods.setTrackerRules(tracker)
state = issue.audited_state
print "tracker, issue, item, rules, and state prepared"

print "(exec(envsetup.read()) to reinit env)"


=== Added File Products/TrackerBase/issue.gif ===
  <Binary-ish file>

=== Added File Products/TrackerBase/item.gif ===
  <Binary-ish file>

=== Added File Products/TrackerBase/tracks.gif ===
  <Binary-ish file>




More information about the zopeorg-checkins mailing list