summary | shortlog | log | commit | commitdiff | tree
raw | patch | inline | side by side (parent: 84e6836)
raw | patch | inline | side by side (parent: 84e6836)
author | neaj <neaj@57a73879-2fb5-44c3-a270-3262357dd7e2> | |
Tue, 24 Jun 2003 12:39:20 +0000 (12:39 +0000) | ||
committer | neaj <neaj@57a73879-2fb5-44c3-a270-3262357dd7e2> | |
Tue, 24 Jun 2003 12:39:20 +0000 (12:39 +0000) |
Documented 'db.curuserid'.
doc/design.txt
Reflowed to 72 columns (even the layer cake fits :)
roundup/mailgw.py
Strip '\n' introduced by rfc822.readheaders
git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@1778 57a73879-2fb5-44c3-a270-3262357dd7e2
doc/design.txt
Reflowed to 72 columns (even the layer cake fits :)
roundup/mailgw.py
Strip '\n' introduced by rfc822.readheaders
git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@1778 57a73879-2fb5-44c3-a270-3262357dd7e2
doc/customizing.txt | patch | blob | history | |
doc/design.txt | patch | blob | history | |
roundup/mailgw.py | patch | blob | history |
diff --git a/doc/customizing.txt b/doc/customizing.txt
index 36712dedd5f485be4bf157b1dd952976a8c5581b..245ca2a7d517ebdc4a4e32dd8c775818e79ff059 100644 (file)
--- a/doc/customizing.txt
+++ b/doc/customizing.txt
Customising Roundup
===================
-:Version: $Revision: 1.91 $
+:Version: $Revision: 1.92 $
.. This document borrows from the ZopeBook section on ZPT. The original is at:
http://www.zope.org/Documentation/Books/ZopeBook/current/ZPT.stx
db/user
python:db.user
+Also, the current id of the current user is available as
+``db.curuserid``. This isn't so useful in templates (where you have
+``request/user``), but it can be useful in detectors or interfaces.
+
The access results in a `hyperdb class wrapper`_.
diff --git a/doc/design.txt b/doc/design.txt
index 67f82f1bdeb9cba5e1c2c4d339582c286c8e40ce..5253184f9905fbee956df38d5362a35c2786342a 100644 (file)
--- a/doc/design.txt
+++ b/doc/design.txt
Everybody seems to like them. I also like cakes (i think they are
tasty). So I, too, shall include a picture of a cake here::
- _________________________________________________________________________
- | E-mail Client | Web Browser | Detector Scripts | Shell |
- |------------------+-----------------+----------------------+-------------|
- | E-mail User | Web User | Detector | Command |
- |-------------------------------------------------------------------------|
- | Roundup Database Layer |
- |-------------------------------------------------------------------------|
- | Hyperdatabase Layer |
- |-------------------------------------------------------------------------|
- | Storage Layer |
- -------------------------------------------------------------------------
+ ________________________________________________________________
+ | E-mail Client | Web Browser | Detector Scripts | Shell |
+ |---------------+---------------+--------------------+-----------|
+ | E-mail User | Web User | Detector | Command |
+ |----------------------------------------------------------------|
+ | Roundup Database Layer |
+ |----------------------------------------------------------------|
+ | Hyperdatabase Layer |
+ |----------------------------------------------------------------|
+ | Storage Layer |
+ ----------------------------------------------------------------
The colourful parts of the cake are part of our system; the faint grey
parts of the cake are external components.
class Date:
def __init__(self, spec, offset):
- """Construct a date given a specification and a time zone offset.
+ """Construct a date given a specification and a time zone
+ offset.
'spec' is a full date or a partial form, with an optional
added or subtracted interval. 'offset' is the local time
"""Add an interval to this date to produce another date."""
def __sub__(self, interval):
- """Subtract an interval from this date to produce another date."""
+ """Subtract an interval from this date to produce another
+ date.
+ """
def __cmp__(self, other):
"""Compare this date to another date."""
def __str__(self):
- """Return this date as a string in the yyyy-mm-dd.hh:mm:ss format."""
+ """Return this date as a string in the yyyy-mm-dd.hh:mm:ss
+ format.
+ """
def local(self, offset):
- """Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone."""
+ """Return this date as yyyy-mm-dd.hh:mm:ss in a local time
+ zone.
+ """
class Interval:
def __init__(self, spec):
>>> Date(". + 2d") - Interval("3w")
<Date 2000-06-07.00:34:02>
+
Items and Classes
~~~~~~~~~~~~~~~~~
class which defines the names and types of its properties. The database
permits the creation and modification of classes as well as items.
+
Identifiers and Designators
~~~~~~~~~~~~~~~~~~~~~~~~~~~
For example, if "spam" and "eggs" are classes, the first item created in
class "spam" has id 1 and designator "spam1". The first item created in
-class "eggs" also has id 1 but has the distinct designator "eggs1".
-Item designators are conventionally enclosed in square brackets when
+class "eggs" also has id 1 but has the distinct designator "eggs1". Item
+designators are conventionally enclosed in square brackets when
mentioned in plain text. This permits a casual mention of, say,
"[patch37]" in an e-mail message to be turned into an active hyperlink.
+
Property Names and Types
~~~~~~~~~~~~~~~~~~~~~~~~
A property that is not specified will return as None from a *get*
operation.
+
Hyperdb Interface Specification
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Here is the interface provided by the hyperdatabase::
class Database:
- """A database for storing records containing flexible data types."""
+ """A database for storing records containing flexible data
+ types.
+ """
def __init__(self, config, journaltag=None):
"""Open a hyperdatabase given a specifier to some storage.
def getclass(self, classname):
"""Get the Class object representing a particular class.
- If 'classname' is not a valid class name, a KeyError is raised.
+ If 'classname' is not a valid class name, a KeyError is
+ raised.
"""
class Class:
- """The handle to a particular class of items in a hyperdatabase."""
+ """The handle to a particular class of items in a hyperdatabase.
+ """
def __init__(self, db, classname, **properties):
- """Create a new class with a given name and property specification.
+ """Create a new class with a given name and property
+ specification.
+
+ 'classname' must not collide with the name of an existing
+ class, or a ValueError is raised. The keyword arguments in
+ 'properties' must map names to property objects, or a
+ TypeError is raised.
+
+ A proxied reference to the database is available as the
+ 'db' attribute on instances. For example, in
+ 'IssueClass.send_message', the following is used to lookup
+ users, messages and files::
- 'classname' must not collide with the name of an existing class,
- or a ValueError is raised. The keyword arguments in 'properties'
- must map names to property objects, or a TypeError is raised.
+ users = self.db.user
+ messages = self.db.msg
+ files = self.db.file
+
+ The id of the current user is also available on the database
+ as 'self.db.curuserid'.
"""
# Editing items:
def create(self, **propvalues):
"""Create a new item of this class and return its id.
- The keyword arguments in 'propvalues' map property names to values.
- The values of arguments must be acceptable for the types of their
- corresponding properties or a TypeError is raised. If this class
- has a key property, it must be present and its value must not
- collide with other key strings or a ValueError is raised. Any other
- properties on this class that are missing from the 'propvalues'
- dictionary are set to None. If an id in a link or multilink
- property does not refer to a valid item, an IndexError is raised.
+ The keyword arguments in 'propvalues' map property names to
+ values. The values of arguments must be acceptable for the
+ types of their corresponding properties or a TypeError is
+ raised. If this class has a key property, it must be
+ present and its value must not collide with other key
+ strings or a ValueError is raised. Any other properties on
+ this class that are missing from the 'propvalues' dictionary
+ are set to None. If an id in a link or multilink property
+ does not refer to a valid item, an IndexError is raised.
"""
def get(self, itemid, propname):
- """Get the value of a property on an existing item of this class.
+ """Get the value of a property on an existing item of this
+ class.
- 'itemid' must be the id of an existing item of this class or an
- IndexError is raised. 'propname' must be the name of a property
- of this class or a KeyError is raised.
+ 'itemid' must be the id of an existing item of this class or
+ an IndexError is raised. 'propname' must be the name of a
+ property of this class or a KeyError is raised.
"""
def set(self, itemid, **propvalues):
"""Modify a property on an existing item of this class.
- 'itemid' must be the id of an existing item of this class or an
- IndexError is raised. Each key in 'propvalues' must be the name
- of a property of this class or a KeyError is raised. All values
- in 'propvalues' must be acceptable types for their corresponding
- properties or a TypeError is raised. If the value of the key
- property is set, it must not collide with other key strings or a
- ValueError is raised. If the value of a Link or Multilink
- property contains an invalid item id, a ValueError is raised.
+ 'itemid' must be the id of an existing item of this class or
+ an IndexError is raised. Each key in 'propvalues' must be
+ the name of a property of this class or a KeyError is
+ raised. All values in 'propvalues' must be acceptable types
+ for their corresponding properties or a TypeError is raised.
+ If the value of the key property is set, it must not collide
+ with other key strings or a ValueError is raised. If the
+ value of a Link or Multilink property contains an invalid
+ item id, a ValueError is raised.
"""
def retire(self, itemid):
"""Retire an item.
- The properties on the item remain available from the get() method,
- and the item's id is never reused. Retired items are not returned
- by the find(), list(), or lookup() methods, and other items may
- reuse the values of their key properties.
+ The properties on the item remain available from the get()
+ method, and the item's id is never reused. Retired items
+ are not returned by the find(), list(), or lookup() methods,
+ and other items may reuse the values of their key
+ properties.
"""
def restore(self, nodeid):
'''Restore a retired node.
- Make node available for all operations like it was before retirement.
+ Make node available for all operations like it was before
+ retirement.
'''
def history(self, itemid):
"""Retrieve the journal of edits on a particular item.
- 'itemid' must be the id of an existing item of this class or an
- IndexError is raised.
+ 'itemid' must be the id of an existing item of this class or
+ an IndexError is raised.
The returned list contains tuples of the form
(date, tag, action, params)
- 'date' is a Timestamp object specifying the time of the change and
- 'tag' is the journaltag specified when the database was opened.
- 'action' may be:
+ 'date' is a Timestamp object specifying the time of the
+ change and 'tag' is the journaltag specified when the
+ database was opened. 'action' may be:
- 'create' or 'set' -- 'params' is a dictionary of property values
- 'link' or 'unlink' -- 'params' is (classname, itemid, propname)
+ 'create' or 'set' -- 'params' is a dictionary of
+ property values
+ 'link' or 'unlink' -- 'params' is (classname, itemid,
+ propname)
'retire' -- 'params' is None
"""
# Locating items:
def setkey(self, propname):
- """Select a String property of this class to be the key property.
+ """Select a String property of this class to be the key
+ property.
- 'propname' must be the name of a String property of this class or
- None, or a TypeError is raised. The values of the key property on
- all existing items must be unique or a ValueError is raised.
+ 'propname' must be the name of a String property of this
+ class or None, or a TypeError is raised. The values of the
+ key property on all existing items must be unique or a
+ ValueError is raised.
"""
def getkey(self):
- """Return the name of the key property for this class or None."""
+ """Return the name of the key property for this class or
+ None.
+ """
def lookup(self, keyvalue):
- """Locate a particular item by its key property and return its id.
+ """Locate a particular item by its key property and return
+ its id.
- If this class has no key property, a TypeError is raised. If the
- 'keyvalue' matches one of the values for the key property among
- the items in this class, the matching item's id is returned;
- otherwise a KeyError is raised.
+ If this class has no key property, a TypeError is raised.
+ If the 'keyvalue' matches one of the values for the key
+ property among the items in this class, the matching item's
+ id is returned; otherwise a KeyError is raised.
"""
def find(self, propname, itemid):
- """Get the ids of items in this class which link to the given items.
+ """Get the ids of items in this class which link to the
+ given items.
- 'propspec' consists of keyword args propname={itemid:1,}
- 'propname' must be the name of a property in this class, or a
- KeyError is raised. That property must be a Link or Multilink
- property, or a TypeError is raised.
+ 'propspec' consists of keyword args propname={itemid:1,}
+ 'propname' must be the name of a property in this class, or
+ a KeyError is raised. That property must be a Link or
+ Multilink property, or a TypeError is raised.
- Any item in this class whose 'propname' property links to any of the
- itemids will be returned. Used by the full text indexing, which
- knows that "foo" occurs in msg1, msg3 and file7, so we have hits
- on these issues:
+ Any item in this class whose 'propname' property links to
+ any of the itemids will be returned. Used by the full text
+ indexing, which knows that "foo" occurs in msg1, msg3 and
+ file7, so we have hits on these issues:
db.issue.find(messages={'1':1,'3':1}, files={'7':1})
"""
def filter(self, search_matches, filterspec, sort, group):
- ''' Return a list of the ids of the active items in this class that
- match the 'filter' spec, sorted by the group spec and then the
- sort spec.
- '''
+ """ Return a list of the ids of the active items in this
+ class that match the 'filter' spec, sorted by the group spec
+ and then the sort spec.
+ """
def list(self):
- """Return a list of the ids of the active items in this class."""
+ """Return a list of the ids of the active items in this
+ class.
+ """
def count(self):
"""Get the number of items in this class.
- If the returned integer is 'numitems', the ids of all the items
- in this class run from 1 to numitems, and numitems+1 will be the
- id of the next item to be created in this class.
+ If the returned integer is 'numitems', the ids of all the
+ items in this class run from 1 to numitems, and numitems+1
+ will be the id of the next item to be created in this class.
"""
# Manipulating properties:
def getprops(self):
- """Return a dictionary mapping property names to property objects."""
+ """Return a dictionary mapping property names to property
+ objects.
+ """
def addprop(self, **properties):
"""Add properties to this class.
- The keyword arguments in 'properties' must map names to property
- objects, or a TypeError is raised. None of the keys in 'properties'
- may collide with the names of existing properties, or a ValueError
- is raised before any properties have been added.
+ The keyword arguments in 'properties' must map names to
+ property objects, or a TypeError is raised. None of the
+ keys in 'properties' may collide with the names of existing
+ properties, or a ValueError is raised before any properties
+ have been added.
"""
def getitem(self, itemid, cache=1):
- ''' Return a Item convenience wrapper for the item.
+ """ Return a Item convenience wrapper for the item.
- 'itemid' must be the id of an existing item of this class or an
- IndexError is raised.
+ 'itemid' must be the id of an existing item of this class or
+ an IndexError is raised.
- 'cache' indicates whether the transaction cache should be queried
- for the item. If the item has been modified and you need to
- determine what its values prior to modification are, you need to
- set cache=0.
- '''
+ 'cache' indicates whether the transaction cache should be
+ queried for the item. If the item has been modified and you
+ need to determine what its values prior to modification are,
+ you need to set cache=0.
+ """
class Item:
- ''' A convenience wrapper for the given item. It provides a mapping
- interface to a single item's properties
- '''
+ """ A convenience wrapper for the given item. It provides a
+ mapping interface to a single item's properties
+ """
Hyperdatabase Implementations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
and so on.
Several implementations are provided - they belong in the
-roundup.backends package.
+``roundup.backends`` package.
Application Example
4
>>> db.issue.create(title="abuse", status=1)
5
- >>> hyperdb.Class(db, "user", username=hyperdb.Key(), password=hyperdb.String())
+ >>> hyperdb.Class(db, "user", username=hyperdb.Key(),
+ ... password=hyperdb.String())
<hyperdb.Class "user">
>>> db.issue.addprop(fixer=hyperdb.Link("user"))
>>> db.issue.getprops()
>>> db.issue.find("status", db.status.lookup("in-progress"))
[2, 4, 5]
>>> db.issue.history(5)
- [(<Date 2000-06-28.19:09:43>, "ping", "create", {"title": "abuse", "status": 1}),
+ [(<Date 2000-06-28.19:09:43>, "ping", "create", {"title": "abuse",
+ "status": 1}),
(<Date 2000-06-28.19:11:04>, "ping", "set", {"status": 2})]
>>> db.status.history(1)
[(<Date 2000-06-28.19:09:43>, "ping", "link", ("issue", 5, "status")),
detectors and user items, and on issues it provides mail spools, nosy
lists, and superseders.
+
Reserved Classes
~~~~~~~~~~~~~~~~
system). The "summary" property contains a summary of the message for
display in a message index.
+
Files
"""""
"name" property holds the original name of the file, and the "type"
property holds the MIME type of the file as received.
+
Issue Classes
~~~~~~~~~~~~~
when any property on the issue was last edited (equivalently, these are
the dates on the first and last records in the issue's journal).
+
Roundupdb Interface Specification
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The default schema included with Roundup turns it into a typical
software bug tracker. The database is set up like this::
- pri = Class(db, "priority", name=hyperdb.String(), order=hyperdb.String())
+ pri = Class(db, "priority", name=hyperdb.String(),
+ order=hyperdb.String())
pri.setkey("name")
pri.create(name="critical", order="1")
pri.create(name="urgent", order="2")
pri.create(name="feature", order="4")
pri.create(name="wish", order="5")
- stat = Class(db, "status", name=hyperdb.String(), order=hyperdb.String())
+ stat = Class(db, "status", name=hyperdb.String(),
+ order=hyperdb.String())
stat.setkey("name")
stat.create(name="unread", order="1")
stat.create(name="deferred", order="2")
"priority" and "status" classes::
def Choice(name, *options):
- cl = Class(db, name, name=hyperdb.String(), order=hyperdb.String())
+ cl = Class(db, name, name=hyperdb.String(),
+ order=hyperdb.String())
for i in range(len(options)):
cl.create(name=option[i], order=i)
return hyperdb.Link(name)
carry out the operation. After it's done, it then calls all of the
*reactors* that have been registered for the operation.
+
Detector Interface Specification
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
For a ``retire()`` or ``restore()`` operation, ``itemid`` is the id of
the retired or restored item and ``olddata`` is None.
+
Detector Example
~~~~~~~~~~~~~~~~
new = newdata["approvals"]
for uid in old:
if uid not in new and uid != db.getuid():
- raise Reject, "You can't remove other users from the "
- "approvals list; you can only remove yourself."
+ raise Reject, "You can't remove other users from " \
+ "the approvals list; you can only remove " \
+ "yourself."
for uid in new:
if uid not in old and uid != db.getuid():
- raise Reject, "You can't add other users to the approvals "
- "list; you can only add yourself."
+ raise Reject, "You can't add other users to the " \
+ "approvals list; you can only add yourself."
- # When three people have approved a project, change its
- # status from "pending" to "approved".
+ # When three people have approved a project, change its status from
+ # "pending" to "approved".
def approve_project(db, cl, id, olddata):
- if olddata.has_key("approvals") and len(cl.get(id, "approvals")) == 3:
+ if (olddata.has_key("approvals") and
+ len(cl.get(id, "approvals")) == 3):
if cl.get(id, "status") == db.status.lookup("pending"):
cl.set(id, status=db.status.lookup("approved"))
maintainer of the package can then apply the patch by setting its status
to "applied"::
- # Only accept attempts to create new patches that come with patch files.
+ # Only accept attempts to create new patches that come with patch
+ # files.
def check_new_patch(db, cl, id, newdata):
if not newdata["files"]:
"attaching a patch file."
for fileid in newdata["files"]:
if db.file.get(fileid, "type") != "text/plain":
- raise Reject, "Submitted patch files must be text/plain."
+ raise Reject, "Submitted patch files must be " \
+ "text/plain."
- # When the status is changed from "approved" to "applied", apply the patch.
+ # When the status is changed from "approved" to "applied", apply the
+ # patch.
def apply_patch(db, cl, id, olddata):
- if cl.get(id, "status") == db.status.lookup("applied") and \
- olddata["status"] == db.status.lookup("approved"):
+ if (cl.get(id, "status") == db.status.lookup("applied") and
+ olddata["status"] == db.status.lookup("approved")):
# ...apply the patch...
def init(db):
interesting can simply be written in Python using the Roundup database
module.)
+
Command Interface Specification
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
commands, they are printed one per line (default) or joined by commas
(with the -list) option.
+
Usage Example
~~~~~~~~~~~~~
script by the mail delivery system (e.g. using an alias beginning with
"|" for sendmail).
+
Message Processing
~~~~~~~~~~~~~~~~~~
raises an exception, the original message is bounced back to the sender
with the explanatory message given in the exception.
+
Nosy Lists
~~~~~~~~~~
hyperdatabase on the "recipients" property then provides a log of when
the message was sent to whom.
+
Setting Properties
~~~~~~~~~~~~~~~~~~
interspersed some nonstandard tags, which we use as placeholders to be
replaced by properties and their values.
+
Views and View Specifiers
~~~~~~~~~~~~~~~~~~~~~~~~~
view, the Web browser should be redirected to a canonical location
containing a complete view specifier so that the view can be bookmarked.
+
Displaying Properties
~~~~~~~~~~~~~~~~~~~~~
section. The filter section provides some widgets for selecting which
issues appear in the index. The index section is a table of issues.
+
Index View Specifiers
"""""""""""""""""""""
<table>
<tr>
- <td colspan=2 tal:content="python:context.title.field(size='60')"></td>
+ <td colspan=2
+ tal:content="python:context.title.field(size='60')"></td>
</tr>
<tr>
<td tal:content="context/fixer/field"></td>
(thus triggering the standard detector to react by sending out this
message to the nosy list).
+
Spool Section
"""""""""""""
if db.security.hasPermission('issue', 'Edit', userid):
# all ok
- if db.security.hasItemPermission('issue', itemid, assignedto=userid):
+ if db.security.hasItemPermission('issue', itemid,
+ assignedto=userid):
# all ok
Code in the core will make use of these methods, as should code in
Anonymous Users
~~~~~~~~~~~~~~~
-The "anonymous" user must always exist, and defines the access permissions for
-anonymous users. Unknown users accessing Roundup through the web or email
-interfaces will be logged in as the "anonymous" user.
+The "anonymous" user must always exist, and defines the access
+permissions for anonymous users. Unknown users accessing Roundup through
+the web or email interfaces will be logged in as the "anonymous" user.
Use Cases
Vincent, Mark Miller, Christopher Simons, Jeff Dunmall, Wayne Gramlich,
and Dean Tribble for their assistance with the first-round submission.
+
Changes to this document
------------------------
diff --git a/roundup/mailgw.py b/roundup/mailgw.py
index eee37cb922312f31e06ec36ac98e553592dad9bb..13a7f939af16ec292777f07f036d9b9b7f428b32 100644 (file)
--- a/roundup/mailgw.py
+++ b/roundup/mailgw.py
an exception, the original message is bounced back to the sender with the
explanatory message given in the exception.
-$Id: mailgw.py,v 1.124 2003-06-23 08:37:15 neaj Exp $
+$Id: mailgw.py,v 1.125 2003-06-24 12:39:20 neaj Exp $
'''
import string, re, os, mimetools, cStringIO, smtplib, socket, binascii, quopri
def getheader(self, name, default=None):
hdr = mimetools.Message.getheader(self, name, default)
+ hdr = hdr.replace('\n','') # Inserted by rfc822.readheaders
return rfc2822.decode_header(hdr)
class MailGW: