Commits

martin.v.loewis  committed 9776384 Draft

Create the Jython instance as a copy of the python-dev instance.

  • Participants

Comments (0)

Files changed (59)

+*.pyc
+*.pyo
+htmlbase.py
+*.cover

File TEMPLATE-INFO.txt

+Name: python-tracker
+Description: This is customisation of the "classic" tracker for the Python
+             Language developers.
+Intended-For: http://dev.python.org/
+

File config.ini.template

+# Roundup issue tracker configuration file
+# Autogenerated at Fri Nov 17 16:59:49 2006
+
+# 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 = xhtml
+
+# 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 = 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 = NO DEFAULT
+#web = http://psf.upfronthosting.co.za/roundup/tracker/
+web = http://localhost:9999/python-dev/
+
+# 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 = no
+
+# 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
+
+# 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 = 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
+
+# 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 = 
+
+# 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
+
+# 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 = multiple

File detectors/.cvsignore

+*.pyc
+*.pyo
+*.cover

File detectors/audit2to3.py

+import roundup
+import roundup.instance
+import sets
+
+def update2to3(db, cl, nodeid, newvalues):
+    '''Component 2to3 issues to be assigned to collinwinter unless otherwise
+       assigned.
+    '''
+    # nodeid will be None if this is a new node
+    componentIDS=None
+    if nodeid is not None:
+        componentIDS = cl.get(nodeid, 'components')
+    if newvalues.has_key('components'):
+        componentIDS = newvalues['components']
+    if componentIDS and (theComponent in componentIDS):
+        if not newvalues.has_key('assignee') or \
+               newvalues['assignee'] == Nobody:
+            newvalues['assignee'] = theMan
+
+def init(db):
+    global theMan, theComponent, Nobody
+    theMan = db.user.lookup('collinwinter')
+    Nobody = db.user.lookup('nobody')
+    theComponent = db.component.lookup('2to3 (2.x to 3.0 conversion tool)')
+
+    db.issue.audit('create', update2to3)
+    db.issue.audit('set', update2to3)
+
+if __name__ == '__main__':
+    global theMan, theComponent, Nobody
+    instanceHome='/home/roundup/trackers/tracker'
+    instance = roundup.instance.open(instanceHome)
+    db = instance.open('admin')
+    cl = db.issue
+    nodeID = '1002'
+    theMan = db.user.lookup('collinwinter')
+    Nobody = db.user.lookup('nobody')
+    theComponent = db.component.lookup('2to3 (2.x to 3.0 conversion tool)')
+    newvalues = { 'components': [theComponent] , 'assignee': Nobody}
+    update2to3(db, cl, nodeID, newvalues)
+    print Nobody, theMan, theComponent, newvalues

File detectors/changes_xml_writer.py

