Anonymous avatar Anonymous committed aa9ae8a Draft

Create setuptools tracker.

Comments (0)

Files changed (43)

config.ini.template

+# WARNING! Following options need adjustments:
+#  [mail]: domain, host
+#  [tracker]: web
+
+[main]
+
+# Database directory path.
+# The path may be either absolute or relative
+# to the directory containig this config file.
+# Default: db
+database = db
+
+# Path to the HTML templates directory.
+# The path may be either absolute or relative
+# to the directory containig this config file.
+# Default: html
+templates = html
+
+# Path to directory holding additional static files
+# available via Web UI.  This directory may contain
+# sitewide images, CSS stylesheets etc. and is searched
+# for these files prior to the TEMPLATES directory
+# specified above.  If this option is not set, all static
+# files are taken from the TEMPLATES directory
+# The path may be either absolute or relative
+# to the directory containig this config file.
+# Default: 
+static_files = 
+
+# Email address that roundup will complain to if it runs into trouble.
+# Default: roundup-admin
+admin_email = roundup-admin
+
+# The 'dispatcher' is a role that can get notified
+# of new items to the database.
+# It is used by the ERROR_MESSAGES_TO config setting.
+# Default: roundup-admin
+dispatcher_email = roundup-admin
+
+# Additional text to include in the "name" part
+# of the From: address used in nosy messages.
+# If the sending user is "Foo Bar", the From: line
+# is usually: "Foo Bar" <issue_tracker@tracker.example>
+# the EMAIL_FROM_TAG goes inside the "Foo Bar" quotes like so:
+# "Foo Bar EMAIL_FROM_TAG" <issue_tracker@tracker.example>
+# Default: 
+email_from_tag = 
+
+# Roles that a user gets when they register with Web User Interface.
+# This is a comma-separated string of role names (e.g. 'Admin,User').
+# Default: User
+new_web_user_roles = User
+
+# Roles that a user gets when they register with Email Gateway.
+# This is a comma-separated string of role names (e.g. 'Admin,User').
+# Default: User
+new_email_user_roles = User
+
+# Send error message emails to the dispatcher, user, or both?
+# The dispatcher is configured using the DISPATCHER_EMAIL setting.
+# Default: user
+error_messages_to = user
+
+# HTML version to generate. The templates are html4 by default.
+# If you wish to make them xhtml, then you'll need to change this
+# var to 'xhtml' too so all auto-generated HTML is compliant.
+# Allowed values: html4, xhtml
+# Default: html4
+html_version = html4
+
+# Default timezone offset, applied when user's timezone is not set.
+# If pytz module is installed, value may be any valid
+# timezone specification (e.g. EET or Europe/Warsaw).
+# If pytz is not installed, value must be integer number
+# giving local timezone offset from UTC in hours.
+# Default: UTC
+timezone = UTC
+
+# Register new users instantly, or require confirmation via
+# email?
+# Allowed values: yes, no
+# Default: no
+instant_registration = no
+
+# Offer registration confirmation by email or only through the web?
+# Allowed values: yes, no
+# Default: yes
+email_registration_confirmation = yes
+
+# Additional stop-words for the full-text indexer specific to
+# your tracker. See the indexer source for the default list of
+# stop-words (eg. A,AND,ARE,AS,AT,BE,BUT,BY, ...)
+# Allowed values: comma-separated list of words
+# Default: 
+indexer_stopwords = 
+
+# Defines the file creation mode mask.
+# Default: 02
+umask = 02
+
+[tracker]
+
+# A descriptive name for your roundup instance.
+# Default: Roundup issue tracker
+name = Setuptools tracker
+
+# The web address that the tracker is viewable at.
+# This will be included in information sent to users of the tracker.
+# The URL MUST include the cgi-bin part or anything else
+# that is required to get to the home page of the tracker.
+# You MUST include a trailing '/' in the URL.
+# Default: NO DEFAULT
+web = http://localhost:9999/setuptools/
+
+# Email address that mail to roundup should go to.
+# Default: issue_tracker
+email = issue_tracker
+
+# Default locale name for this tracker.
+# If this option is not set, the language is determined
+# by OS environment variable LANGUAGE, LC_ALL, LC_MESSAGES,
+# or LANG, in that order of preference.
+# Default: 
+language = 
+
+[web]
+
+# Whether to use HTTP Basic Authentication, if present.
+# Roundup will use either the REMOTE_USER or HTTP_AUTHORIZATION
+# variables supplied by your web server (in that order).
+# Set this option to 'no' if you do not wish to use HTTP Basic
+# Authentication in your web interface.
+# Allowed values: yes, no
+# Default: yes
+http_auth = yes
+
+# Whether to use HTTP Accept-Language, if present.
+# Browsers send a language-region preference list.
+# It's usually set in the client's browser or in their
+# Operating System.
+# Set this option to 'no' if you want to ignore it.
+# Allowed values: yes, no
+# Default: yes
+use_browser_language = yes
+
+# Setting this option makes Roundup display error tracebacks
+# in the user's browser rather than emailing them to the
+# tracker admin.
+# Allowed values: yes, no
+# Default: no
+debug = no
+
+# Settings in this section are used by Postgresql and MySQL backends only
+[rdbms]
+
+# Name of the database to use.
+# Default: roundup
+name = roundup_setuptools
+
+# Database server host.
+# Default: localhost
+host = localhost
+
+# TCP port number of the database server.
+# Postgresql usually resides on port 5432 (if any),
+# for MySQL default port number is 3306.
+# Leave this option empty to use backend default
+# Default: 
+port = 
+
+# Database user name that Roundup should use.
+# Default: roundup
+user = roundup
+
+# Database user password.
+# Default: roundup
+password = roundup
+
+# Name of the MySQL defaults file.
+# Only used in MySQL connections.
+# Default: ~/.my.cnf
+read_default_file = ~/.my.cnf
+
+# Name of the group to use in the MySQL defaults file (.my.cnf).
+# Only used in MySQL connections.
+# Default: roundup
+read_default_group = roundup
+
+[logging]
+
+# Path to configuration file for standard Python logging module.
+# If this option is set, logging configuration is loaded
+# from specified file; options 'filename' and 'level'
+# in this section are ignored.
+# The path may be either absolute or relative
+# to the directory containig this config file.
+# Default: 
+config = 
+
+# Log file name for minimal logging facility built into Roundup.
+# If no file name specified, log messages are written on stderr.
+# If above 'config' option is set, this option has no effect.
+# The path may be either absolute or relative
+# to the directory containig this config file.
+# Default: 
+filename = 
+
+# Minimal severity level of messages written to log file.
+# If above 'config' option is set, this option has no effect.
+# Allowed values: DEBUG, INFO, WARNING, ERROR
+# Default: ERROR
+level = ERROR
+
+# Outgoing email options.
+# Used for nozy messages and approval requests
+[mail]
+
+# Domain name used for email addresses.
+# Default: NO DEFAULT
+#domain = NO DEFAULT
+
+# SMTP mail host that roundup will use to send mail
+# Default: NO DEFAULT
+host = localhost
+
+# SMTP login name.
+# Set this if your mail host requires authenticated access.
+# If username is not empty, password (below) MUST be set!
+# Default: 
+username = 
+
+# SMTP login password.
+# Set this if your mail host requires authenticated access.
+# Default: NO DEFAULT
+#password = NO DEFAULT
+
+# Default port to send SMTP on.
+# Set this if your mail server runs on a different port.
+# Default: 25
+port = 25
+
+# The local hostname to use during SMTP transmission.
+# Set this if your mail server requires something specific.
+# Default: 
+local_hostname = 
+
+# If your SMTP mail host provides or requires TLS
+# (Transport Layer Security) then set this option to 'yes'.
+# Allowed values: yes, no
+# Default: no
+tls = no
+
+# If TLS is used, you may set this option to the name
+# of a PEM formatted file that contains your private key.
+# The path may be either absolute or relative
+# to the directory containig this config file.
+# Default: 
+tls_keyfile = 
+
+# If TLS is used, you may set this option to the name
+# of a PEM formatted certificate chain file.
+# The path may be either absolute or relative
+# to the directory containig this config file.
+# Default: 
+tls_certfile = 
+
+# Character set to encode email headers with.
+# We use utf-8 by default, as it's the most flexible.
+# Some mail readers (eg. Eudora) can't cope with that,
+# so you might need to specify a more limited character set
+# (eg. iso-8859-1).
+# Default: utf-8
+charset = utf-8
+
+# Setting this option makes Roundup to write all outgoing email
+# messages to this file *instead* of sending them.
+# This option has the same effect as environment variable SENDMAILDEBUG.
+# Environment variable takes precedence.
+# The path may be either absolute or relative
+# to the directory containig this config file.
+# Default: 
+debug = 
+
+# Add a line with author information at top of all messages
+# sent by roundup
+# Allowed values: yes, no
+# Default: yes
+add_authorinfo = yes
+
+# Add the mail address of the author to the author information at
+# the top of all messages.
+# If this is false but add_authorinfo is true, only the name
+# of the actor is added which protects the mail address of the
+# actor from being exposed at mail archives, etc.
+# Allowed values: yes, no
+# Default: yes
+add_authoremail = yes
+
+# Roundup Mail Gateway options
+[mailgw]
+
+# Keep email citations when accepting messages.
+# Setting this to "no" strips out "quoted" text from the message.
+# Signatures are also stripped.
+# Allowed values: yes, no
+# Default: yes
+keep_quoted_text = yes
+
+# Preserve the email body as is - that is,
+# keep the citations _and_ signatures.
+# Allowed values: yes, no
+# Default: no
+leave_body_unchanged = no
+
+# Default class to use in the mailgw
+# if one isn't supplied in email subjects.
+# To disable, leave the value blank.
+# Default: issue
+default_class = issue
+
+# Default locale name for the tracker mail gateway.
+# If this option is not set, mail gateway will use
+# the language of the tracker instance.
+# Default: 
+language = 
+
+# Controls the parsing of the [prefix] on subject
+# lines in incoming emails. "strict" will return an
+# error to the sender if the [prefix] is not recognised.
+# "loose" will attempt to parse the [prefix] but just
+# pass it through as part of the issue title if not
+# recognised. "none" will always pass any [prefix]
+# through as part of the issue title.
+# Default: strict
+subject_prefix_parsing = strict
+
+# Controls the parsing of the [suffix] on subject
+# lines in incoming emails. "strict" will return an
+# error to the sender if the [suffix] is not recognised.
+# "loose" will attempt to parse the [suffix] but just
+# pass it through as part of the issue title if not
+# recognised. "none" will always pass any [suffix]
+# through as part of the issue title.
+# Default: strict
+subject_suffix_parsing = strict
+
+# Defines the brackets used for delimiting the prefix and 
+# suffix in a subject line. The presence of "suffix" in
+# the config option name is a historical artifact and may
+# be ignored.
+# Default: []
+subject_suffix_delimiters = []
+
+# Controls matching of the incoming email subject line
+# against issue titles in the case where there is no
+# designator [prefix]. "never" turns off matching.
+# "creation + interval" or "activity + interval"
+# will match an issue for the interval after the issue's
+# creation or last activity. The interval is a standard
+# Roundup interval.
+# Default: always
+subject_content_match = always
+
+# Regular expression matching a single reply or forward
+# prefix prepended by the mailer. This is explicitly
+# stripped from the subject during parsing.
+# Value is Python Regular Expression (UTF8-encoded).
+# Default: (\s*\W?\s*(fw|fwd|re|aw|sv|ang)\W)+
+refwd_re = (\s*\W?\s*(fw|fwd|re|aw|sv|ang)\W)+
+
+# Regular expression matching start of an original message
+# if quoted the in body.
+# Value is Python Regular Expression (UTF8-encoded).
+# Default: ^[>|\s]*-----\s?Original Message\s?-----$
+origmsg_re = ^[>|\s]*-----\s?Original Message\s?-----$
+
+# Regular expression matching the start of a signature
+# in the message body.
+# Value is Python Regular Expression (UTF8-encoded).
+# Default: ^[>|\s]*-- ?$
+sign_re = ^[>|\s]*-- ?$
+
+# Regular expression matching end of line.
+# Value is Python Regular Expression (UTF8-encoded).
+# Default: [\r\n]+
+eol_re = [\r\n]+
+
+# Regular expression matching a blank line.
+# Value is Python Regular Expression (UTF8-encoded).
+# Default: [\r\n]+\s*[\r\n]+
+blankline_re = [\r\n]+\s*[\r\n]+
+
+# When parsing incoming mails, roundup uses the first
+# text/plain part it finds. If this part is inside a
+# multipart/alternative, and this option is set, all other
+# parts of the multipart/alternative are ignored. The default
+# is to keep all parts and attach them to the issue.
+# Allowed values: yes, no
+# Default: no
+ignore_alternatives = no
+
+# OpenPGP mail processing options
+[pgp]
+
+# Enable PGP processing. Requires pyme.
+# Allowed values: yes, no
+# Default: no
+enable = no
+
+# If specified, a comma-separated list of roles to perform
+# PGP processing on. If not specified, it happens for all
+# users.
+# Default: 
+roles = 
+
+# Location of PGP directory. Defaults to $HOME/.gnupg if
+# not specified.
+# Default: 
+homedir = 
+
+# Nosy messages sending
+[nosy]
+
+# Send nosy messages to the author of the message.
+# Allowed values: yes, no, new
+# Default: no
+messages_to_author = yes
+
+# Where to place the email signature.
+# Allowed values: top, bottom, none
+# Default: bottom
+signature_position = bottom
+
+# Does the author of a message get placed on the nosy list
+# automatically?  If 'new' is used, then the author will
+# only be added when a message creates a new issue.
+# If 'yes', then the author will be added on followups too.
+# If 'no', they're never added to the nosy.
+# 
+# Allowed values: yes, no, new
+# Default: new
+add_author = yes
+
+# Do the recipients (To:, Cc:) of a message get placed on the
+# nosy list?  If 'new' is used, then the recipients will
+# only be added when a message creates a new issue.
+# If 'yes', then the recipients will be added on followups too.
+# If 'no', they're never added to the nosy.
+# 
+# Allowed values: yes, no, new
+# Default: new
+add_recipients = new
+
+# Controls the email sending from the nosy reactor. If
+# "multiple" then a separate email is sent to each
+# recipient. If "single" then a single email is sent with
+# each recipient as a CC address.
+# Default: single
+email_sending = single
+
+# Attachments larger than the given number of bytes
+# won't be attached to nosy mails. They will be replaced by
+# a link to the tracker's download page for the file.
+# Default: 2147483647
+max_attachment_size = 2147483647
+postgresql

