[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(hUZope FactoryqhUfTrackerqu}q(U meta_typeqUProduct 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
UOwnerqasUinitialqUaddTrackerFormq
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
UidqUREQUESTqUinstq
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
]qUOwnerqasUglobalsq
}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> </td>
<td><br>
<input type="submit" value="Add ...">
[-=- -=- -=- 6078 lines omitted -=- -=- -=-]
FuncCode
qoq}q(Uco_varnamesq(Uselfq tq
Uco_argcountqKubUidqUindex_objectq
U__ac_local_roles__q}qUbrianq]qUOwnerqasUtitleqU U _functionqUindex_objectqU
func_defaultsqNU_moduleqUZopeSitequ.
ú (cProducts.ExternalMethod.ExternalMethod
ExternalMethod
qNtq.}q(U func_codeq(cApp.Extensions
FuncCode
qoq}q(Uco_varnamesq(Uselfq Uftypeq
UfhostqUurlqUtypeq
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 containerqUobjectqUstq
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}qu.
ú b(cProducts.ExternalMethod.ExternalMethod
ExternalMethod
qNtq.}q(U func_codeq(cApp.Extensions
FuncCode
qoq}q(Uco_varnamesq(Uselfq Uitemq
U containerqUobjectqUstq
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_argcountqKubUidqUunindex_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
UobjectqUstqUco_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_onlyqUusersqUuserq
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> </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