+#
+#  changes.xml writer detector.
+#
+# Copyright (c) 2007  Michal Kwiatkowski <constant.beta@gmail.com>
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+#   notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+#   notice, this list of conditions and the following disclaimer in the
+#   documentation and/or other materials provided with the distribution.
+#
+# * Neither the name of the author nor the names of his contributors
+#   may be used to endorse or promote products derived from this software
+#   without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+"""changes.xml writer detector -- save each database change to an XML file.
+
+Root element is called `changes` and it has at most `ChangesXml.max_items`
+children, each called a `change`. Each `change` has the following attributes:
+
+:date:  date in RFC2822 format when the change was made
+:id:    unique identifier of this change (note: not an integer)
+:type:  type of this change (see below)
+
+A structure of a `change` depends on its `type`. Currently implemented
+change types and their formats are listed below.
+
+* type = `file-added`
+
+  Describes a new file attached to an existing issue. Child elements:
+
+  :file-id:   unique integer identifier of the file
+  :file-name: name of the uploaded file
+  :file-type: MIME type of the file content
+  :file-url:  permanent URL of the file
+  :issue-id:  unique integer identifier of an issue this file is attached to
+"""
+
+import os
+import urllib
+from xml.dom import minidom
+from xml.parsers.expat import ExpatError
+from time import gmtime, strftime
+
+# Relative to tracker home directory.
+FILENAME = os.path.join('%(TEMPLATES)s', 'recent-changes.xml')
+
+
+def tracker_url(db):
+    return str(db.config.options[('tracker', 'web')])
+
+def changes_xml_path(db):
+    return os.path.join(db.config.HOME, FILENAME % db.config.options)
+
+def rfc2822_date():
+    return strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())
+
+class File(object):
+    def __init__(self, db, id, issue_id):
+        self.db = db
+        self.id = id
+        self.issue_id = issue_id
+
+        self.name = db.file.get(id, 'name')
+        self.type = db.file.get(id, 'type')
+        # Based on roundup.cgi.templating._HTMLItem.download_url().
+        self.download_url = tracker_url(self.db) +\
+            urllib.quote('%s%s/%s' % ('file', self.id, self.name))
+
+class ChangesXml(object):
+    # Maximum number of changes stored in a file.
+    max_items = 20
+
+    def __init__(self, filename):
+        self.filename = filename
+        self._read_document()
+        self.modified = False
+
+    def save(self):
+        if not self.modified:
+            return
+
+        self._trim_to_max_items()
+
+        fd = open(self.filename, 'w')
+        self.document.writexml(fd, encoding="UTF-8")
+        fd.close()
+
+    def add_file(self, file):
+        change = self._change("file%s-added-to-issue%s" % (file.id, file.issue_id),
+                              "file-added")
+
+        change.appendChild(self._element_with_text("file-id",   file.id))
+        change.appendChild(self._element_with_text("file-name", file.name))
+        change.appendChild(self._element_with_text("file-type", file.type))
+        change.appendChild(self._element_with_text("file-url",  file.download_url))
+        change.appendChild(self._element_with_text("issue-id",  file.issue_id))
+
+        self.root.appendChild(change)
+        self.modified = True
+
+    def add_files(self, files):
+        for file in files:
+            self.add_file(file)
+
+    def _change(self, id, type):
+        """Return new 'change' element of a given type.
+           <change id='id' date='now' type='type'></change>
+        """
+        change = self.document.createElement("change")
+        change.setAttribute("id",   id)
+        change.setAttribute("type", type)
+        change.setAttribute("date", rfc2822_date())
+        return change
+
+    def _element_with_text(self, name, value):
+        """Return new element with given name and text node as a value.
+           <name>value</name>
+        """
+        element = self.document.createElement(name)
+        text = self.document.createTextNode(str(value))
+        element.appendChild(text)
+        return element
+
+    def _trim_to_max_items(self):
+        """Remove changes exceeding self.max_items.
+        """
+        # Assumes that changes are stored sequentially from oldest to newest.
+        # Will do for now.
+        for change in self.root.getElementsByTagName("change")[0:-self.max_items]:
+            self.root.removeChild(change)
+
+    def _read_document(self):
+        try:
+            self.document = minidom.parse(self.filename)
+            self.root = self.document.firstChild
+        except IOError, e:
+            # File not found, create a new one then.
+            if e.errno != 2:
+                raise
+            self._create_new_document()
+        except ExpatError:
+            # File has been damaged, forget about it and create a new one.
+            self._create_new_document()
+
+    def _create_new_document(self):
+        self.document = minidom.Document()
+        self.root = self.document.createElement("changes")
+        self.document.appendChild(self.root)
+
+def get_new_files_ids(issue_now, issue_then):
+    """Return ids of files added between `now` and `then`.
+    """
+    files_now = set(issue_now['files'])
+    if issue_then:
+        files_then = set(issue_then['files'])
+    else:
+        files_then = set()
+    return map(int, files_now - files_then)
+
+def file_added_to_issue(db, cl, issue_id, olddata):
+    try:
+        changes   = ChangesXml(changes_xml_path(db))
+        issue     = db.issue.getnode(issue_id)
+        new_files = [ File(db, id, issue_id) for id in get_new_files_ids(issue, olddata) ]
+
+        changes.add_files(new_files)
+        changes.save()
+    except:
+        # We can't mess up with a database commit.
+        pass
+
+
+def init(db):
+    db.issue.react('create', file_added_to_issue)
+    db.issue.react('set',    file_added_to_issue)

File detectors/cia.py

+# Reactor for sending changes to CIA.vc
+import xmlrpclib
+import cgi
+
+server = "http://CIA.vc"
+
+parameters = {
+    'name':'Roundup Reactor for CIA',
+    'revision': "$Revision$"[11:-2],
+    'project': 'Python',
+    'branch': 'roundup',
+    'urlprefix': 'http://bugs.python.org/issue',
+}
+
+max_content = 150
+
+TEMPLATE = """
+<message>
+<generator>
+  <name>Roundup Reactor for CIA</name>
+  <version>%(revision)s</version>
+</generator>
+<source>
+  <project>%(project)s</project>
+  <module>#%(nodeid)s</module>
+  <branch>%(branch)s</branch>
+</source>
+<body>
+  <commit>
+    <author>%(author)s</author>
+    <files><file>%(file)s</file></files>
+    <log>%(log)s</log>
+    <url>%(urlprefix)s%(nodeid)s</url>
+  </commit>
+</body>
+</message>
+"""
+
+
+def sendcia(db, cl, nodeid, oldvalues):
+    messages = set(cl.get(nodeid, 'messages'))
+    if oldvalues:
+        messages -= set(oldvalues.get('messages',()))
+    if not messages:
+        return
+    messages = list(messages)
+
+    if oldvalues:
+        oldstatus = oldvalues['status']
+    else:
+        oldstatus = None
+    newstatus = db.issue.get(nodeid, 'status')
+    if oldstatus != newstatus:
+        if oldvalues:
+            status = db.status.get(newstatus, 'name')
+        else:
+            status = 'new'
+        log = '[' + status + '] '
+    else:
+        log = ''
+    for msg in messages:
+        log += db.msg.get(msg, 'content')
+    if len(log) > max_content:
+        log = log[:max_content-4] + ' ...'
+    log = log.replace('\n', ' ')
+
+    params = parameters.copy()
+    params['file'] = cgi.escape(db.issue.get(nodeid, 'title'))
+    params['nodeid'] = nodeid
+    params['author'] = db.user.get(db.getuid(), 'username')
+    params['log'] = cgi.escape(log)
+
+    payload = TEMPLATE % params
+
+    try:
+        rpc = xmlrpclib.ServerProxy(server)
+        rpc.hub.deliver(payload)
+    except:
+        # Ignore any errors in sending the CIA;
+        # if the server is down, that's just bad luck
+        # XXX might want to do some logging here
+        pass
+
+def init(db):
+    db.issue.react('create', sendcia)
+    db.issue.react('set', sendcia)

File detectors/config.ini.template

+#This configuration file controls the behavior of busybody.py and tellteam.py
+#The two definitions can be comma-delimited lists of email addresses.
+#Be sure these addresses will accept mail from the tracker's email address.
+[main]
+triage_email =  triage@example.com
+busybody_email= busybody@example.com
+
+# URI to XMLRPC server doing the actual spam check.
+spambayes_uri = http://www.webfast.com:80/sbrpc
+# These must match the {ham,spam}_cutoff setting in the SpamBayes server
+# config.
+spambayes_ham_cutoff = 0.2
+spambayes_spam_cutoff = 0.85
+
+spambayes_may_view_spam = User,Coordinator,Developer
+spambayes_may_classify = Coordinator
+spambayes_may_report_misclassified = User,Coordinator,Developer

File detectors/messagesummary.py

+#$Id: messagesummary.py,v 1.1 2003/04/17 03:26:38 richard Exp $
+
+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'], 1, 1)
+    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

File detectors/no_texthtml.py

+
+def audit_html_files(db, cl, nodeid, newvalues):
+    if newvalues.has_key('type') and newvalues['type'] == 'text/html':
+        newvalues['type'] = 'text/plain'
+    
+
+def init(db):
+    db.file.audit('set', audit_html_files)
+    db.file.audit('create', audit_html_files)

File detectors/nosyreaction.py

+import sets
+from roundup import roundupdb, hyperdb
+
+def updatenosy(db, cl, nodeid, newvalues):
+    '''Update the nosy list for changes to the assignee
+    '''
+    # 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 assignee(s) to the nosy list
+    if newvalues.has_key('assignee') and newvalues['assignee'] is not None:
+        propdef = cl.getprops()
+        if isinstance(propdef['assignee'], hyperdb.Link):
+            assignee_ids = [newvalues['assignee']]
+        elif isinstance(propdef['assignee'], hyperdb.Multilink):
+            assignee_ids = newvalues['assignee']
+        for assignee_id in assignee_ids:
+            new_nosy.add(assignee_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 addcreator(db, cl, nodeid, newvalues):
+    assert None == nodeid, "addcreator called for existing node"
+    nosy = newvalues.get('nosy', [])
+    if not db.getuid() in nosy:
+        nosy.append(db.getuid())
+        newvalues['nosy'] = nosy
+                         
+
+def init(db):
+    db.issue.audit('create', updatenosy)
+    db.issue.audit('set', updatenosy)
+
+    # Make sure creator of issue is added. Do this after 'updatenosy'. 
+    db.issue.audit('create', addcreator, priority=110)

File detectors/sendmail.py

+from roundup import roundupdb
+
+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 is_spam(db, msgid):
+    """Return true if message has a spambayes score above
+    db.config.detectors['SPAMBAYES_SPAM_CUTOFF']. Also return true if
+    msgid is None, which happens when there are no messages (i.e., a
+    property-only change)"""
+    if not msgid:
+        return False
+    cutoff_score = float(db.config.detectors['SPAMBAYES_SPAM_CUTOFF'])    
+
+    msg = db.getnode("msg", msgid)
+    if msg.has_key('spambayes_score') and \
+           msg['spambayes_score'] > cutoff_score:
+        return True
+    return False
+
+
+def sendmail(db, cl, nodeid, oldvalues):
+    """Send mail to various recipients, when changes occur:
+
+    * For all changes (property-only, or with new message), send mail
+      to all e-mail addresses defined in
+      db.config.detectors['BUSYBODY_EMAIL']
+
+    * For all changes (property-only, or with new message), send mail
+      to all members of the nosy list.
+
+    * For new issues, and only for new issue, send mail to
+      db.config.detectors['TRIAGE_EMAIL']
+
+    """
+
+    sendto = []
+
+    # The busybody addresses always get mail.
+    try:
+        sendto += db.config.detectors['BUSYBODY_EMAIL'].split(",")
+    except KeyError:
+        pass
+
+    # New submission? 
+    if None == oldvalues:
+        changenote = cl.generateCreateNote(nodeid)
+        try:
+            # Add triage addresses
+            sendto += db.config.detectors['TRIAGE_EMAIL'].split(",")
+        except KeyError:
+            pass
+        oldfiles = []
+    else:
+        changenote = cl.generateChangeNote(nodeid, oldvalues)
+        oldfiles = oldvalues.get('files', [])        
+
+    newfiles = db.issue.get(nodeid, 'files', [])
+    if oldfiles != newfiles:
+        added = [fid for fid in newfiles if fid not in oldfiles]
+        removed = [fid for fid in oldfiles if fid not in newfiles]
+        filemsg = ""
+
+        for fid in added:
+            url = db.config.TRACKER_WEB + "file%s/%s" % \
+                  (fid, db.file.get(fid, "name"))
+            changenote+="\nAdded file: %s" % url
+        for fid in removed:
+            url = db.config.TRACKER_WEB + "file%s/%s" % \
+                  (fid, db.file.get(fid, "name"))            
+            changenote+="\nRemoved file: %s" % url
+
+
+    authid = db.getuid()
+
+    new_messages = determineNewMessages(cl, nodeid, oldvalues)
+
+    # Make sure we send a nosy mail even for property-only
+    # changes. 
+    if not new_messages:
+        new_messages = [None]
+
+    for msgid in [msgid for msgid in new_messages if not is_spam(db, msgid)]:
+        try:
+            cl.send_message(nodeid, msgid, changenote, sendto,
+                            authid=authid)
+            nosymessage(db, nodeid, msgid, oldvalues, changenote)
+        except roundupdb.MessageSendError, message:
+            raise roundupdb.DetectorError, message
+
+def nosymessage(db, nodeid, msgid, oldvalues, note,
+                whichnosy='nosy',
+                from_address=None, cc=[], bcc=[]):
+    """Send a message to the members of an issue's nosy list.
+
+    The message is sent only to users on the nosy list who are not
+    already on the "recipients" list for the message.
+
+    These users are then added to the message's "recipients" list.
+
+    If 'msgid' is None, the message gets sent only to the nosy
+    list, and it's called a 'System Message'.
+
+    The "cc" argument indicates additional recipients to send the
+    message to that may not be specified in the message's recipients
+    list.
+
+    The "bcc" argument also indicates additional recipients to send the
+    message to that may not be specified in the message's recipients
+    list. These recipients will not be included in the To: or Cc:
+    address lists.
+    """
+    if msgid:
+        authid = db.msg.get(msgid, 'author')
+        recipients = db.msg.get(msgid, 'recipients', [])
+    else:
+        # "system message"
+        authid = None
+        recipients = []
+
+    sendto = []
+    bcc_sendto = []
+    seen_message = {}
+    for recipient in recipients:
+        seen_message[recipient] = 1
+
+    def add_recipient(userid, to):
+        # make sure they have an address
+        address = db.user.get(userid, 'address')
+        if address:
+            to.append(address)
+            recipients.append(userid)
+
+    def good_recipient(userid):
+        # Make sure we don't send mail to either the anonymous
+        # user or a user who has already seen the message.
+        return (userid and
+                (db.user.get(userid, 'username') != 'anonymous') and
+                not seen_message.has_key(userid))
+
+    # possibly send the message to the author, as long as they aren't
+    # anonymous
+    if (good_recipient(authid) and
+        (db.config.MESSAGES_TO_AUTHOR == 'yes' or
+         (db.config.MESSAGES_TO_AUTHOR == 'new' and not oldvalues))):
+        add_recipient(authid, sendto)
+
+    if authid:
+        seen_message[authid] = 1
+
+    # now deal with the nosy and cc people who weren't recipients.
+    for userid in cc + db.issue.get(nodeid, whichnosy):
+        if good_recipient(userid):
+            add_recipient(userid, sendto)
+
+    # now deal with bcc people.
+    for userid in bcc:
+        if good_recipient(userid):
+            add_recipient(userid, bcc_sendto)
+
+    # If we have new recipients, update the message's recipients
+    # and send the mail.
+    if sendto or bcc_sendto:
+        if msgid is not None:
+            db.msg.set(msgid, recipients=recipients)
+        db.issue.send_message(nodeid, msgid, note, sendto, from_address,
+                              bcc_sendto)
+
+
+def init(db):
+    db.issue.react('set', sendmail)
+    db.issue.react('create', sendmail)

File detectors/severityauditor.py

+
+def init_severity(db, cl, nodeid, newvalues):
+    """Make sure severity is set on new issues"""
+    if newvalues.has_key('severity') and newvalues['severity']:
+        return
+
+    normal = db.severity.lookup('normal')
+    newvalues['severity'] = normal
+
+def init(db):
+    db.issue.audit('create', init_severity)

File detectors/spambayes.py

+../../spambayes_integration/detectors/spambayes.py

File detectors/statusauditor.py

+def init_status(db, cl, nodeid, newvalues):
+    """ Make sure the status is set on new issues"""
+
+    if newvalues.has_key('status') and newvalues['status']:
+        return
+
+    new_id = db.status.lookup('open')
+    newvalues['status'] = new_id
+
+
+def block_resolution(db, cl, nodeid, newvalues):
+    """ If the issue has blockers, don't allow it to be resolved."""
+
+    if nodeid is None:
+        dependencies = []
+    else:
+        dependencies = cl.get(nodeid, 'dependencies')
+    dependencies = newvalues.get('dependencies', dependencies)
+
+    # don't do anything if there's no blockers or the status hasn't
+    # changed
+    if not dependencies or not newvalues.has_key('status'):
+        return
+
+    # format the info
+    u = db.config.TRACKER_WEB
+    s = ', '.join(['<a href="%sissue%s">%s</a>'%(u,id,id) for id in dependencies])
+    if len(dependencies) == 1:
+        s = 'issue %s is'%s
+    else:
+        s = 'issues %s are'%s
+
+    # ok, see if we're trying to resolve
+    if newvalues.get('status') and newvalues['status'] == db.status.lookup('closed'):
+        raise ValueError, "This issue can't be closed until %s closed."%s
+
+
+def resolve(db, cl, nodeid, newvalues):
+    """Make sure status, resolution, and superseder values match."""
+
+    status_change = newvalues.get('status')
+    status_close = status_change and newvalues['status'] == db.status.lookup('closed')
+
+    # Make sure resolution and superseder get only set when status->close
+    if not status_change or not status_close:
+        if newvalues.get('resolution') or newvalues.get('superseder'):
+            raise ValueError, "resolution and superseder must only be set when a issue is closed"
+
+    # Make sure resolution is set when status->close
+    if status_close:
+        if not newvalues.get('resolution'):
+            raise ValueError, "resolution must be set when a issue is closed"
+
+        # Make sure superseder is set when resolution->duplicate
+        if newvalues['resolution'] == db.resolution.lookup('duplicate'):
+            if not newvalues.get('superseder'):
+                raise ValueError, "please provide a superseder when closing a issue as 'duplicate'"
+
+
+
+def resolve_dependencies(db, cl, nodeid, oldvalues):
+    """ When we resolve an issue that's a blocker, remove it from the
+    blockers list of the issue(s) it blocks."""
+
+    newstatus = cl.get(nodeid,'status')
+
+    # no change?
+    if oldvalues.get('status', None) == newstatus:
+        return
+
+    closed_id = db.status.lookup('closed')
+
+    # interesting?
+    if newstatus != closed_id:
+        return
+
+    # yes - find all the dependend issues, if any, and remove me from
+    # their dependency list
+    issues = cl.find(dependencies=nodeid)
+    for issueid in issues:
+        dependencies = cl.get(issueid, 'dependencies')
+        if nodeid in dependencies:
+            dependencies.remove(nodeid)
+            cl.set(issueid, dependencies=dependencies)
+
+
+def init(db):
+    # fire before changes are made
+    db.issue.audit('create', init_status)
+#    db.issue.audit('create', block_resolution)
+#    db.issue.audit('set', block_resolution)
+#    db.issue.audit('set', resolve)
+
+    # adjust after changes are committed
+#    db.issue.react('set', resolve_dependencies)