detectors/messagesummary.py

+#$Id$
+
+from roundup.mailgw import parseContent
+
+def summarygenerator(db, cl, nodeid, newvalues):
+    ''' If the message doesn't have a summary, make one for it.
+    '''
+    if newvalues.has_key('summary') or not newvalues.has_key('content'):
+        return
+
+    summary, content = parseContent(newvalues['content'], config=db.config)
+    newvalues['summary'] = summary
+
+
+def init(db):
+    # fire before changes are made
+    db.msg.audit('create', summarygenerator)
+
+# vim: set filetype=python ts=4 sw=4 et si
+#SHA: 538f90cb7f4eb63f77eca252b87afbe037d29c48

detectors/nosyreaction.py

+#
+# Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
+# This module is free software, and you may redistribute it and/or modify
+# under the same terms as Python, so long as this copyright message and
+# disclaimer are retained in their original form.
+#
+# IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
+# DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
+# OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
+# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
+# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
+# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+# 
+#$Id$
+
+import sets
+
+from roundup import roundupdb, hyperdb
+
+def nosyreaction(db, cl, nodeid, oldvalues):
+    ''' A standard detector is provided that watches for additions to the
+        "messages" property.
+        
+        When a new message is added, the detector sends it to all the users on
+        the "nosy" list for the issue that are not already on the "recipients"
+        list of the message.
+        
+        Those users are then appended to the "recipients" property on the
+        message, so multiple copies of a message are never sent to the same
+        user.
+        
+        The journal recorded by the hyperdatabase on the "recipients" property
+        then provides a log of when the message was sent to whom. 
+    '''
+    # send a copy of all new messages to the nosy list
+    for msgid in determineNewMessages(cl, nodeid, oldvalues):
+        try:
+            cl.nosymessage(nodeid, msgid, oldvalues)
+        except roundupdb.MessageSendError, message:
+            raise roundupdb.DetectorError, message
+
+def determineNewMessages(cl, nodeid, oldvalues):
+    ''' Figure a list of the messages that are being added to the given
+        node in this transaction.
+    '''
+    messages = []
+    if oldvalues is None:
+        # the action was a create, so use all the messages in the create
+        messages = cl.get(nodeid, 'messages')
+    elif oldvalues.has_key('messages'):
+        # the action was a set (so adding new messages to an existing issue)
+        m = {}
+        for msgid in oldvalues['messages']:
+            m[msgid] = 1
+        messages = []
+        # figure which of the messages now on the issue weren't there before
+        for msgid in cl.get(nodeid, 'messages'):
+            if not m.has_key(msgid):
+                messages.append(msgid)
+    return messages
+
+def updatenosy(db, cl, nodeid, newvalues):
+    '''Update the nosy list for changes to the assignedto
+    '''
+    # nodeid will be None if this is a new node
+    current_nosy = sets.Set()
+    if nodeid is None:
+        ok = ('new', 'yes')
+    else:
+        ok = ('yes',)
+        # old node, get the current values from the node if they haven't
+        # changed
+        if not newvalues.has_key('nosy'):
+            nosy = cl.get(nodeid, 'nosy')
+            for value in nosy:
+                current_nosy.add(value)
+
+    # if the nosy list changed in this transaction, init from the new value
+    if newvalues.has_key('nosy'):
+        nosy = newvalues.get('nosy', [])
+        for value in nosy:
+            if not db.hasnode('user', value):
+                continue
+            current_nosy.add(value)
+
+    new_nosy = sets.Set(current_nosy)
+
+    # add assignedto(s) to the nosy list
+    if newvalues.has_key('assignedto') and newvalues['assignedto'] is not None:
+        propdef = cl.getprops()
+        if isinstance(propdef['assignedto'], hyperdb.Link):
+            assignedto_ids = [newvalues['assignedto']]
+        elif isinstance(propdef['assignedto'], hyperdb.Multilink):
+            assignedto_ids = newvalues['assignedto']
+        for assignedto_id in assignedto_ids:
+            new_nosy.add(assignedto_id)
+
+    # see if there's any new messages - if so, possibly add the author and
+    # recipient to the nosy
+    if newvalues.has_key('messages'):
+        if nodeid is None:
+            ok = ('new', 'yes')
+            messages = newvalues['messages']
+        else:
+            ok = ('yes',)
+            # figure which of the messages now on the issue weren't
+            oldmessages = cl.get(nodeid, 'messages')
+            messages = []
+            for msgid in newvalues['messages']:
+                if msgid not in oldmessages:
+                    messages.append(msgid)
+
+        # configs for nosy modifications
+        add_author = getattr(db.config, 'ADD_AUTHOR_TO_NOSY', 'new')
+        add_recips = getattr(db.config, 'ADD_RECIPIENTS_TO_NOSY', 'new')
+
+        # now for each new message:
+        msg = db.msg
+        for msgid in messages:
+            if add_author in ok:
+                authid = msg.get(msgid, 'author')
+                new_nosy.add(authid)
+
+            # add on the recipients of the message
+            if add_recips in ok:
+                for recipient in msg.get(msgid, 'recipients'):
+                    new_nosy.add(recipient)
+
+    if current_nosy != new_nosy:
+        # that's it, save off the new nosy list
+        newvalues['nosy'] = list(new_nosy)
+
+def init(db):
+    db.issue.react('create', nosyreaction)
+    db.issue.react('set', nosyreaction)
+    db.issue.audit('create', updatenosy)
+    db.issue.audit('set', updatenosy)
+
+# vim: set filetype=python ts=4 sw=4 et si
+#SHA: 3d11590f7c86b53e2733fff80fa1108534245023

detectors/statusauditor.py

+# Copyright (c) 2002 ekit.com Inc (http://www.ekit-inc.com/)
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+#   The above copyright notice and this permission notice shall be included in
+#   all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+#
+#$Id$
+
+def chatty(db, cl, nodeid, newvalues):
+    ''' If the issue is currently 'unread', 'resolved', 'done-cbb' or None,
+        then set it to 'chatting'
+    '''
+    # don't fire if there's no new message (ie. chat)
+    if not newvalues.has_key('messages'):
+        return
+    if newvalues['messages'] == cl.get(nodeid, 'messages'):
+        return
+
+    # get the chatting state ID
+    try:
+        chatting_id = db.status.lookup('chatting')
+    except KeyError:
+        # no chatting state, ignore all this stuff
+        return
+
+    # get the current value
+    current_status = cl.get(nodeid, 'status')
+
+    # see if there's an explicit change in this transaction
+    if newvalues.has_key('status'):
+        # yep, skip
+        return
+
+    # determine the id of 'unread', 'resolved' and 'chatting'
+    fromstates = []
+    for state in 'unread resolved done-cbb'.split():
+        try:
+            fromstates.append(db.status.lookup(state))
+        except KeyError:
+            pass
+
+    # ok, there's no explicit change, so check if we are in a state that
+    # should be changed
+    if current_status in fromstates + [None]:
+        # yep, we're now chatting
+        newvalues['status'] = chatting_id
+
+
+def presetunread(db, cl, nodeid, newvalues):
+    ''' Make sure the status is set on new issues
+    '''
+    if newvalues.has_key('status') and newvalues['status']:
+        return
+
+    # get the unread state ID
+    try:
+        unread_id = db.status.lookup('unread')
+    except KeyError:
+        # no unread state, ignore all this stuff
+        return
+
+    # ok, do it
+    newvalues['status'] = unread_id
+
+
+def init(db):
+    # fire before changes are made
+    db.issue.audit('set', chatty)
+    db.issue.audit('create', presetunread)
+
+# vim: set filetype=python ts=4 sw=4 et si
+#SHA: a8fd93023ef598400b495c1a5293cf8b9287b330

detectors/userauditor.py

+# Copyright (c) 2003 Richard Jones (richard@mechanicalcat.net)
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+#   The above copyright notice and this permission notice shall be included in
+#   all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+#
+#$Id$
+
+import re
+
+# regular expression thanks to: http://www.regular-expressions.info/email.html
+# this is the "99.99% solution for syntax only".
+email_regexp = (r"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*", r"(localhost|(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9]))")
+email_rfc = re.compile('^' + email_regexp[0] + '@' + email_regexp[1] + '$', re.IGNORECASE)
+email_local = re.compile('^' + email_regexp[0] + '$', re.IGNORECASE)
+
+def valid_address(address):
+    ''' If we see an @-symbol in the address then check against the full
+        RFC syntax. Otherwise it is a local-only address so only check
+        the local part of the RFC syntax.
+    '''
+    if '@' in address:
+        return email_rfc.match(address)
+    else:
+        return email_local.match(address)
+
+def get_addresses(user):
+    ''' iterate over all known addresses in a newvalues dict
+        this takes of the address/alterate_addresses handling
+    '''
+    if user.has_key('address'):
+        yield user['address']
+    if user.get('alternate_addresses', None):
+        for address in user['alternate_addresses'].split('\n'):
+            yield address
+
+def audit_user_fields(db, cl, nodeid, newvalues):
+    ''' Make sure user properties are valid.
+
+        - email address is syntactically valid
+        - email address is unique
+        - roles specified exist
+        - timezone is valid
+    '''
+
+    for address in get_addresses(newvalues):
+        if not valid_address(address):
+            raise ValueError, 'Email address syntax is invalid'
+
+        check_main = db.user.stringFind(address=address)
+        # make sure none of the alts are owned by anyone other than us (x!=nodeid)
+        check_alts = [x for x in db.user.filter(None, {'alternate_addresses' : address}) if x != nodeid]
+        if check_main or check_alts:
+            raise ValueError, 'Email address %s already in use' % address
+
+    for rolename in [r.lower().strip() for r in newvalues.get('roles', '').split(',')]:
+            if rolename and not db.security.role.has_key(rolename):
+                raise ValueError, 'Role "%s" does not exist'%rolename
+
+    tz = newvalues.get('timezone', None)
+    if tz:
+        # if they set a new timezone validate the timezone by attempting to
+        # use it before we store it to the db.
+        import roundup.date
+        import datetime
+        try:
+            TZ = roundup.date.get_timezone(tz)
+            dt = datetime.datetime.now()
+            local = TZ.localize(dt).utctimetuple()
+        except IOError:
+            raise ValueError, 'Timezone "%s" does not exist' % tz
+        except ValueError:
+            raise ValueError, 'Timezone "%s" exceeds valid range [-23...23]' % tz
+
+def init(db):
+    # fire before changes are made
+    db.user.audit('set', audit_user_fields)
+    db.user.audit('create', audit_user_fields)
+
+# vim: sts=4 sw=4 et si
+#SHA: 302da829b8ac2cd0b6ef17a1678cd0d7cd308642

extensions/README.txt

+This directory is for tracker extensions:
+
+- CGI Actions
+- Templating functions
+
+See the customisation doc for more information.

html/_generic.404.html

+<html>
+<head>
+<title>Item Not Found</title>
+</head>
+
+<body>
+There is no <span tal:content="context/_classname" /> with id <span tal:content="context/id"/>
+</body>
+</html>
+<!-- SHA: 7c74a39904a98fd7aacba48daf5c6820da253ed2 -->

html/_generic.calendar.html

+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+ <head>
+  <link rel="stylesheet" type="text/css" href="@@file/style.css" />
+  <meta http-equiv="Content-Type" content="text/html; charset=utf-8;" />
+  <title tal:content="string:Roundup Calendar"></title>
+  <script language="Javascript"
+          type="text/javascript"
+          tal:content="structure string:
+          // this is the name of the field in the original form that we're working on
+          form  = window.opener.document.${request/form/form/value};
+          field = '${request/form/property/value}';" >
+  </script>
+ </head>
+ <body class="body"
+       tal:content="structure python:utils.html_calendar(request)">
+ </body>
+</html>
+<!-- SHA: 3c1535fe01902cf3fa7115c982c45f7b0674b590 -->

html/_generic.collision.html

+<tal:block metal:use-macro="templates/page/macros/icing">
+<title metal:fill-slot="head_title" i18n:translate=""
+ ><span tal:replace="python:context._classname.capitalize()"
+ i18n:name="class" /> Edit Collision - <span i18n:name="tracker"
+ tal:replace="config/TRACKER_NAME" /></title>
+<tal:block metal:fill-slot="body_title" i18n:translate=""
+ ><span tal:replace="python:context._classname.capitalize()"
+ i18n:name="class" /> Edit Collision</tal:block>
+
+<td class="content" metal:fill-slot="content" i18n:translate="
+  There has been a collision. Another user updated this node
+  while you were editing. Please <a href='${context}'>reload</a>
+  the node and review your edits.
+"><span tal:replace="context/designator" i18n:name="context" />
+</td>
+</tal:block>
+<!-- SHA: db15fb6c88215d4baf223910a6c9cd81c63dc994 -->

html/_generic.help-empty.html