File 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: userauditor.py,v 1.3 2006/09/18 03:24:38 tobias-herp Exp $
+
+def audit_user_fields(db, cl, nodeid, newvalues):
+    ''' Make sure user properties are valid.
+
+        - email address has no spaces in it
+        - roles specified exist
+    '''
+    if newvalues.has_key('address') and ' ' in newvalues['address']:
+        raise ValueError, 'Email address must not contain spaces'
+
+    if newvalues.has_key('roles') and newvalues['roles']:
+        roles = [x.lower().strip() for x in newvalues['roles'].split(',')]
+        for rolename in roles:
+            if not db.security.role.has_key(rolename):
+                raise ValueError, 'Role "%s" does not exist'%rolename
+
+        if None != nodeid and "admin" in roles:
+            if not "admin" in [x.lower().strip() for x in cl.get(nodeid, 'roles').split(",")]:
+                raise ValueError, "Only Admins may assign the Admin role!"
+
+
+def init(db):
+    # fire before changes are made
+    db.user.audit('set', audit_user_fields)
+    db.user.audit('create', audit_user_fields)
+
+# vim: set filetype=python ts=4 sw=4 et si

File extensions/README.txt

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

File extensions/local_replace.py

+import re
+
+substitutions = [ (re.compile('\#(?P<ws>\s*)(?P<id>\d+)'),
+                   "<a href='issue\g<id>'>#\g<ws>\g<id></a>" ),
+                  (re.compile('(?P<prews>\s+)revision(?P<ws>\s*)(?P<revision>\d+)'),
+                   "\g<prews><a href='http://svn.python.org/view?rev=\g<revision>&view=rev'>revision\g<ws>\g<revision></a>"),
+                  (re.compile('(?P<prews>\s+)rev(?P<ws>\s*)(?P<revision>\d+)'),
+                   "\g<prews><a href='http://svn.python.org/view?rev=\g<revision>&view=rev'>rev\g<ws>\g<revision></a>"),
+                  (re.compile('(?P<prews>\s+)(?P<revstr>r|r\s+)(?P<revision>\d+)'),
+                   "\g<prews><a href='http://svn.python.org/view?rev=\g<revision>&view=rev'>\g<revstr>\g<revision></a>"),
+                  (re.compile('(?P<prews>\s+)(?P<path>(Demo|Doc|Grammar|Include|Lib|Mac|Misc|Modules|Parser|PC|PCbuild|Python|RISCOS|Tools)/[-.a-zA-Z0-9]+[a-zA-Z0-9])'),
+                   "\g<prews><a href='http://svn.python.org/view/python/trunk/\g<path>'>\g<path></a>"),
+                   ]
+
+def localReplace(message):
+
+    for cre, replacement in substitutions:
+        message = cre.sub(replacement, message)
+
+    return message
+        
+    
+    
+def init(instance):
+    instance.registerUtil('localReplace', localReplace)
+    
+
+if "__main__" == __name__:
+    print " revision 222", localReplace(" revision 222")
+    print " wordthatendswithr 222", localReplace(" wordthatendswithr 222")
+    print " r222", localReplace(" r222")
+    print " r 222", localReplace(" r 222")
+    print " #555", localReplace(" #555")
+    