+<html>
+  <head>
+    <title>Empty page (no search performed yet)</title>
+  </head>
+  <body>
+    <p i18n:translate="">Please specify your search parameters!</p>
+  </body>
+</html>
+<!-- SHA: 9a118377b03172347d95097ff75fca26a6dd3738 -->

html/_generic.help-list.html

+<!-- $Id: _generic.help-list.html,v 1.1 2006/09/18 00:03:02 tobias-herp Exp $ vim: sw=2 ts=8 et
+--><html tal:define="vok context/is_view_ok">
+  <head>
+    <title>Search result for user helper</title>
+    <link rel="stylesheet" type="text/css" href="@@file/style.css" />
+    <script language="Javascript" type="text/javascript"
+        tal:content="structure string:<!--
+        // this is the name of the field in the original form that we're working on
+        form  = parent.opener.document.${request/form/form/value};
+        field  = '${request/form/property/value}';
+    //-->"></script>
+    <script src="@@file/help_controls.js" type="text/javascript"></script>
+<script type="text/javascript"><!--
+var text_field = parent.submit.document.frm_help.text_preview;
+//--></script>
+  </head>
+  <body>
+    <pre tal:content="request/env/QUERY_STRING" tal:condition=false />
+
+  <p tal:condition="not:vok" i18n:translate="">You are not
+  allowed to view this page.</p>
+
+  <tal:if condition="context/is_view_ok">
+  <tal:def define="batch request/batch;">
+  <form name=dummyform>
+    <table width="100%"
+      tal:define="template string:help-list"
+      metal:use-macro="templates/help/macros/batch_navi"
+      >
+      <tr class="navigation">
+       <th>
+        <a href="#">&lt;&lt; previous</a>
+       </th>
+       <th i18n:translate="">1..25 out of 50
+       </th>
+       <th>
+        <a href="#">next &gt;&gt;</a>
+       </th>
+      </tr>
+     </table>
+
+  <form name=dummyform>
+  <table class="classhelp"
+    tal:define="
+       props python:request.form['properties'].value.split(',');
+       legend templates/help/macros/legend;
+    "><thead>
+      <tr metal:use-macro="legend">
+         <th>&nbsp;<b>x</b></th>
+         <th tal:repeat="prop props" tal:content="prop" i18n:translate=""></th>
+       </tr>
+     </thead>
+     <tfoot tal:condition=true>
+       <tr metal:use-macro="legend" />
+     </tfoot>
+     <tbody>
+       <tr tal:repeat="item batch">
+         <tal:block tal:define="attr python:item[props[0]]" >
+           <td>
+             <input name="check"
+             onclick="switch_val(text_field, this);" type="checkbox"
+             tal:attributes="value attr; id string:id_$attr" />
+             </td>
+             <td tal:repeat="prop props">
+                 <label class="classhelp-label"
+                        tal:attributes="for string:id_$attr"
+                        tal:content="structure python:item[prop]"></label>
+             </td>
+           </tal:block>
+         </tr>
+       </tbody>
+     </table>
+   </form>
+     </tal:def>
+     </tal:if>
+     
+     <pre tal:content=request tal:condition=false />
+     <script type="text/javascript"><!--
+       parent.submit.document.frm_help.cb_listpresent.checked=true;
+       reviseList_framed(document.dummyform, text_field)
+     //--></script>
+  </body>
+</html>
+<!-- SHA: 49245b7c26aef974103dda39eb2d6c87a3234ac2 -->

html/_generic.help-search.html

+<html>
+  <head>
+    <title>Frame for search input fields</title>
+  </head>
+  <body>
+    <p i18n:translate="">Generic template
+    <span tal:replace="request/template" i18n:name="template">help-search</span>
+    or version for class
+    <span tal:replace="request/classname" i18n:name="classname">user</span>
+    is not yet implemented</p>
+  </body>
+</html>
+
+<!-- SHA: b95a7bda7189c0747d2f4112d1d3d02808fd1753 -->

html/_generic.help-submit.html

+<html>
+  <head>
+      <link rel="stylesheet" type="text/css" href="@@file/style.css" />
+      <meta http-equiv="Content-Type"
+       tal:attributes="content string:text/html;; charset=${request/client/charset}" />
+      <tal:block tal:condition="python:request.form.has_key('property')">
+      <title>Generic submit page for framed helper windows</title>
+      <script language="Javascript" type="text/javascript"
+          tal:content="structure string:<!--
+// this is the name of the field in the original form that we're working on
+form  = parent.opener.document.${request/form/form/value};
+callingform=form
+field  = '${request/form/property/value}';
+var listform = null
+function listPresent() {
+  return document.frm_help.cb_listpresent.checked
+}
+function getListForm() {
+  if (listPresent()) {
+    return parent.list.document.forms.dummyform
+  } else {
+    return null
+  }
+}
+
+
+function checkListForm() {
+  // global listform
+  if (listform != null)
+    if (parent.list.document.dummyform) {
+      listform = parent.list.document.dummyform
+      alert(listform)
+    }
+
+  var bol= listform != null
+  alert('checkListForm: bol='+bol)
+  return bol
+}
+//-->">
+      </script>
+      <script src="@@file/help_controls.js" type="text/javascript"></script>
+      </tal:block>
+  </head>
+ <body class="body" onload="parent.focus();" id="submit">
+ <pre tal:content="request/env/QUERY_STRING" tal:condition=false />
+ <form name="frm_help"
+       tal:define="batch request/batch;
+       props python:request.form['properties'].value.split(',')"
+       class="help-submit"
+       id="classhelp-controls">
+     <div style="width:100%;text-align:left;margin-bottom:0.2em">
+       <input type="text" name="text_preview" size="24" class="preview"
+       onchange="f=getListForm();if(f){ reviseList_framed(f, this)};"
+       />
+     </div>
+     <input type=checkbox name="cb_listpresent" readonly="readonly" style="display:none">
+     <input type="button" id="btn_cancel"
+            value=" Cancel " onclick="parent.close();return false;"
+            i18n:attributes="value" />
+     <input type="reset" id="btn_reset"
+     onclick="text_field.value=original_field;f=getListForm();if (f) {reviseList_framed(f, this)};return false"
+            />
+     <input type="submit" id="btn_apply" class="apply"
+            value=" Apply " onclick="callingform[field].value=text_field.value; parent.close();"
+            i18n:attributes="value" />
+ </form>
+ <script type="text/javascript"><!--
+var text_field = document.frm_help.text_preview;
+original_field=form[field].value;
+text_field.value=original_field;
+//--></script>
+ </body>
+</html>
+<!-- SHA: 1de39ac0d15dc59c64187b6c691d58ba20931372 -->

html/_generic.help.html

+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html tal:define="property request/form/property/value" >
+  <head>
+      <link rel="stylesheet" type="text/css" href="@@file/style.css" />
+      <meta http-equiv="Content-Type"
+       tal:attributes="content string:text/html;; charset=${request/client/charset}" />
+      <tal:block tal:condition="python:request.form.has_key('property')">
+      <title i18n:translate=""><tal:x i18n:name="property"
+       tal:content="property" i18n:translate="" /> help - <span i18n:name="tracker"
+       tal:replace="config/TRACKER_NAME" /></title>
+      <script language="Javascript" type="text/javascript"
+          tal:content="structure string:
+          // this is the name of the field in the original form that we're working on
+          form  = window.opener.document.${request/form/form/value};
+          field  = '${request/form/property/value}';">
+      </script>
+      <script src="@@file/help_controls.js" type="text/javascript"><!--
+      //--></script>
+      </tal:block>
+  </head>
+ <body class="body" onload="resetList();">
+ <form name="frm_help" tal:attributes="action request/base"
+       tal:define="batch request/batch;
+                   props python:request.form['properties'].value.split(',')">
+
+     <div id="classhelp-controls">
+       <!--input type="button" name="btn_clear"
+              value="Clear" onClick="clearList()"/ -->
+       <input type="text" name="text_preview" size="24" class="preview"
+              onchange="reviseList(this.value);"/>
+       <input type="button" name="btn_reset"
+              value=" Cancel " onclick="resetList(); window.close();"
+              i18n:attributes="value" />
+       <input type="button" name="btn_apply" class="apply"
+              value=" Apply " onclick="updateList(); window.close();"
+              i18n:attributes="value" />
+     </div>
+     <table width="100%">
+      <tr class="navigation">
+       <th>
+        <a tal:define="prev batch/previous" tal:condition="prev"
+           tal:attributes="href python:request.indexargs_url(request.classname,
+           {'@template':'help', 'property': request.form['property'].value,
+            'properties': request.form['properties'].value,
+            'form': request.form['form'].value,
+            'type': request.form['type'].value,
+            '@startwith':prev.first, '@pagesize':prev.size})"
+           i18n:translate="" >&lt;&lt; previous</a>
+        &nbsp;
+       </th>
+       <th i18n:translate=""><span tal:replace="batch/start" i18n:name="start"
+        />..<span tal:replace="python: batch.start + batch.length -1" i18n:name="end"
+        /> out of <span tal:replace="batch/sequence_length" i18n:name="total"
+        />
+       </th>
+       <th>
+        <a tal:define="next batch/next" tal:condition="next"
+           tal:attributes="href python:request.indexargs_url(request.classname,
+           {'@template':'help', 'property': request.form['property'].value,
+            'properties': request.form['properties'].value,
+            'form': request.form['form'].value,
+            'type': request.form['type'].value,
+            '@startwith':next.first, '@pagesize':next.size})"
+           i18n:translate="" >next &gt;&gt;</a>
+        &nbsp;
+       </th>
+      </tr>
+     </table>
+
+     <table class="classhelp">
+       <tr>
+           <th>&nbsp;<b>x</b></th>
+           <th tal:repeat="prop props" tal:content="prop" i18n:translate=""></th>
+       </tr>
+       <tr tal:repeat="item batch">
+         <tal:block tal:define="attr python:item[props[0]]" >
+           <td>
+             <input name="check"
+                 onclick="updatePreview();"
+                 tal:attributes="type python:request.form['type'].value;
+                                 value attr; id string:id_$attr" />
+             </td>
+             <td tal:repeat="prop props">
+                 <label class="classhelp-label"
+                        tal:attributes="for string:id_$attr"
+                        tal:content="structure python:item[prop]"></label>
+             </td>
+           </tal:block>
+       </tr>
+       <tr>
+           <th>&nbsp;<b>x</b></th>
+           <th tal:repeat="prop props" tal:content="prop" i18n:translate=""></th>
+       </tr>
+     </table>
+
+ </form>
+ </body>
+</html>
+<!-- SHA: 10ab79422912c5dab17ede05ca8293133eb76951 -->

html/_generic.index.html

+<!-- dollarId: issue.index,v 1.2 2001/07/29 04:07:37 richard Exp dollar-->
+
+<tal:block metal:use-macro="templates/page/macros/icing">
+<title metal:fill-slot="head_title" i18n:translate=""
+ ><span tal:replace="python:context._classname.capitalize()"
+ i18n:name="class" /> editing - <span i18n:name="tracker"
+ tal:replace="config/TRACKER_NAME" /></title>
+<tal:block metal:fill-slot="body_title" i18n:translate=""
+ ><span tal:replace="python:context._classname.capitalize()"
+ i18n:name="class" /> editing</tal:block>
+
+<td class="content" metal:fill-slot="content">
+
+<span tal:condition="python:not (context.is_view_ok() or context.is_edit_ok()
+ or request.user.hasRole('Anonymous'))"
+ tal:omit-tag="python:1" i18n:translate=""
+>You are not allowed to view this page.</span>
+
+<span tal:condition="python:not (context.is_view_ok() or context.is_edit_ok())
+ and request.user.hasRole('Anonymous')"
+ tal:omit-tag="python:1" i18n:translate=""
+>Please login with your username and password.</span>
+
+<tal:block tal:condition="context/is_edit_ok">
+<tal:block i18n:translate="">
+<p class="form-help">
+ You may edit the contents of the
+ <span tal:replace="request/classname" i18n:name="classname"/>
+ class using this form. Commas, newlines and double quotes (") must be
+ handled delicately. You may include commas and newlines by enclosing the
+ values in double-quotes ("). Double quotes themselves must be quoted by
+ doubling ("").
+</p>
+
+<p class="form-help">
+ Multilink properties have their multiple values colon (":") separated
+ (... ,"one:two:three", ...)
+</p>
+
+<p class="form-help">
+ Remove entries by deleting their line. Add new entries by appending
+ them to the table - put an X in the id column.
+</p>
+</tal:block>
+<form onSubmit="return submit_once()" method="POST"
+      tal:attributes="action context/designator">
+<textarea rows="15" style="width:90%" name="rows" tal:content="context/csv"></textarea>
+<br>
+<input type="hidden" name="@action" value="editCSV">
+<input type="submit" value="Edit Items" i18n:attributes="value">
+</form>
+</tal:block>
+
+<table tal:condition="context/is_only_view_ok" width="100%" class="list">
+ <tr>
+  <th tal:repeat="property context/propnames" tal:content="property">&nbsp;</th>
+ </tr>
+ <tal:block repeat="item context/list">
+ <tr tal:condition="item/is_view_ok"
+     tal:attributes="class python:['normal', 'alt'][repeat['item'].index%6/3]">
+  <td tal:repeat="property context/propnames"
+   tal:content="python: item[property] or default"
+  >&nbsp;</td>
+ </tr>
+ </tal:block>
+</table>
+
+</td>
+
+</tal:block>
+<!-- SHA: 822d4da93deeec71c56224d26ad970a2b141cdd1 -->

html/_generic.item.html

+<tal:block metal:use-macro="templates/page/macros/icing">
+<title metal:fill-slot="head_title" i18n:translate=""
+ ><span tal:replace="python:context._classname.capitalize()"
+ i18n:name="class" /> editing - <span i18n:name="tracker"
+ tal:replace="config/TRACKER_NAME" /></title>
+<tal:block metal:fill-slot="body_title" i18n:translate=""
+ ><span tal:replace="python:context._classname.capitalize()"
+ i18n:name="class" /> editing</tal:block>
+
+<td class="content" metal:fill-slot="content">
+
+<p tal:condition="python:not (context.is_view_ok()
+ or request.user.hasRole('Anonymous'))" i18n:translate="">
+ You are not allowed to view this page.</p>
+
+<p tal:condition="python:not context.is_view_ok()
+ and request.user.hasRole('Anonymous')" i18n:translate="">
+ Please login with your username and password.</p>
+
+<div tal:condition="context/is_view_ok">
+
+<form method="POST" onSubmit="return submit_once()"
+      enctype="multipart/form-data" tal:condition="context/is_view_ok"
+      tal:attributes="action context/designator">
+
+<input type="hidden" name="@template" value="item">
+
+<table class="form">
+
+<tr tal:repeat="prop python:db[context._classname].properties()">
+ <tal:block tal:condition="python:prop._name not in ('id',
+   'creator', 'creation', 'actor', 'activity')">
+  <th tal:content="prop/_name"></th>
+  <td tal:content="structure python:context[prop._name].field()"></td>
+ </tal:block>
+</tr>
+<tr>
+ <td>&nbsp;</td>
+ <td colspan=3 tal:content="structure context/submit">
+  submit button will go here
+ </td>
+</tr>
+</table>
+
+</form>
+
+<tal:block tal:condition="context/id" tal:replace="structure context/history" />
+
+</div>
+
+</td>
+
+</tal:block>
+<!-- SHA: 62cac36f8fe4e3705913e32fc0e91f803e8b0167 -->

html/file.index.html

+<!-- dollarId: file.index,v 1.4 2002/01/23 05:10:27 richard Exp dollar-->
+<tal:block metal:use-macro="templates/page/macros/icing">
+<title metal:fill-slot="head_title" i18n:translate=""
+ >List of files - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker" /></title>
+<span metal:fill-slot="body_title" tal:omit-tag="python:1"
+  i18n:translate="">List of files</span>
+<td class="content" metal:fill-slot="content">
+
+<table class="otherinfo" tal:define="batch request/batch">
+ <tr><th style="padding-right: 10" i18n:translate="">Download</th>
+     <th style="padding-right: 10" i18n:translate="">Content Type</th>
+     <th style="padding-right: 10" i18n:translate="">Uploaded By</th>
+     <th style="padding-right: 10" i18n:translate="">Date</th>
+ </tr>
+ <tr tal:repeat="file batch" tal:attributes="class python:['normal', 'alt'][repeat['file'].index%6/3]">
+  <td>
+   <a tal:attributes="href string:file${file/id}/${file/name}"
+      tal:content="file/name">dld link</a>
+  </td>
+  <td tal:content="file/type">content type</td>
+  <td tal:content="file/creator">creator's name</td>
+  <td tal:content="file/creation">creation date</td>
+ </tr>
+
+ <metal:block use-macro="templates/issue.index/macros/batch-footer" />
+
+</table>
+
+</td>
+
+</tal:block>
+<!-- SHA: f3a343f0682801cb8c47bd793d2d436fc7258d73 -->

html/file.item.html