File extensions/spambayes.py

+../../spambayes_integration/extensions/spambayes.py

File extensions/timestamp.py

+import time, struct, base64
+from roundup.cgi.actions import RegisterAction
+from roundup.cgi.exceptions import *
+
+def timestamp():
+    return base64.encodestring(struct.pack("i", time.time())).strip()
+
+def unpack_timestamp(s):
+    return struct.unpack("i",base64.decodestring(s))[0]
+
+class Timestamped:
+    def check(self):
+        try:
+            created = unpack_timestamp(self.form['opaque'].value)
+        except KeyError:
+            raise FormError, "somebody tampered with the form"
+        if time.time() - created < 4:
+            raise FormError, "responding to the form too quickly"
+        return True
+
+class TimestampedRegister(Timestamped, RegisterAction):
+    def permission(self):
+        self.check()
+        RegisterAction.permission(self)
+
+def init(instance):
+    instance.registerUtil('timestamp', timestamp)
+    instance.registerAction('register', TimestampedRegister)

File extensions/timezone.py

+# Utility for replacing the simple input field for the timezone with
+# a select-field that lists the available values.
+
+import cgi
+
+try:
+    import pytz
+except ImportError:
+    pytz = None
+
+
+def tzfield(prop, name, default):
+    if pytz:
+        value = prop.plain()        
+        if '' == value:
+            value = default
+        else:
+            try:
+                value = "Etc/GMT%+d" % int(value)
+            except ValueError:
+                pass
+
+        l = ['<select name="%s"' % name]
+        for zone in pytz.all_timezones:
+            s = ' '
+            if zone == value:
+                s = 'selected=selected '
+            z = cgi.escape(zone)
+            l.append('<option %svalue="%s">%s</option>' % (s, z, z))
+        l.append('</select>')
+        return '\n'.join(l)
+        
+    else:
+        return prop.field()
+
+def init(instance):
+    instance.registerUtil('tzfield', tzfield)