+<tal:block metal:use-macro="templates/page/macros/icing">
+<title metal:fill-slot="head_title" i18n:translate="">File display - <span
+ i18n:name="tracker" tal:replace="config/TRACKER_NAME" /></title>
+<span metal:fill-slot="body_title" tal:omit-tag="python:1"
+ i18n:translate="">File display</span>
+
+<td class="content" metal:fill-slot="content">
+
+<p tal:condition="python:not (context.is_view_ok()
+ or request.user.hasRole('Anonymous'))" i18n:translate="">
+ You are not allowed to view this page.</p>
+
+<p tal:condition="python:not context.is_view_ok()
+ and request.user.hasRole('Anonymous')" i18n:translate="">
+ Please login with your username and password.</p>
+
+<form method="POST" onSubmit="return submit_once()"
+      enctype="multipart/form-data" tal:condition="context/is_view_ok"
+      tal:attributes="action context/designator">
+
+<table class="form">
+ <tr>
+  <th i18n:translate="">Name</th>
+  <td tal:content="structure context/name/field"></td>
+ </tr>
+ <tr>
+  <th i18n:translate="">Content Type</th>
+  <td tal:content="structure context/type/field"></td>
+ </tr>
+
+ <tr>
+  <td>
+   &nbsp;
+   <input type="hidden" name="@template" value="item">
+   <input type="hidden" name="@required" value="name,type">
+   <input type="hidden" name="@multilink"
+          tal:condition="python:request.form.has_key('@multilink')"
+          tal:attributes="value request/form/@multilink/value">
+  </td>
+  <td tal:content="structure context/submit">submit button here</td>
+ </tr>
+</table>
+</form>
+
+<a tal:condition="python:context.id and context.is_view_ok()"
+ tal:attributes="href string:file${context/id}/${context/name}"
+ i18n:translate="">download</a>
+
+<tal:block tal:condition="context/id" tal:replace="structure context/history" />
+
+</td>
+
+</tal:block>
+<!-- SHA: 9b0955c553e1df1d791dfc24530aeb945896cf46 -->
+<!--
+Macros for framed help windows
+-->
+
+<!-- legend for helper search results -->
+<thead>
+<tr metal:define-macro="legend">
+  <th><b>x</b></th>
+  <th tal:repeat="prop props" tal:content="prop" i18n:translate=""></th>
+</tr>
+</thead>
+
+<table width="100%"
+  metal:define-macro="batch_navi"
+  tal:define="prev batch/previous;
+  next batch/next;
+  "
+  tal:condition="python:prev or next">
+  <tr class="navigation">
+   <th width="30%">
+    <a tal:condition="prev"
+       tal:attributes="href python:request.indexargs_url(request.classname, {'@template':'help-list', 'property': request.form['property'].value, 'properties': request.form['properties'].value, 'form': request.form['form'].value, '@startwith':prev.first, '@pagesize':prev.size})"
+       i18n:translate="" >&lt;&lt; previous</a>
+    &nbsp;
+   </th>
+   <th i18n:translate="" width="40%"><span tal:replace="batch/start" i18n:name="start"
+    />..<span tal:replace="python: batch.start + batch.length -1" i18n:name="end"
+    /> out of <span tal:replace="batch/sequence_length" i18n:name="total"
+    />
+   </th>
+   <th width="30%">
+    <a tal:condition="next"
+       tal:attributes="href python:request.indexargs_url(request.classname, {'@template':'help-list', 'property': request.form['property'].value, 'properties': request.form['properties'].value, 'form': request.form['form'].value, '@startwith':next.first, '@pagesize':next.size})"
+       i18n:translate="" >next &gt;&gt;</a>
+    &nbsp;
+   </th>
+  </tr>
+ </table>
+<!-- SHA: 5bb5e9db92d4dea06f6bd0224f34dce86020d4c2 -->

html/help_controls.js

+// initial values for either Nosy, Superseder, Keyword and Waiting On,
+// depending on which has called
+original_field = form[field].value;
+
+// Some browsers (ok, IE) don't define the "undefined" variable.
+undefined = document.geez_IE_is_really_friggin_annoying;
+
+function trim(value) {
+  var temp = value;
+  var obj = /^(\s*)([\W\w]*)(\b\s*$)/;
+  if (obj.test(temp)) { temp = temp.replace(obj, '$2'); }
+  var obj = /  /g;
+  while (temp.match(obj)) { temp = temp.replace(obj, " "); }
+  return temp;
+}
+
+function determineList() {
+     // generate a comma-separated list of the checked items
+     var list = new String('');
+
+     // either a checkbox object or an array of checkboxes
+     var check = document.frm_help.check;
+
+     if ((check.length == undefined) && (check.checked != undefined)) {
+         // only one checkbox on page
+         if (check.checked) {
+             list = check.value;
+         }
+     } else {
+         // array of checkboxes
+         for (box=0; box < check.length; box++) {
+             if (check[box].checked) {
+                 if (list.length == 0) {
+                     separator = '';
+                 }
+                 else {
+                     separator = ',';
+                 }
+                 // we used to use an Array and push / join, but IE5.0 sux
+                 list = list + separator + check[box].value;
+             }
+         }
+     }
+     return list;
+}
+
+/**
+ * update the field in the opening window;
+ * the text_field variable must be set in the calling page
+ */
+function updateOpener() {
+  // write back to opener window
+  if (document.frm_help.check==undefined) { return; }
+  form[field].value = text_field.value;
+}
+
+function updateList() {
+  // write back to opener window
+  if (document.frm_help.check==undefined) { return; }
+  form[field].value = determineList();
+}
+
+function updatePreview() {
+  // update the preview box
+  if (document.frm_help.check==undefined) { return; }
+  writePreview(determineList());
+}
+
+function clearList() {
+  // uncheck all checkboxes
+  if (document.frm_help.check==undefined) { return; }
+  for (box=0; box < document.frm_help.check.length; box++) {
+      document.frm_help.check[box].checked = false;
+  }
+}
+
+function reviseList_framed(form, textfield) {
+  // update the checkboxes based on the preview field
+  // alert('reviseList_framed')
+  // alert(form)
+  if (form.check==undefined)
+      return;
+  // alert(textfield)
+  var to_check;
+  var list = textfield.value.split(",");
+  if (form.check.length==undefined) {
+      check = form.check;
+      to_check = false;
+      for (val in list) {
+          if (check.value==trim(list[val])) {
+              to_check = true;
+              break;
+          }
+      }
+      check.checked = to_check;
+  } else {
+    for (box=0; box < form.check.length; box++) {
+      check = form.check[box];
+      to_check = false;
+      for (val in list) {
+          if (check.value==trim(list[val])) {
+              to_check = true;
+              break;
+          }
+      }
+      check.checked = to_check;
+    }
+  }
+}
+
+function reviseList(vals) {
+  // update the checkboxes based on the preview field
+  if (document.frm_help.check==undefined) { return; }
+  var to_check;
+  var list = vals.split(",");
+  if (document.frm_help.check.length==undefined) {
+      check = document.frm_help.check;
+      to_check = false;
+      for (val in list) {
+          if (check.value==trim(list[val])) {
+              to_check = true;
+              break;
+          }
+      }
+      check.checked = to_check;
+  } else {
+    for (box=0; box < document.frm_help.check.length; box++) {
+      check = document.frm_help.check[box];
+      to_check = false;
+      for (val in list) {
+          if (check.value==trim(list[val])) {
+              to_check = true;
+              break;
+          }
+      }
+      check.checked = to_check;
+    }
+  }
+}
+
+function resetList() {
+  // reset preview and check boxes to initial values
+  if (document.frm_help.check==undefined) { return; }
+  writePreview(original_field);
+  reviseList(original_field);
+}
+
+function writePreview(val) {
+   // writes a value to the text_preview
+   document.frm_help.text_preview.value = val;
+}
+
+function focusField(name) {
+    for(i=0; i < document.forms.length; ++i) {
+      var obj = document.forms[i].elements[name];
+      if (obj && obj.focus) {obj.focus();}
+    }
+}
+
+function selectField(name) {
+    for(i=0; i < document.forms.length; ++i) {
+      var obj = document.forms[i].elements[name];
+      if (obj && obj.focus){obj.focus();}
+      if (obj && obj.select){obj.select();}
+    }
+}
+
+function checkRequiredFields(fields)
+{
+    var bonk='';
+    var res='';
+    var argv = checkRequiredFields.arguments;
+    var argc = argv.length;
+    var input = '';
+    var val='';
+
+    for (var i=0; i < argc; i++) {
+        fi = argv[i];
+        input = document.getElementById(fi);
+        if (input) {
+            val = input.value
+            if (val == '' || val == '-1' || val == -1) {
+                if (res == '') {
+                    res = fi;
+                    bonk = input;
+                } else {
+                    res += ', '+fi;
+                }
+            }
+        } else {
+            alert('Field with id='+fi+' not found!')
+        }
+    }
+    if (res == '') {
+        return submit_once();
+    } else {
+        alert('Missing value here ('+res+')!');
+        if (window.event && window.event.returnvalue) {
+            event.returnValue = 0;    // work-around for IE
+        }
+        bonk.focus();
+        return false;
+    }
+}
+
+/**
+ * seeks the given value (2nd argument)
+ * in the value of the given input element (1st argument),
+ * which is considered a list of values, separated by commas
+ */
+function has_value(input, val)
+{
+    var actval = input.value
+    var arr = feld.value.split(',');
+    var max = arr.length;
+    for (i=0;i<max;i++) {
+        if (trim(arr[i]) == val) {
+            return true
+        }
+    }
+    return false
+}
+
+/**
+ * Switch Value:
+ * change the value of the given input field (might be of type text or hidden),
+ * adding or removing the value of the given checkbox field (might be a radio
+ * button as well)
+ *
+ * This function doesn't care whether or not the checkboxes of all values of
+ * interest are present; but of course it doesn't have total control of the
+ * text field.
+ */
+function switch_val(text, check)
+{
+    var switched_val = check.value
+    var arr = text.value.split(',')
+    var max = arr.length
+    if (check.checked) {
+        for (i=0; i<max; i++) {
+            if (trim(arr[i]) == switched_val) {
+                return
+            }
+        }
+	if (text.value)
+            text.value = text.value+','+switched_val
+	else
+            text.value = switched_val
+    } else {
+        var neu = ''
+	var changed = false
+        for (i=0; i<max; i++) {
+            if (trim(arr[i]) == switched_val) {
+                changed=true
+            } else {
+                neu = neu+','+trim(arr[i])
+            }
+        }
+        if (changed) {
+            text.value = neu.substr(1)
+        }
+    }
+}
+
+/**
+ * append the given value (2nd argument) to an input field
+ * (1st argument) which contains comma-separated values;
+ * see --> remove_val()
+ *
+ * This will work nicely even for batched lists
+ */
+function append_val(name, val)
+{
+    var feld = document.itemSynopsis[name];
+    var actval = feld.value;
+    if (actval == '') {
+        feld.value = val
+    } else {
+        var arr = feld.value.split(',');
+        var max = arr.length;
+        for (i=0;i<max;i++) {
+            if (trim(arr[i]) == val) {
+                return
+            }
+        }
+        feld.value = actval+','+val
+    }
+}
+
+/**
+ * remove the given value (2nd argument) from the comma-separated values
+ * of the given input element (1st argument); see --> append_val()
+ */
+function remove_val(name, val)
+{
+    var feld = document.itemSynopsis[name];
+    var actval = feld.value;
+    var changed=false;
+    if (actval == '') {
+	return
+    } else {
+        var arr = feld.value.split(',');
+        var max = arr.length;
+        var neu = ''
+        for (i=0;i<max;i++) {
+            if (trim(arr[i]) == val) {
+                changed=true
+            } else {
+                neu = neu+','+trim(arr[i])
+            }
+        }
+        if (changed) {
+            feld.value = neu.substr(1)
+        }
+    }
+}
+
+/**
+ * give the focus to the element given by id
+ */
+function focus2id(name)
+{
+    document.getElementById(name).focus();
+}

html/home.classlist.html

+<tal:block metal:use-macro="templates/page/macros/icing">
+<title metal:fill-slot="head_title" i18n:translate="">List of classes - <span
+ i18n:name="tracker" tal:replace="config/TRACKER_NAME" /></title>
+<span metal:fill-slot="body_title" tal:omit-tag="python:1"
+ i18n:translate="">List of classes</span>
+<td class="content" metal:fill-slot="content">
+<table class="classlist">
+
+<tal:block tal:repeat="cl db/classes">
+ <tr>
+  <th class="header" colspan="2" align="left">
+   <a tal:attributes="href string:${cl/classname}"
+      tal:content="python:cl.classname.capitalize()">classname</a>
+  </th>
+ </tr>
+ <tr tal:repeat="prop cl/properties">
+  <th tal:content="prop/_name">name</th>
+  <td tal:content="prop/_prop">type</td>
+ </tr>
+</tal:block>
+
+</table>
+</td>
+
+</tal:block>
+<!-- SHA: e82456270ae1048cefdead99afda95578fff7b74 -->
+<!--
+ This is the default body that is displayed when people visit the
+ tracker. The tag below lists the currently open issues. You may
+ replace it with a greeting message, or a different list of issues or
+ whatever. It's a good idea to have the issues on the front page though
+-->
+<span tal:replace="structure python:db.issue.renderWith('index',
+    sort=[('-', 'activity')], group=[('+', 'priority')], filter=['status'],
+    columns=['id','activity','title','creator','assignedto', 'status'],
+    filterspec={'status':['-1','1','2','3','4','5','6','7']})" />
+<!-- SHA: c87a4e18d59a527331f1d367c0c6cc67ee123e63 -->

html/issue.index.html

+<!-- $Id: issue.index.html,v 1.29 2007/09/18 17:44:26 jpend Exp $ -->
+<tal:block metal:use-macro="templates/page/macros/icing">
+<title metal:fill-slot="head_title" >
+  <span tal:omit-tag="true" i18n:translate="" >List of issues</span>
+  <span tal:condition="request/dispname"
+   tal:replace="python:' - %s '%request.dispname"
+  /> - <span tal:replace="config/TRACKER_NAME" />
+</title>
+<span metal:fill-slot="body_title" tal:omit-tag="true">
+  <span tal:omit-tag="true" i18n:translate="" >List of issues</span>
+  <span tal:condition="request/dispname"
+   tal:replace="python:' - %s' % request.dispname" />
+</span>
+<td class="content" metal:fill-slot="content">
+
+<p tal:condition="python:not (context.is_view_ok()
+ or request.user.hasRole('Anonymous'))" i18n:translate="">
+ You are not allowed to view this page.</p>
+
+<p tal:condition="python:not context.is_view_ok()
+ and request.user.hasRole('Anonymous')" i18n:translate="">
+ Please login with your username and password.</p>
+
+<tal:block tal:define="batch request/batch" tal:condition="context/is_view_ok">
+ <table class="list">
+  <tr>
+   <th tal:condition="request/show/priority" i18n:translate="">Priority</th>
+   <th tal:condition="request/show/id" i18n:translate="">ID</th>
+   <th tal:condition="request/show/creation" i18n:translate="">Creation</th>
+   <th tal:condition="request/show/activity" i18n:translate="">Activity</th>
+   <th tal:condition="request/show/actor" i18n:translate="">Actor</th>
+   <th tal:condition="request/show/keyword" i18n:translate="">Keyword</th>
+   <th tal:condition="request/show/title" i18n:translate="">Title</th>
+   <th tal:condition="request/show/status" i18n:translate="">Status</th>
+   <th tal:condition="request/show/creator" i18n:translate="">Creator</th>
+   <th tal:condition="request/show/assignedto" i18n:translate="">Assigned&nbsp;To</th>
+  </tr>
+ <tal:block tal:repeat="i batch" condition=true>
+  <tr tal:define="group python:[r[1] for r in request.group]"
+      tal:condition="python:group and batch.propchanged(*group)">
+   <th tal:attributes="colspan python:len(request.columns)" class="group">
+    <tal:block tal:repeat="g group">
+     <tal:block i18n:translate="" tal:content="python:str(i[g]) or '(no %s set)'%g"/>
+    </tal:block>
+   </th>
+  </tr>
+
+  <tr>
+   <td tal:condition="request/show/priority"
+       tal:content="python:i.priority.plain() or default">&nbsp;</td>
+   <td tal:condition="request/show/id" tal:content="i/id">&nbsp;</td>
+   <td class="date" tal:condition="request/show/creation"
+       tal:content="i/creation/reldate">&nbsp;</td>
+   <td class="date" tal:condition="request/show/activity"
+       tal:content="i/activity/reldate">&nbsp;</td>
+   <td class="date" tal:condition="request/show/actor"
+       tal:content="python:i.actor.plain() or default">&nbsp;</td>
+   <td tal:condition="request/show/keyword"
+       tal:content="python:i.keyword.plain() or default">&nbsp;</td>
+   <td tal:condition="request/show/title">
+    <a tal:attributes="href string:issue${i/id}"
+		tal:content="python:str(i.title.plain(hyperlink=0)) or '[no title]'">title</a>
+   </td>
+   <td tal:condition="request/show/status"
+       i18n:translate=""
+       tal:content="python:i.status.plain() or default">&nbsp;</td>
+   <td tal:condition="request/show/creator"
+       tal:content="python:i.creator.plain() or default">&nbsp;</td>
+   <td tal:condition="request/show/assignedto"
+       tal:content="python:i.assignedto.plain() or default">&nbsp;</td>
+  </tr>
+
+ </tal:block>
+
+ <metal:index define-macro="batch-footer">
+ <tr tal:condition="batch">
+  <th tal:attributes="colspan python:len(request.columns)">
+   <table width="100%">
+    <tr class="navigation">
+     <th>
+      <a tal:define="prev batch/previous" tal:condition="prev"
+         tal:attributes="href python:request.indexargs_url(request.classname,
+         {'@startwith':prev.first, '@pagesize':prev.size})"
+         i18n:translate="">&lt;&lt; previous</a>
+      &nbsp;
+     </th>
+     <th i18n:translate=""><span tal:replace="batch/start" i18n:name="start"
+     />..<span tal:replace="python: batch.start + batch.length -1" i18n:name="end"
+     /> out of <span tal:replace="batch/sequence_length" i18n:name="total"
+     /></th>
+     <th>
+      <a tal:define="next batch/next" tal:condition="next"
+         tal:attributes="href python:request.indexargs_url(request.classname,
+         {'@startwith':next.first, '@pagesize':next.size})"
+         i18n:translate="">next &gt;&gt;</a>
+      &nbsp;
+     </th>
+    </tr>
+   </table>
+  </th>
+ </tr>
+ </metal:index>
+</table>
+
+<a tal:attributes="href python:request.indexargs_url('issue',
+            {'@action':'export_csv'})" i18n:translate="">Download as CSV</a>
+
+<form method="GET" class="index-controls"
+    tal:attributes="action request/classname">
+
+ <table class="form" tal:define="n_sort python:2">
+  <tal:block tal:repeat="n python:range(n_sort)" tal:condition="batch">
+  <tr tal:define="key python:len(request.sort)>n and request.sort[n]">
+   <th>
+    <tal:block tal:condition="not:n" i18n:translate="">Sort on:</tal:block>
+   </th>
+   <td>
+    <select tal:attributes="name python:'@sort%d'%n">
+     <option value="" i18n:translate="">- nothing -</option>
+     <option tal:repeat="col context/properties"
+             tal:attributes="value col/_name;
+                             selected python:key and col._name == key[1]"
+             tal:content="col/_name"
+             i18n:translate="">column</option>
+    </select>
+   </td>
+   <th i18n:translate="">Descending:</th>
+   <td><input type="checkbox" tal:attributes="name python:'@sortdir%d'%n;
+              checked python:key and key[0] == '-'">
+   </td>
+  </tr>
+  </tal:block>
+  <tal:block tal:repeat="n python:range(n_sort)" tal:condition="batch">
+  <tr tal:define="key python:len(request.group)>n and request.group[n]">
+   <th>
+    <tal:block tal:condition="not:n" i18n:translate="">Group on:</tal:block>
+   </th>
+   <td>
+    <select tal:attributes="name python:'@group%d'%n">
+     <option value="" i18n:translate="">- nothing -</option>
+     <option tal:repeat="col context/properties"
+             tal:attributes="value col/_name;
+                             selected python:key and col._name == key[1]"
+             tal:content="col/_name"
+             i18n:translate="">column</option>
+    </select>
+   </td>
+   <th i18n:translate="">Descending:</th>
+   <td><input type="checkbox" tal:attributes="name python:'@groupdir%d'%n;
+              checked python:key and key[0] == '-'">
+   </td>
+  </tr>
+  </tal:block>
+  <tr><td colspan="4">
+              <input type="submit" value="Redisplay" i18n:attributes="value">
+              <tal:block tal:replace="structure
+                python:request.indexargs_form(sort=0, group=0)" />
+  </td></tr>
+ </table>
+</form>
+
+</tal:block>
+
+</td>
+</tal:block><tal:comment condition=false> vim: sw=1 ts=8 et si
+</tal:comment>
+<!-- SHA: 5a8b0dc29ea78a88b54db4eecb0fe3198e836a95 -->