File 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>

File 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>

File 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>

File 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>

File 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>
+

File 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>

File 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>
+  <head>
+      <link media="screen" href="http://python.org/styles/screen-switcher-default.css" type="text/css" id="screen-switcher-stylesheet" rel="stylesheet" />
+      <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')">
+      <tal:block tal:define="property request/form/property/value">
+      <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>
+      </tal:block>
+  </head>
+ <body class="body" onload="resetList();">
+ <h1 id="logoheader">
+   <img src="http://python.org/images/python-logo.gif" alt="logo" border="0" />
+ </h1>
+<div id="content-body">
+<div id="content">
+<div id="breadcrumb">Python Issue Tracker: Help - <tal:block tal:content="context/classname">item</tal:block></div>
+<tal:block tal:condition="python:request.form.has_key('property')">
+ <form name="frm_help" action="#"
+       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>
+ </tal:block>
+ <tal:block tal:condition="python:not request.form.has_key('property')">
+ <tal:block tal:define="batch request/batch;
+                        props python:request.form['properties'].value.split(',')">
+     <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 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 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 tal:repeat="prop props" tal:content="prop" i18n:translate=""></th>
+       </tr>
+     </table>
+ </tal:block>
+ </tal:block>
+</div> <!-- content-body -->
+</div> <!-- content -->
+ </body>
+</html>

File html/_generic.index.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">
+
+<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>

File 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>

File html/file.index.html

+<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="">Descriptoin</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/description">description</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>

File 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="">Description</th>
+  <td tal:content="structure context/description/field"></td>
+ </tr>
+ <tr>
+  <th i18n:translate="">Content Type</th>
+  <td tal:content="structure context/type/field"/>
+  <td style="border: none" tal:condition="python: context.is_edit_ok()">Please note that
+  for security reasons, it's not permitted to set content type to <i>text/html</i>.</td>
+ </tr>
+ <tr>
+  <th i18n:translate="">SpamBayes Score</th>
+  <td tal:content="structure context/spambayes_score/plain"></td>
+ </tr>
+
+ <tr>
+  <th i18n:translate="">Marked as misclassified</th>
+  <td tal:content="structure context/spambayes_misclassified/plain"></td>
+ </tr>
+
+ <tr tal:condition="python:context.is_edit_ok()">
+  <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>
+
+<p tal:condition="python:utils.sb_is_spam(context)" class="error-message">
+   File has been classified as spam.</p>
+
+<a tal:condition="python:context.id and context.content.is_view_ok()"
+ tal:attributes="href string:file${context/id}/${context/name}"
+ i18n:translate="">download</a>
+
+<p tal:condition="python:context.id and not context.content.is_view_ok()">
+   Files classified as spam are not available for download by
+   unathorized users. If you think the file has been misclassified,
+   please login and click on the button for reclassification.
+</p>
+
+
+     <form method="POST" onSubmit="return submit_once()"
+       enctype="multipart/form-data"
+       tal:attributes="action context/designator"
+       tal:condition="python:request.user.hasPermission('SB: May Classify')">
+ 
+      <input type="hidden" name="@action" value="spambayes_classify">
+      <input type="submit" name="trainspam" value="Mark as SPAM" i18n:attributes="value">
+      <input type="submit" name="trainham" value="Mark as HAM (not SPAM)" i18n:attributes="value">
+     </form>
+
+<tal:block tal:condition="context/id" tal:replace="structure context/history" />