html/issue.item.html

+<!-- dollarId: issue.item,v 1.4 2001/08/03 01:19:43 richard Exp dollar-->
+<tal:block metal:use-macro="templates/page/macros/icing">
+<title metal:fill-slot="head_title">
+<tal:block condition="context/id" i18n:translate=""
+ >Issue <span tal:replace="context/id" i18n:name="id"
+ />: <span tal:replace="context/title" i18n:name="title"
+ /> - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker"
+/></tal:block>
+<tal:block condition="not:context/id" i18n:translate=""
+ >New Issue - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker"
+/></tal:block>
+</title>
+<tal:block metal:fill-slot="body_title">
+ <span tal:condition="python: not (context.id or context.is_edit_ok())"
+  tal:omit-tag="python:1" i18n:translate="">New Issue</span>
+ <span tal:condition="python: not context.id and context.is_edit_ok()"
+  tal:omit-tag="python:1" i18n:translate="">New Issue Editing</span>
+ <span tal:condition="python: context.id and not context.is_edit_ok()"
+  tal:omit-tag="python:1" i18n:translate="">Issue<tal:x
+  replace="context/id" i18n:name="id" /></span>
+ <span tal:condition="python: context.id and context.is_edit_ok()"
+  tal:omit-tag="python:1" i18n:translate="">Issue<tal:x
+  replace="context/id" i18n:name="id" /> Editing</span>
+</tal:block>
+
+<td class="content" metal:fill-slot="content">
+
+<p tal:condition="python:not (context.is_view_ok()
+ or request.user.hasRole('Anonymous'))" i18n:translate="">
+ You are not allowed to view this page.</p>
+
+<p tal:condition="python:not context.is_view_ok()
+ and request.user.hasRole('Anonymous')" i18n:translate="">
+ Please login with your username and password.</p>
+
+<div tal:condition="context/is_view_ok">
+
+<form method="POST" name="itemSynopsis"
+      onSubmit="return submit_once()" enctype="multipart/form-data"
+      tal:attributes="action context/designator">
+
+<table class="form">
+<tr>
+ <th class="required" i18n:translate="">Title</th>
+ <td colspan=3 tal:content="structure python:context.title.field(size=60)">title</td>
+</tr>
+
+<tr>
+ <th class="required" i18n:translate="">Priority</th>
+ <td tal:content="structure context/priority/menu">priority</td>
+ <th i18n:translate="">Status</th>
+ <td tal:content="structure context/status/menu">status</td>
+</tr>
+
+<tr>
+ <th i18n:translate="">Superseder</th>
+ <td>
+  <span tal:replace="structure python:context.superseder.field(showid=1, size=20)" />
+  <span tal:condition="context/is_edit_ok" tal:replace="structure python:db.issue.classhelp('id,title', property='superseder')" />
+  <span tal:condition="context/superseder">
+   <br><span i18n:translate="">View:</span>
+     <a tal:repeat="sup context/superseder"
+        tal:content="python:sup['id'] + ', '*(not repeat['sup'].end)"
+        tal:attributes="href string:issue${sup/id}"></a>
+  </span>
+ </td>
+ <th i18n:translate="">Nosy List</th>
+ <td>
+  <span tal:replace="structure context/nosy/field" />
+  <span tal:condition="context/is_edit_ok" tal:replace="structure
+python:db.user.classhelp('username,realname,address', property='nosy', width='600')" /><br>
+ </td>
+</tr>
+
+<tr>
+ <th i18n:translate="">Assigned To</th>
+ <td tal:content="structure context/assignedto/menu">assignedto menu</td>
+ <th i18n:translate="">Keywords</th>
+ <td>
+  <span tal:replace="structure context/keyword/field" />
+  <span tal:condition="context/is_edit_ok" tal:replace="structure python:db.keyword.classhelp(property='keyword')" />
+ </td>
+</tr>
+
+<tr tal:condition="context/is_edit_ok">
+ <th i18n:translate="">Change Note</th>
+ <td colspan=3>
+  <textarea tal:content="request/form/@note/value | default"
+            name="@note" wrap="hard" rows="5" cols="80"></textarea>
+ </td>
+</tr>
+
+<tr tal:condition="context/is_edit_ok">
+ <th i18n:translate="">File</th>
+ <td colspan=3><input type="file" name="@file" size="40"></td>
+</tr>
+
+<tr tal:condition="context/is_edit_ok">
+ <td>
+  &nbsp;
+  <input type="hidden" name="@template" value="item">
+  <input type="hidden" name="@required" value="title,priority">
+ </td>
+ <td colspan=3>
+  <span tal:replace="structure context/submit">submit button</span>
+  <a tal:condition="context/id" tal:attributes="href context/copy_url"
+   i18n:translate="">Make a copy</a>
+ </td>
+</tr>
+
+</table>
+</form>
+
+<tal:block tal:condition="not:context/id" i18n:translate="">
+<table class="form">
+<tr>
+ <td>Note:&nbsp;</td>
+ <th class="required">highlighted</th>
+ <td>&nbsp;fields are required.</td>
+</tr>
+</table>
+</tal:block>
+
+<p tal:condition="context/id" i18n:translate="">
+ Created on <b><tal:x replace="context/creation" i18n:name="creation" /></b>
+ by <b><tal:x replace="context/creator" i18n:name="creator" /></b>,
+ last changed <b><tal:x replace="context/activity" i18n:name="activity" /></b>
+ by <b><tal:x replace="context/actor" i18n:name="actor" /></b>.
+</p>
+
+<table class="files" tal:condition="context/files">
+ <tr><th colspan="5" class="header" i18n:translate="">Files</th></tr>
+ <tr>
+  <th i18n:translate="">File name</th>
+  <th i18n:translate="">Uploaded</th>
+  <th i18n:translate="">Type</th>
+  <th i18n:translate="">Edit</th>
+  <th i18n:translate="">Remove</th>
+ </tr>
+ <tr tal:repeat="file context/files">
+  <td>
+   <a tal:attributes="href file/download_url"
+      tal:content="file/name">dld link</a>
+  </td>
+  <td>
+   <span tal:content="file/creator">creator's name</span>,
+   <span tal:content="file/creation">creation date</span>
+  </td>
+  <td tal:content="file/type" />
+  <td><a tal:condition="file/is_edit_ok"
+          tal:attributes="href string:file${file/id}">edit</a>
+  </td>
+  <td>
+   <form style="padding:0" tal:condition="context/is_edit_ok"
+         tal:attributes="action string:issue${context/id}">
+    <input type="hidden" name="@remove@files" tal:attributes="value file/id">
+    <input type="hidden" name="@action" value="edit">
+    <input type="submit" value="remove" i18n:attributes="value">
+   </form>
+  </td>
+ </tr>
+</table>
+
+<table class="messages" tal:condition="context/messages">
+ <tr><th colspan="4" class="header" i18n:translate="">Messages</th></tr>
+ <tal:block tal:repeat="msg context/messages/reverse">
+  <tr>
+   <th><a tal:attributes="href string:msg${msg/id}"
+    i18n:translate="">msg<tal:x replace="msg/id" i18n:name="id" /> (view)</a></th>
+   <th i18n:translate="">Author: <tal:x replace="msg/author"
+       i18n:name="author" /></th>
+   <th i18n:translate="">Date: <tal:x replace="msg/date"
+       i18n:name="date" /></th>
+   <th>
+    <form style="padding:0" tal:condition="context/is_edit_ok"
+          tal:attributes="action string:issue${context/id}">
+     <input type="hidden" name="@remove@messages" tal:attributes="value msg/id">
+     <input type="hidden" name="@action" value="edit">
+     <input type="submit" value="remove" i18n:attributes="value">
+    </form>
+   </th>
+  </tr>
+  <tr>