Mercurial > p > roundup > code
changeset 602:c242455d9b46 config-0-4-0-branch
Brought the config branch up to date with HEAD
line wrap: on
line diff
--- a/BUILD.txt Wed Feb 06 03:47:17 2002 +0000 +++ b/BUILD.txt Wed Feb 06 04:05:55 2002 +0000 @@ -10,28 +10,46 @@ This means that we only need to ever build source releases. This is done by running: - 0. python setup.py clean --all - 1. Edit setup.py to ensure that all information therein (version, contact + 0. Edit setup.py and doc/announcement.txt to reflect the new version and + appropriate announcments. + 1. python setup.py clean --all + 2. Edit setup.py to ensure that all information therein (version, contact information etc) is correct. - 2. python setup.py sdist --manifest-only - 3. Check the MANIFEST to make sure that any new files are included. If + 3. python setup.py sdist --manifest-only + 4. Check the MANIFEST to make sure that any new files are included. If they are not, edit MANIFEST.in to include them. "Documentation" for MANIFEST.in may be found in disutils.filelist._parse_template_line. - 4. python setup.py sdist + 5. python setup.py sdist (if you find sdist a little verbose, add "--quiet" to the end of the command) - 5. FTP the tar.gz from the dist directory to to the "incoming" directory on - "upload.sourceforge.net". - 6. Make a quick release at: - http://sourceforge.net/project/admin/qrs.php?package_id=&group_id=31577 So, those commands in a nice, cut'n'pasteable form :) python setup.py clean --all python setup.py sdist --manifest-only python setup.py sdist --quiet +or, for the sad RedHat users: +python2 setup.py clean --all +python2 setup.py sdist --manifest-only +python2 setup.py sdist --quiet -2. Author + +2. Distributing Releases +======================== + +Once a release is built, follow these steps: + 1. FTP the tar.gz from the dist directory to to the "incoming" directory on + "upload.sourceforge.net". + 2. Make a quick release at: + http://sourceforge.net/project/admin/qrs.php?package_id=&group_id=31577 + 3. Add a news item at: + https://sourceforge.net/news/submit.php?group_id=31577 + using the top of doc/announcement.txt + 4. Send doc/announcement.txt to python-announce@python.org + 5. Notify any other news services as appropriate... + + +3. Author ========= richard@users.sourceforge.net
--- a/CHANGES.txt Wed Feb 06 03:47:17 2002 +0000 +++ b/CHANGES.txt Wed Feb 06 04:05:55 2002 +0000 @@ -1,7 +1,75 @@ This file contains the changes to the Roundup system over time. The entries are given with the most recent entry first. -2001-12-?? - 0.4.0b1 +2002-02-?? - ????? +Fixed: + . respect encodings in non multipart messages. + . makeHtmlBase: re.sub under python 2.2 did not replace '.', string.replace + does it. + . preamble in tepmlateBuilder mentioned htmldata + . mailgw checks encoding on first part too. + . #511586 ] unittest FAIL: testReldate_date + + +2002-01-24 - 0.4.0 +Feature: + . much nicer history display (actualy real handling of property types etc) + . journal entries for link and mutlilink properties can be switched on or + off + . properties in change note are now sorted + . you can now use the roundup-admin tool pack the database + +Fixed: + . the mail gateway now responds with an error message when invalid values + for arguments are specified for link or mutlilink properties + . modified unit test to check nosy and assignedto when specified as arguments + . handle attachments with no name (eg tnef) + . fixed setting nosy as argument in subject line + . fixed back_bsddb so it passed the journal tests + . fixed status changes in mail gateway (eg. unread -> chatting) + . we'll actually distribute the frontends directory now, as advertised... + . handle stripping of "AW:" from subject line + . htmltemplate list() wasn't sorting... + . unit tests for html templating (and re-enabled the listbox field for + multilinks) + . allow abbreviation of "help" in admin tool too. + . run_tests testReldate_date failed if LANG is 'german' + . mailgw failures (unexpected ones) are forwarded to the roundup admin + + +2002-01-16 - 0.4.0b2 +Fixed: + . #495392 ] empty nosy -patch + . #500574 ] messageid must have format <part1@part2> + . fixed some problems with web editing and change detection + . mail splitting wasn't detecting responses in the same "section" as quoted + text + . missed a "from i18n import _" in date.py + . #501690 ] MIGRATION.txt incomplete + . #502342 ] pipe interface + . #502437 ] rogue reactor and unittest + . re-enabled dumbdbm when using python >2.1.1 (ie 2.1.2, 2.2) + . changed all config accesses so they access either the instance or the + config attriubute on the db. This means that all config is obtained from + instance_config instead of the mish-mash of classes. This will make + switching to a ConfigParser setup easier too, I hope. + . #502951 ] adding new properties to old database + . #502953 ] nosy-like treatment of other multilinks + . #503164 ] create and passwords + . plain rendering of links in the htmltemplate now generate a hyperlink to + the linked node's page. + . #503330 ] ANONYMOUS_REGISTER now applies to mail + . #503353 ] setting properties in initial email + . #502956 ] filtering by multilink not supported + . #503340 ] creating issue with [asignedto=p.ohly] + . #502949 ] index view for non-issues and redisplay + . #503793 ] changing assignedto resets nosy list + . lots of date/interval related changes: + - more relaxed date format for input + - handle None for date/interval properties + + +2002-01-08 - 0.4.0b1 Feature: . Added INSTANCE_NAME to configuration - used in web and email to identify the instance.
--- a/I18N_PROGRESS.txt Wed Feb 06 03:47:17 2002 +0000 +++ b/I18N_PROGRESS.txt Wed Feb 06 04:05:55 2002 +0000 @@ -17,14 +17,6 @@ THESE FILES DO NOT USE _() ========================== -roundup-admin -roundup-mailgw -roundup-server -cgi-bin/roundup.cgi -roundup/__init__.py -roundup/cgitb.py -roundup/date.py -roundup/htmltemplate.py roundup/hyperdb.py roundup/i18n.py roundup/init.py @@ -57,7 +49,16 @@ THESE FILES DO USE _() ====================== +roundup-admin +roundup-mailgw +roundup-server +cgi-bin/roundup.cgi +roundup/__init__.py roundup/cgi_client.py +roundup/admin.py +roundup/cgitb.py +roundup/date.py +roundup/htmltemplate.py WE DON'T CARE ABOUT THESE FILES
--- a/INSTALL.txt Wed Feb 06 03:47:17 2002 +0000 +++ b/INSTALL.txt Wed Feb 06 04:05:55 2002 +0000 @@ -4,16 +4,15 @@ Installation =============== These instructions work on redhat 6.2 and mandrake 8.0 - with the caveat -that these systems don't come with python 2.0 or newer installed, so you'll +that these systems don't come with python 2.1.1 or newer installed, so you'll have to upgrade python before this stuff will work. Prerequisites ------------- -Either: - . Python 2.0 with pydoc installed. See http://www.lfw.org/ for pydoc. -or - . Python 2.1 +Python 2.1.1 or newer. + +Note: Python 2.1.1 shipped with SuSE7.3 might miss module _weakref. You will need either the anydbm or bsddb module. @@ -21,9 +20,8 @@ Testing the Software -------------------- -Run "python -c 'import test;test.go()'" and make sure there's no errors. -If there are errors, please let us know! - +Either run "run_tests" or "python -c 'import test;test.go()'" and make sure +there's no errors. If there are errors, please let us know! Installing the Software -----------------------
--- a/MANIFEST.in Wed Feb 06 03:47:17 2002 +0000 +++ b/MANIFEST.in Wed Feb 06 04:05:55 2002 +0000 @@ -1,4 +1,5 @@ recursive-include roundup *.py *.txt *.item *.index *.css *.filter *.newitem +recursive-include frontends *.py *.txt *.dtml *.gif *.css *.html recursive-include cgi-bin *.cgi recursive-include test *.py *.txt recursive-include doc *.html *.png *.txt
--- a/MIGRATION.txt Wed Feb 06 03:47:17 2002 +0000 +++ b/MIGRATION.txt Wed Feb 06 04:05:55 2002 +0000 @@ -25,12 +25,11 @@ If you used the extended schema, the file is in: - <roundup source>/roundup/templates/extended/dbinit.pybinit.py needs updating from the original. + <roundup source>/roundup/templates/extended/dbinit.py -If you have modified your dbinit.py file, you may use encoded passwords: - - 1. Edit the dbinit.py file in your instance home directory. Find the lines - which define the msg class: +If you have modified your dbinit.py file, you need to edit the dbinit.py +file in your instance home directory. Find the lines which define the msg +class: msg = FileClass(db, "msg", author=Link("user"), recipients=Multilink("user"), @@ -45,6 +44,50 @@ files=Multilink("file"), messageid=String(), inreplyto=String()) +Also, configuration is being cleaned up. This means that your dbinit.py will +also need to be changed in the open function. If you haven't changed your +dbinit.py, the above copy will be enough. If you have, you'll need to change +the line (round line 50): + + db = Database(instance_config.DATABASE, name) + +to: + + db = Database(instance_config, name) + + +Configuration +------------- +INSTANCE_NAME and EMAIL_SIGNATURE_POSITION have been added to the +instance_config.py. The simplest solution is to copy the default values +from template in the core source. + +The mail gateway now checks ANONYMOUS_REGISTER to see if unknown users are to +be automatically registered with the tracker. If it is set to "deny" then +unknown users will not have access. If it is set to "allow" they will be +automatically registered with the tracker. + + +CGI script roundup.cgi +---------------------- +The CGI script has been updated with some features and a bugfix, so you should +copy it from the roundup cgi-bin source directory again. Make sure you update +the ROUNDUP_INSTANCE_HOMES after the copy. + + +Nosy reactor +------------ +The nosy reactor has also changed - copy the nosyreactor.py file from the core +source roundup/templates/[schema]/detectors/nosyreactor.py to your instance +home "detectors" directory. + + +HTML templating +--------------- +The field() function was incorrectly implemented - links and multilinks now +display as text fields when rendered using field(). To display a menu (drop- +down or select box) you need to use the menu() function. + Migrating from 0.2.x to 0.3.x
--- a/cgi-bin/roundup.cgi Wed Feb 06 03:47:17 2002 +0000 +++ b/cgi-bin/roundup.cgi Wed Feb 06 04:05:55 2002 +0000 @@ -16,11 +16,13 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: roundup.cgi,v 1.22.2.2 2002-01-03 08:28:15 titus Exp $ +# $Id: roundup.cgi,v 1.22.2.3 2002-02-06 04:05:53 richard Exp $ # python version check import sys from roundup import version_check +from roundup.i18n import _ +import sys # ## Configuration @@ -72,7 +74,7 @@ from roundup import cgitb except: print "Content-Type: text/html\n" - print "Failed to import cgitb.<pre>" + print _("Failed to import cgitb.<pre>") s = StringIO.StringIO() traceback.print_exc(None, s) print cgi.escape(s.getvalue()), "</pre>" @@ -168,15 +170,15 @@ request.send_header('Content-Type', 'text/html') request.end_headers() w = request.write - w('<html><head><title>Roundup instances index</title></head>\n') - w('<body><h1>Roundup instances index</h1><ol>\n') + w(_('<html><head><title>Roundup instances index</title></head>\n')) + w(_('<body><h1>Roundup instances index</h1><ol>\n')) homes = ROUNDUP_INSTANCE_HOMES.keys() homes.sort() for instance in homes: - w('<li><a href="%s/%s/index">%s</a>\n'%( - os.environ['SCRIPT_NAME'], urllib.quote(instance), - cgi.escape(instance))) - w('</ol></body></html>') + w(_('<li><a href="%(instance_url)s/index">%(instance_name)s</a>\n')%{ + 'instance_url': os.environ['SCRIPT_NAME']+'/'+urllib.quote(instance), + 'instance_name': cgi.escape(instance)}) + w(_('</ol></body></html>')) # # Now do the actual CGI handling @@ -203,9 +205,27 @@ # # $Log: not supported by cvs2svn $ +# Revision 1.24 2002/01/05 02:21:22 richard +# fixes +# +# Revision 1.23 2002/01/05 02:19:03 richard +# i18n'ification +# +# Revision 1.22.2.2 2002/01/03 08:28:15 titus +# * Altered function names in roundup.config to bE NeAtEr rather_than_ugly; +# +# * Created some extra roundup.config exceptions to let people know what's +# going on; +# +# * Modified instance_config.py to use those exceptions, so that syntax errors +# or other exceptions did *not* trigger the same behavior as import +# while in the templates/ directory. +# +# * Modified roundup.cgi to use the same neater function names, plus did some +# minor cleanup. +# # Revision 1.22.2.1 2002/01/03 02:12:05 titus # -# # Initial ConfigParser implementation. # # Revision 1.22 2001/12/13 00:20:01 richard
--- a/doc/announcement.txt Wed Feb 06 03:47:17 2002 +0000 +++ b/doc/announcement.txt Wed Feb 06 04:05:55 2002 +0000 @@ -1,15 +1,15 @@ - Roundup 0.3.1b1 - an issue tracking system + Roundup 0.4.0 - an issue tracking system -If you are upgrading from pre-0.3.0, please read MIGRATION.txt. +If you are upgrading please read MIGRATION.txt. -Roundup requires python 2.1.1 for correct operation. Support for dumbdbm has -been disabled until python 2.1.2 and 2.2 are released. +Roundup requires python 2.1.1 for correct operation. Support for dumbdbm +requires python 2.1.2 or 2.2. Big stuff in this release: - Use of transactions to prevent partial data commits - Zope Product front-end - Nicer, more consistent change message generation - - Several bug fixes + - Several bug fixes and more unit tests - Much, much more: see the CHANGES file for details. Source and documentation is available at the website:
--- a/doc/index.html Wed Feb 06 03:47:17 2002 +0000 +++ b/doc/index.html Wed Feb 06 04:05:55 2002 +0000 @@ -751,6 +751,10 @@ </pre> <h2><a name="custinst">Instance Schema</a></h2> +<b>Note:</b> if you modify the schema, you'll most likely need to +<a href="#schemarepurcussions"> +web interface to reflect your changes</a>. +<p> An instance schema defines what data is stored in the instance's database. The two schemas shipped with Roundup turn it into a typical software bug tracker (the extended schema allowing for support issues as well as bugs). @@ -918,6 +922,20 @@ newitem views. The newitem view is optional - the item view will be used if the newitem view doesn't exist. +<h3><a name="schemarepurcussions">Repurcussions of changing the instance schema</a></h3> + +If you choose to <a href="custinst">change the instance schema</a> you will need to +ensure the web interface knows about it: +<ol> +<li>Index, item and filter pages for the relevant classes may need to have properties +added or removed, +<li>The default page header relies on the existence of, and some values of the +priority, status, assignedto and activity classes. If you change any of these (specifically +if you remove any of the classes or their default values) you will need to implement your +own pagehead() method in your instance's interfaces.py module. +</ol> + + <h3>Displaying Properties</h3> <p> @@ -991,11 +1009,7 @@ For other properties, link to this node with the property as the text. <p> <em>Options:</em><br> -property (property name) - the property to use in the second case.<br> -is_download (boolean) - to be used for links te <em>file</em> class nodes - -this indicates that the URL should have the specified property (usually -<em>name</em>) appended so that the download has the correct name (instead -of the node designator.) +property (property name) - the property to use in the second case. </td></tr> <tr><td valign="top"><strong>count</strong></td> @@ -1019,8 +1033,13 @@ </td></tr> <tr><td valign="top"><strong>download</strong></td> -<td>Show a Link("file") or Multilink("file") -property using links that allow you to download files. +<td>For a Link or Multilink property, display the names of the linked +nodes, hyperlinked to the item views on those nodes. +<p> +For other properties, link to this node with the property as the text. +<p> +In all cases, append the name (key property) of the item to the path so it +is the name of the file being downloaded. <p> <em>Arguments:</em><br> property (property name) - the property to use. @@ -1125,10 +1144,12 @@ tags are included or omitted depending on whether the view specifier requests a filter for a particular property. +<p>A property must appear in the filter template for it to be available +as a filter. + <p>Here's a simple example of a filter template. -<blockquote><pre><small -><property name=status> +<blockquote><pre><small><property name=status> <display call="checklist('status')"> </property> <br> @@ -1157,8 +1178,7 @@ <p>Here's a simple example of an index template. -<blockquote><pre><small -><tr> +<blockquote><pre><small><tr> <property name=title> <td><display call="plain('title', max=50)"></td> </property> @@ -1195,8 +1215,7 @@ <p>Here's an example of a basic editor template. -<blockquote><pre><small -><table> +<blockquote><pre><small><table> <tr> <td colspan=2> <display call="field('title', size=60)"> @@ -1285,8 +1304,7 @@ <p> </p> <hr> -$Id: index.html,v 1.23 2001-12-31 03:07:04 richard Exp $ +$Id: index.html,v 1.23.2.1 2002-02-06 04:05:53 richard Exp $ <p> </p> </body></html> -
--- a/frontends/ZRoundup/ZRoundup.py Wed Feb 06 03:47:17 2002 +0000 +++ b/frontends/ZRoundup/ZRoundup.py Wed Feb 06 04:05:55 2002 +0000 @@ -14,19 +14,23 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: ZRoundup.py,v 1.3 2001-12-12 23:55:00 richard Exp $ +# $Id: ZRoundup.py,v 1.3.2.1 2002-02-06 04:05:53 richard Exp $ # ''' ZRoundup module - exposes the roundup web interface to Zope -This frontend works by providing a thin layer that sits between Zope and the regular CGI -interface of roundup, providing the web frontend with the minimum of effort. +This frontend works by providing a thin layer that sits between Zope and the +regular CGI interface of roundup, providing the web frontend with the minimum +of effort. -This means that the regular CGI interface does all authentication quite independently of -Zope. +This means that the regular CGI interface does all authentication quite +independently of Zope. The roundup code is kept in memory though, and it +runs in the same server as all your other Zope stuff, so it does have _some_ +advantages over regular CGI :) -It also means that any requests which specify :filter, :columns or :sort _must_ be done -using a GET, so that this interface can re-parse the QUERY_STRING. Zope interprets the -':' as a special character, and the special args are lost to it. +It also means that any requests which specify :filter, :columns or :sort +_must_ be done using a GET, so that this interface can re-parse the +QUERY_STRING. Zope interprets the ':' as a special character, and the special +args are lost to it. ''' from Globals import InitializeClass, HTMLFile from OFS.SimpleItem import Item @@ -40,7 +44,8 @@ import roundup.instance from roundup import cgi_client -modulesecurity.declareProtected('View management screens', 'manage_addZRoundupForm') +modulesecurity.declareProtected('View management screens', + 'manage_addZRoundupForm') manage_addZRoundupForm = HTMLFile('dtml/manage_addZRoundupForm', globals()) modulesecurity.declareProtected('Add Z Roundups', 'manage_addZRoundup') @@ -87,8 +92,8 @@ return self.form.keys() class ZRoundup(Item, PropertyManager, Implicit, Persistent): - '''An instance of this class provides an interface between Zope and roundup for one - roundup instance + '''An instance of this class provides an interface between Zope and + roundup for one roundup instance ''' meta_type = 'Z Roundup' security = ClassSecurityInfo() @@ -163,6 +168,12 @@ # # $Log: not supported by cvs2svn $ +# Revision 1.4 2002/01/10 03:38:16 richard +# reformatting for 80 cols +# +# Revision 1.3 2001/12/12 23:55:00 richard +# Fixed some problems with user editing +# # Revision 1.2 2001/12/12 23:33:58 richard # added some implementation notes #
--- a/roundup-admin Wed Feb 06 03:47:17 2002 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1292 +0,0 @@ -#! /usr/bin/env python -# -# 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: roundup-admin,v 1.59 2001-12-31 05:20:34 richard Exp $ - -# python version check -from roundup import version_check - -import sys, os, getpass, getopt, re, UserDict, shlex -try: - import csv -except ImportError: - csv = None -from roundup import date, hyperdb, roundupdb, init, password, token -import roundup.instance - -class CommandDict(UserDict.UserDict): - '''Simple dictionary that lets us do lookups using partial keys. - - Original code submitted by Engelbert Gruber. - ''' - _marker = [] - def get(self, key, default=_marker): - if self.data.has_key(key): - return [(key, self.data[key])] - keylist = self.data.keys() - keylist.sort() - l = [] - for ki in keylist: - if ki.startswith(key): - l.append((ki, self.data[ki])) - if not l and default is self._marker: - raise KeyError, key - return l - -class UsageError(ValueError): - pass - -class AdminTool: - - def __init__(self): - self.commands = CommandDict() - for k in AdminTool.__dict__.keys(): - if k[:3] == 'do_': - self.commands[k[3:]] = getattr(self, k) - self.help = {} - for k in AdminTool.__dict__.keys(): - if k[:5] == 'help_': - self.help[k[5:]] = getattr(self, k) - self.instance_home = '' - self.db = None - - def usage(self, message=''): - if message: message = 'Problem: '+message+'\n\n' - print '''%sUsage: roundup-admin [-i instance home] [-u login] [-c] <command> <arguments> - -Help: - roundup-admin -h - roundup-admin help -- this help - roundup-admin help <command> -- command-specific help - roundup-admin help all -- all available help -Options: - -i instance home -- specify the issue tracker "home directory" to administer - -u -- the user[:password] to use for commands - -c -- when outputting lists of data, just comma-separate them'''%message - self.help_commands() - - def help_commands(self): - print 'Commands:', - commands = [''] - for command in self.commands.values(): - h = command.__doc__.split('\n')[0] - commands.append(' '+h[7:]) - commands.sort() - commands.append( -'Commands may be abbreviated as long as the abbreviation matches only one') - commands.append('command, e.g. l == li == lis == list.') - print '\n'.join(commands) - print - - def help_commands_html(self, indent_re=re.compile(r'^(\s+)\S+')): - commands = self.commands.values() - def sortfun(a, b): - return cmp(a.__name__, b.__name__) - commands.sort(sortfun) - for command in commands: - h = command.__doc__.split('\n') - name = command.__name__[3:] - usage = h[0] - print ''' -<tr><td valign=top><strong>%(name)s</strong></td> - <td><tt>%(usage)s</tt><p> -<pre>'''%locals() - indent = indent_re.match(h[3]) - if indent: indent = len(indent.group(1)) - for line in h[3:]: - if indent: - print line[indent:] - else: - print line - print '</pre></td></tr>\n' - - def help_all(self): - print ''' -All commands (except help) require an instance specifier. This is just the path -to the roundup instance you're working with. A roundup instance is where -roundup keeps the database and configuration file that defines an issue -tracker. It may be thought of as the issue tracker's "home directory". It may -be specified in the environment variable ROUNDUP_INSTANCE or on the command -line as "-i instance". - -A designator is a classname and a nodeid concatenated, eg. bug1, user10, ... - -Property values are represented as strings in command arguments and in the -printed results: - . Strings are, well, strings. - . Date values are printed in the full date format in the local time zone, and - accepted in the full format or any of the partial formats explained below. - . Link values are printed as node designators. When given as an argument, - node designators and key strings are both accepted. - . Multilink values are printed as lists of node designators joined by commas. - When given as an argument, node designators and key strings are both - accepted; an empty string, a single node, or a list of nodes joined by - commas is accepted. - -When property values must contain spaces, just surround the value with -quotes, either ' or ". A single space may also be backslash-quoted. If a -valuu must contain a quote character, it must be backslash-quoted or inside -quotes. Examples: - hello world (2 tokens: hello, world) - "hello world" (1 token: hello world) - "Roch'e" Compaan (2 tokens: Roch'e Compaan) - Roch\'e Compaan (2 tokens: Roch'e Compaan) - address="1 2 3" (1 token: address=1 2 3) - \\ (1 token: \) - \n\r\t (1 token: a newline, carriage-return and tab) - -When multiple nodes are specified to the roundup get or roundup set -commands, the specified properties are retrieved or set on all the listed -nodes. - -When multiple results are returned by the roundup get or roundup find -commands, they are printed one per line (default) or joined by commas (with -the -c) option. - -Where the command changes data, a login name/password is required. The -login may be specified as either "name" or "name:password". - . ROUNDUP_LOGIN environment variable - . the -u command-line option -If either the name or password is not supplied, they are obtained from the -command-line. - -Date format examples: - "2000-04-17.03:45" means <Date 2000-04-17.08:45:00> - "2000-04-17" means <Date 2000-04-17.00:00:00> - "01-25" means <Date yyyy-01-25.00:00:00> - "08-13.22:13" means <Date yyyy-08-14.03:13:00> - "11-07.09:32:43" means <Date yyyy-11-07.14:32:43> - "14:25" means <Date yyyy-mm-dd.19:25:00> - "8:47:11" means <Date yyyy-mm-dd.13:47:11> - "." means "right now" - -Command help: -''' - for name, command in self.commands.items(): - print '%s:'%name - print ' ',command.__doc__ - - def do_help(self, args, nl_re=re.compile('[\r\n]'), - indent_re=re.compile(r'^(\s+)\S+')): - '''Usage: help topic - Give help about topic. - - commands -- list commands - <command> -- help specific to a command - initopts -- init command options - all -- all available help - ''' - topic = args[0] - - # try help_ methods - if self.help.has_key(topic): - self.help[topic]() - return 0 - - # try command docstrings - try: - l = self.commands.get(topic) - except KeyError: - print 'Sorry, no help for "%s"'%topic - return 1 - - # display the help for each match, removing the docsring indent - for name, help in l: - lines = nl_re.split(help.__doc__) - print lines[0] - indent = indent_re.match(lines[1]) - if indent: indent = len(indent.group(1)) - for line in lines[1:]: - if indent: - print line[indent:] - else: - print line - return 0 - - def help_initopts(self): - import roundup.templates - templates = roundup.templates.listTemplates() - print 'Templates:', ', '.join(templates) - import roundup.backends - backends = roundup.backends.__all__ - print 'Back ends:', ', '.join(backends) - - - def do_initialise(self, instance_home, args): - '''Usage: initialise [template [backend [admin password]]] - Initialise a new Roundup instance. - - The command will prompt for the instance home directory (if not supplied - through INSTANCE_HOME or the -i option. The template, backend and admin - password may be specified on the command-line as arguments, in that - order. - - See also initopts help. - ''' - if len(args) < 1: - raise UsageError, 'Not enough arguments supplied' - # select template - import roundup.templates - templates = roundup.templates.listTemplates() - template = len(args) > 1 and args[1] or '' - if template not in templates: - print 'Templates:', ', '.join(templates) - while template not in templates: - template = raw_input('Select template [classic]: ').strip() - if not template: - template = 'classic' - - import roundup.backends - backends = roundup.backends.__all__ - backend = len(args) > 2 and args[2] or '' - if backend not in backends: - print 'Back ends:', ', '.join(backends) - while backend not in backends: - backend = raw_input('Select backend [anydbm]: ').strip() - if not backend: - backend = 'anydbm' - if len(args) > 3: - adminpw = confirm = args[3] - else: - adminpw = '' - confirm = 'x' - while adminpw != confirm: - adminpw = getpass.getpass('Admin Password: ') - confirm = getpass.getpass(' Confirm: ') - init.init(instance_home, template, backend, adminpw) - return 0 - - - def do_get(self, args): - '''Usage: get property designator[,designator]* - Get the given property of one or more designator(s). - - Retrieves the property value of the nodes specified by the designators. - ''' - if len(args) < 2: - raise UsageError, 'Not enough arguments supplied' - propname = args[0] - designators = args[1].split(',') - l = [] - for designator in designators: - # decode the node designator - try: - classname, nodeid = roundupdb.splitDesignator(designator) - except roundupdb.DesignatorError, message: - raise UsageError, message - - # get the class - try: - cl = self.db.getclass(classname) - except KeyError: - raise UsageError, 'invalid class "%s"'%classname - try: - if self.comma_sep: - l.append(cl.get(nodeid, propname)) - else: - print cl.get(nodeid, propname) - except IndexError: - raise UsageError, 'no such %s node "%s"'%(classname, nodeid) - except KeyError: - raise UsageError, 'no such %s property "%s"'%(classname, - propname) - if self.comma_sep: - print ','.join(l) - return 0 - - - def do_set(self, args): - '''Usage: set designator[,designator]* propname=value ... - Set the given property of one or more designator(s). - - Sets the property to the value for all designators given. - ''' - if len(args) < 2: - raise UsageError, 'Not enough arguments supplied' - from roundup import hyperdb - - designators = args[0].split(',') - props = {} - for prop in args[1:]: - if prop.find('=') == -1: - raise UsageError, 'argument "%s" not propname=value'%prop - try: - key, value = prop.split('=') - except ValueError: - raise UsageError, 'argument "%s" not propname=value'%prop - props[key] = value - for designator in designators: - # decode the node designator - try: - classname, nodeid = roundupdb.splitDesignator(designator) - except roundupdb.DesignatorError, message: - raise UsageError, message - - # get the class - try: - cl = self.db.getclass(classname) - except KeyError: - raise UsageError, 'invalid class "%s"'%classname - - properties = cl.getprops() - for key, value in props.items(): - proptype = properties[key] - if isinstance(proptype, hyperdb.String): - continue - elif isinstance(proptype, hyperdb.Password): - props[key] = password.Password(value) - elif isinstance(proptype, hyperdb.Date): - try: - props[key] = date.Date(value) - except ValueError, message: - raise UsageError, '"%s": %s'%(value, message) - elif isinstance(proptype, hyperdb.Interval): - try: - props[key] = date.Interval(value) - except ValueError, message: - raise UsageError, '"%s": %s'%(value, message) - elif isinstance(proptype, hyperdb.Link): - props[key] = value - elif isinstance(proptype, hyperdb.Multilink): - props[key] = value.split(',') - - # try the set - try: - apply(cl.set, (nodeid, ), props) - except (TypeError, IndexError, ValueError), message: - raise UsageError, message - return 0 - - def do_find(self, args): - '''Usage: find classname propname=value ... - Find the nodes of the given class with a given link property value. - - Find the nodes of the given class with a given link property value. The - value may be either the nodeid of the linked node, or its key value. - ''' - if len(args) < 1: - raise UsageError, 'Not enough arguments supplied' - classname = args[0] - # get the class - try: - cl = self.db.getclass(classname) - except KeyError: - raise UsageError, 'invalid class "%s"'%classname - - # TODO: handle > 1 argument - # handle the propname=value argument - if args[1].find('=') == -1: - raise UsageError, 'argument "%s" not propname=value'%prop - try: - propname, value = args[1].split('=') - except ValueError: - raise UsageError, 'argument "%s" not propname=value'%prop - - # if the value isn't a number, look up the linked class to get the - # number - num_re = re.compile('^\d+$') - if not num_re.match(value): - # get the property - try: - property = cl.properties[propname] - except KeyError: - raise UsageError, '%s has no property "%s"'%(classname, - propname) - - # make sure it's a link - if (not isinstance(property, hyperdb.Link) and not - isinstance(property, hyperdb.Multilink)): - raise UsageError, 'You may only "find" link properties' - - # get the linked-to class and look up the key property - link_class = self.db.getclass(property.classname) - try: - value = link_class.lookup(value) - except TypeError: - raise UsageError, '%s has no key property"'%link_class.classname - except KeyError: - raise UsageError, '%s has no entry "%s"'%(link_class.classname, - propname) - - # now do the find - try: - if self.comma_sep: - print ','.join(apply(cl.find, (), {propname: value})) - else: - print apply(cl.find, (), {propname: value}) - except KeyError: - raise UsageError, '%s has no property "%s"'%(classname, - propname) - except (ValueError, TypeError), message: - raise UsageError, message - return 0 - - def do_specification(self, args): - '''Usage: specification classname - Show the properties for a classname. - - This lists the properties for a given class. - ''' - if len(args) < 1: - raise UsageError, 'Not enough arguments supplied' - classname = args[0] - # get the class - try: - cl = self.db.getclass(classname) - except KeyError: - raise UsageError, 'invalid class "%s"'%classname - - # get the key property - keyprop = cl.getkey() - for key, value in cl.properties.items(): - if keyprop == key: - print '%s: %s (key property)'%(key, value) - else: - print '%s: %s'%(key, value) - - def do_display(self, args): - '''Usage: display designator - Show the property values for the given node. - - This lists the properties and their associated values for the given - node. - ''' - if len(args) < 1: - raise UsageError, 'Not enough arguments supplied' - - # decode the node designator - try: - classname, nodeid = roundupdb.splitDesignator(args[0]) - except roundupdb.DesignatorError, message: - raise UsageError, message - - # get the class - try: - cl = self.db.getclass(classname) - except KeyError: - raise UsageError, 'invalid class "%s"'%classname - - # display the values - for key in cl.properties.keys(): - value = cl.get(nodeid, key) - print '%s: %s'%(key, value) - - def do_create(self, args): - '''Usage: create classname property=value ... - Create a new entry of a given class. - - This creates a new entry of the given class using the property - name=value arguments provided on the command line after the "create" - command. - ''' - if len(args) < 1: - raise UsageError, 'Not enough arguments supplied' - from roundup import hyperdb - - classname = args[0] - - # get the class - try: - cl = self.db.getclass(classname) - except KeyError: - raise UsageError, 'invalid class "%s"'%classname - - # now do a create - props = {} - properties = cl.getprops(protected = 0) - if len(args) == 1: - # ask for the properties - for key, value in properties.items(): - if key == 'id': continue - name = value.__class__.__name__ - if isinstance(value , hyperdb.Password): - again = None - while value != again: - value = getpass.getpass('%s (Password): '%key.capitalize()) - again = getpass.getpass(' %s (Again): '%key.capitalize()) - if value != again: print 'Sorry, try again...' - if value: - props[key] = value - else: - value = raw_input('%s (%s): '%(key.capitalize(), name)) - if value: - props[key] = value - else: - # use the args - for prop in args[1:]: - if prop.find('=') == -1: - raise UsageError, 'argument "%s" not propname=value'%prop - try: - key, value = prop.split('=') - except ValueError: - raise UsageError, 'argument "%s" not propname=value'%prop - props[key] = value - - # convert types - for key in props.keys(): - # get the property - try: - proptype = properties[key] - except KeyError: - raise UsageError, '%s has no property "%s"'%(classname, key) - - if isinstance(proptype, hyperdb.Date): - try: - props[key] = date.Date(value) - except ValueError, message: - raise UsageError, '"%s": %s'%(value, message) - elif isinstance(proptype, hyperdb.Interval): - try: - props[key] = date.Interval(value) - except ValueError, message: - raise UsageError, '"%s": %s'%(value, message) - elif isinstance(proptype, hyperdb.Password): - props[key] = password.Password(value) - elif isinstance(proptype, hyperdb.Multilink): - props[key] = value.split(',') - - # check for the key property - if cl.getkey() and not props.has_key(cl.getkey()): - raise UsageError, "you must provide the '%s' property."%cl.getkey() - - # do the actual create - try: - print apply(cl.create, (), props) - except (TypeError, IndexError, ValueError), message: - raise UsageError, message - return 0 - - def do_list(self, args): - '''Usage: list classname [property] - List the instances of a class. - - Lists all instances of the given class. If the property is not - specified, the "label" property is used. The label property is tried - in order: the key, "name", "title" and then the first property, - alphabetically. - ''' - if len(args) < 1: - raise UsageError, 'Not enough arguments supplied' - classname = args[0] - - # get the class - try: - cl = self.db.getclass(classname) - except KeyError: - raise UsageError, 'invalid class "%s"'%classname - - # figure the property - if len(args) > 1: - key = args[1] - else: - key = cl.labelprop() - - if self.comma_sep: - print ','.join(cl.list()) - else: - for nodeid in cl.list(): - try: - value = cl.get(nodeid, key) - except KeyError: - raise UsageError, '%s has no property "%s"'%(classname, key) - print "%4s: %s"%(nodeid, value) - return 0 - - def do_table(self, args): - '''Usage: table classname [property[,property]*] - List the instances of a class in tabular form. - - Lists all instances of the given class. If the properties are not - specified, all properties are displayed. By default, the column widths - are the width of the property names. The width may be explicitly defined - by defining the property as "name:width". For example:: - roundup> table priority id,name:10 - Id Name - 1 fatal-bug - 2 bug - 3 usability - 4 feature - ''' - if len(args) < 1: - raise UsageError, 'Not enough arguments supplied' - classname = args[0] - - # get the class - try: - cl = self.db.getclass(classname) - except KeyError: - raise UsageError, 'invalid class "%s"'%classname - - # figure the property names to display - if len(args) > 1: - prop_names = args[1].split(',') - all_props = cl.getprops() - for spec in prop_names: - if ':' in spec: - try: - name, width = spec.split(':') - except (ValueError, TypeError): - raise UsageError, '"%s" not name:width'%spec - else: - name = spec - if not all_props.has_key(name): - raise UsageError, '%s has no property "%s"'%(classname, - name) - else: - prop_names = cl.getprops().keys() - - # now figure column widths - props = [] - for spec in prop_names: - if ':' in spec: - try: - name, width = spec.split(':') - except (ValueError, TypeError): - raise UsageError, '"%s" not name:width'%spec - props.append((name, int(width))) - else: - props.append((spec, len(spec))) - - # now display the heading - print ' '.join([name.capitalize().ljust(width) for name,width in props]) - - # and the table data - for nodeid in cl.list(): - l = [] - for name, width in props: - if name != 'id': - try: - value = str(cl.get(nodeid, name)) - except KeyError: - # we already checked if the property is valid - a - # KeyError here means the node just doesn't have a - # value for it - value = '' - else: - value = str(nodeid) - f = '%%-%ds'%width - l.append(f%value[:width]) - print ' '.join(l) - return 0 - - def do_history(self, args): - '''Usage: history designator - Show the history entries of a designator. - - Lists the journal entries for the node identified by the designator. - ''' - if len(args) < 1: - raise UsageError, 'Not enough arguments supplied' - try: - classname, nodeid = roundupdb.splitDesignator(args[0]) - except roundupdb.DesignatorError, message: - raise UsageError, message - - # TODO: handle the -c option? - try: - print self.db.getclass(classname).history(nodeid) - except KeyError: - raise UsageError, 'no such class "%s"'%classname - except IndexError: - raise UsageError, 'no such %s node "%s"'%(classname, nodeid) - return 0 - - def do_commit(self, args): - '''Usage: commit - Commit all changes made to the database. - - The changes made during an interactive session are not - automatically written to the database - they must be committed - using this command. - - One-off commands on the command-line are automatically committed if - they are successful. - ''' - self.db.commit() - return 0 - - def do_rollback(self, args): - '''Usage: rollback - Undo all changes that are pending commit to the database. - - The changes made during an interactive session are not - automatically written to the database - they must be committed - manually. This command undoes all those changes, so a commit - immediately after would make no changes to the database. - ''' - self.db.rollback() - return 0 - - def do_retire(self, args): - '''Usage: retire designator[,designator]* - Retire the node specified by designator. - - This action indicates that a particular node is not to be retrieved by - the list or find commands, and its key value may be re-used. - ''' - if len(args) < 1: - raise UsageError, 'Not enough arguments supplied' - designators = args[0].split(',') - for designator in designators: - try: - classname, nodeid = roundupdb.splitDesignator(designator) - except roundupdb.DesignatorError, message: - raise UsageError, message - try: - self.db.getclass(classname).retire(nodeid) - except KeyError: - raise UsageError, 'no such class "%s"'%classname - except IndexError: - raise UsageError, 'no such %s node "%s"'%(classname, nodeid) - return 0 - - def do_export(self, args): - '''Usage: export class[,class] destination_dir - Export the database to tab-separated-value files. - - This action exports the current data from the database into - tab-separated-value files that are placed in the nominated destination - directory. The journals are not exported. - ''' - if len(args) < 2: - raise UsageError, 'Not enough arguments supplied' - classes = args[0].split(',') - dir = args[1] - - # use the csv parser if we can - it's faster - if csv is not None: - p = csv.parser(field_sep=':') - - # do all the classes specified - for classname in classes: - try: - cl = self.db.getclass(classname) - except KeyError: - raise UsageError, 'no such class "%s"'%classname - f = open(os.path.join(dir, classname+'.csv'), 'w') - f.write(':'.join(cl.properties.keys()) + '\n') - - # all nodes for this class - properties = cl.properties.items() - for nodeid in cl.list(): - l = [] - for prop, proptype in properties: - value = cl.get(nodeid, prop) - # convert data where needed - if isinstance(proptype, hyperdb.Date): - value = value.get_tuple() - elif isinstance(proptype, hyperdb.Interval): - value = value.get_tuple() - elif isinstance(proptype, hyperdb.Password): - value = str(value) - l.append(repr(value)) - - # now write - if csv is not None: - f.write(p.join(l) + '\n') - else: - # escape the individual entries to they're valid CSV - m = [] - for entry in l: - if '"' in entry: - entry = '""'.join(entry.split('"')) - if ':' in entry: - entry = '"%s"'%entry - m.append(entry) - f.write(':'.join(m) + '\n') - return 0 - - def do_import(self, args): - '''Usage: import class file - Import the contents of the tab-separated-value file. - - The file must define the same properties as the class (including having - a "header" line with those property names.) The new nodes are added to - the existing database - if you want to create a new database using the - imported data, then create a new database (or, tediously, retire all - the old data.) - ''' - if len(args) < 2: - raise UsageError, 'Not enough arguments supplied' - if csv is None: - raise UsageError, \ - 'Sorry, you need the csv module to use this function.\n'\ - 'Get it from: http://www.object-craft.com.au/projects/csv/' - - from roundup import hyperdb - - # ensure that the properties and the CSV file headings match - classname = args[0] - try: - cl = self.db.getclass(classname) - except KeyError: - raise UsageError, 'no such class "%s"'%classname - f = open(args[1]) - p = csv.parser(field_sep=':') - file_props = p.parse(f.readline()) - props = cl.properties.keys() - m = file_props[:] - m.sort() - props.sort() - if m != props: - raise UsageError, 'Import file doesn\'t define the same '\ - 'properties as "%s".'%args[0] - - # loop through the file and create a node for each entry - n = range(len(props)) - while 1: - line = f.readline() - if not line: break - - # parse lines until we get a complete entry - while 1: - l = p.parse(line) - if l: break - - # make the new node's property map - d = {} - for i in n: - # Use eval to reverse the repr() used to output the CSV - value = eval(l[i]) - # Figure the property for this column - key = file_props[i] - proptype = cl.properties[key] - # Convert for property type - if isinstance(proptype, hyperdb.Date): - value = date.Date(value) - elif isinstance(proptype, hyperdb.Interval): - value = date.Interval(value) - elif isinstance(proptype, hyperdb.Password): - pwd = password.Password() - pwd.unpack(value) - value = pwd - if value is not None: - d[key] = value - - # and create the new node - apply(cl.create, (), d) - return 0 - - def run_command(self, args): - '''Run a single command - ''' - command = args[0] - - # handle help now - if command == 'help': - if len(args)>1: - self.do_help(args[1:]) - return 0 - self.do_help(['help']) - return 0 - if command == 'morehelp': - self.do_help(['help']) - self.help_commands() - self.help_all() - return 0 - - # figure what the command is - try: - functions = self.commands.get(command) - except KeyError: - # not a valid command - print 'Unknown command "%s" ("help commands" for a list)'%command - return 1 - - # check for multiple matches - if len(functions) > 1: - print 'Multiple commands match "%s": %s'%(command, - ', '.join([i[0] for i in functions])) - return 1 - command, function = functions[0] - - # make sure we have an instance_home - while not self.instance_home: - self.instance_home = raw_input('Enter instance home: ').strip() - - # before we open the db, we may be doing an init - if command == 'initialise': - return self.do_initialise(self.instance_home, args) - - # get the instance - try: - instance = roundup.instance.open(self.instance_home) - except ValueError, message: - self.instance_home = '' - print "Couldn't open instance: %s"%message - return 1 - - # only open the database once! - if not self.db: - self.db = instance.open('admin') - - # do the command - ret = 0 - try: - ret = function(args[1:]) - except UsageError, message: - print 'Error: %s'%message - print function.__doc__ - ret = 1 - except: - import traceback - traceback.print_exc() - ret = 1 - return ret - - def interactive(self): - '''Run in an interactive mode - ''' - print 'Roundup {version} ready for input.' - print 'Type "help" for help.' - try: - import readline - except ImportError: - print "Note: command history and editing not available" - - while 1: - try: - command = raw_input('roundup> ') - except EOFError: - print 'exit...' - break - if not command: continue - args = token.token_split(command) - if not args: continue - if args[0] in ('quit', 'exit'): break - self.run_command(args) - - # exit.. check for transactions - if self.db and self.db.transactions: - commit = raw_input("There are unsaved changes. Commit them (y/N)? ") - if commit and commit[0].lower() == 'y': - self.db.commit() - return 0 - - def main(self): - try: - opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc') - except getopt.GetoptError, e: - self.usage(str(e)) - return 1 - - # handle command-line args - self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '') - name = password = '' - if os.environ.has_key('ROUNDUP_LOGIN'): - l = os.environ['ROUNDUP_LOGIN'].split(':') - name = l[0] - if len(l) > 1: - password = l[1] - self.comma_sep = 0 - for opt, arg in opts: - if opt == '-h': - self.usage() - return 0 - if opt == '-i': - self.instance_home = arg - if opt == '-c': - self.comma_sep = 1 - - # if no command - go interactive - ret = 0 - if not args: - self.interactive() - else: - ret = self.run_command(args) - if self.db: self.db.commit() - return ret - - -if __name__ == '__main__': - tool = AdminTool() - sys.exit(tool.main()) - -# -# $Log: not supported by cvs2svn $ -# Revision 1.58 2001/12/31 05:12:52 richard -# actually handle the advertised <cr> response to "commit y/N?" -# -# Revision 1.57 2001/12/31 05:12:01 richard -# added some quoting instructions to roundup-admin -# -# Revision 1.56 2001/12/31 05:09:20 richard -# Added better tokenising to roundup-admin - handles spaces and stuff. Can -# use quoting or backslashes. See the roundup.token pydoc. -# -# Revision 1.55 2001/12/17 03:52:47 richard -# Implemented file store rollback. As a bonus, the hyperdb is now capable of -# storing more than one file per node - if a property name is supplied, -# the file is called designator.property. -# I decided not to migrate the existing files stored over to the new naming -# scheme - the FileClass just doesn't specify the property name. -# -# Revision 1.54 2001/12/15 23:09:23 richard -# Some cleanups in roundup-admin, also made it work again... -# -# Revision 1.53 2001/12/13 00:20:00 richard -# . Centralised the python version check code, bumped version to 2.1.1 (really -# needs to be 2.1.2, but that isn't released yet :) -# -# Revision 1.52 2001/12/12 21:47:45 richard -# . Message author's name appears in From: instead of roundup instance name -# (which still appears in the Reply-To:) -# . envelope-from is now set to the roundup-admin and not roundup itself so -# delivery reports aren't sent to roundup (thanks Patrick Ohly) -# -# Revision 1.51 2001/12/10 00:57:38 richard -# From CHANGES: -# . Added the "display" command to the admin tool - displays a node's values -# . #489760 ] [issue] only subject -# . fixed the doc/index.html to include the quoting in the mail alias. -# -# Also: -# . fixed roundup-admin so it works with transactions -# . disabled the back_anydbm module if anydbm tries to use dumbdbm -# -# Revision 1.50 2001/12/02 05:06:16 richard -# . We now use weakrefs in the Classes to keep the database reference, so -# the close() method on the database is no longer needed. -# I bumped the minimum python requirement up to 2.1 accordingly. -# . #487480 ] roundup-server -# . #487476 ] INSTALL.txt -# -# I also cleaned up the change message / post-edit stuff in the cgi client. -# There's now a clearly marked "TODO: append the change note" where I believe -# the change note should be added there. The "changes" list will obviously -# have to be modified to be a dict of the changes, or somesuch. -# -# More testing needed. -# -# Revision 1.49 2001/12/01 07:17:50 richard -# . We now have basic transaction support! Information is only written to -# the database when the commit() method is called. Only the anydbm -# backend is modified in this way - neither of the bsddb backends have been. -# The mail, admin and cgi interfaces all use commit (except the admin tool -# doesn't have a commit command, so interactive users can't commit...) -# . Fixed login/registration forwarding the user to the right page (or not, -# on a failure) -# -# Revision 1.48 2001/11/27 22:32:03 richard -# typo -# -# Revision 1.47 2001/11/26 22:55:56 richard -# Feature: -# . Added INSTANCE_NAME to configuration - used in web and email to identify -# the instance. -# . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup -# signature info in e-mails. -# . Some more flexibility in the mail gateway and more error handling. -# . Login now takes you to the page you back to the were denied access to. -# -# Fixed: -# . Lots of bugs, thanks Roché and others on the devel mailing list! -# -# Revision 1.46 2001/11/21 03:40:54 richard -# more new property handling -# -# Revision 1.45 2001/11/12 22:51:59 jhermann -# Fixed option & associated error handling -# -# Revision 1.44 2001/11/12 22:01:06 richard -# Fixed issues with nosy reaction and author copies. -# -# Revision 1.43 2001/11/09 22:33:28 richard -# More error handling fixes. -# -# Revision 1.42 2001/11/09 10:11:08 richard -# . roundup-admin now handles all hyperdb exceptions -# -# Revision 1.41 2001/11/09 01:25:40 richard -# Should parse with python 1.5.2 now. -# -# Revision 1.40 2001/11/08 04:42:00 richard -# Expanded the already-abbreviated "initialise" and "specification" commands, -# and added a comment to the command help about the abbreviation. -# -# Revision 1.39 2001/11/08 04:29:59 richard -# roundup-admin now accepts abbreviated commands (eg. l = li = lis = list) -# [thanks Engelbert Gruber for the inspiration] -# -# Revision 1.38 2001/11/05 23:45:40 richard -# Fixed newuser_action so it sets the cookie with the unencrypted password. -# Also made it present nicer error messages (not tracebacks). -# -# Revision 1.37 2001/10/23 01:00:18 richard -# Re-enabled login and registration access after lopping them off via -# disabling access for anonymous users. -# Major re-org of the htmltemplate code, cleaning it up significantly. Fixed -# a couple of bugs while I was there. Probably introduced a couple, but -# things seem to work OK at the moment. -# -# Revision 1.36 2001/10/21 00:45:15 richard -# Added author identification to e-mail messages from roundup. -# -# Revision 1.35 2001/10/20 11:58:48 richard -# Catch errors in login - no username or password supplied. -# Fixed editing of password (Password property type) thanks Roch'e Compaan. -# -# Revision 1.34 2001/10/18 02:16:42 richard -# Oops, committed the admin script with the wierd #! line. -# Also, made the thing into a class to reduce parameter passing. -# Nuked the leading whitespace from the help __doc__ displays too. -# -# Revision 1.33 2001/10/17 23:13:19 richard -# Did a fair bit of work on the admin tool. Now has an extra command "table" -# which displays node information in a tabular format. Also fixed import and -# export so they work. Removed freshen. -# Fixed quopri usage in mailgw from bug reports. -# -# Revision 1.32 2001/10/17 06:57:29 richard -# Interactive startup blurb - need to figure how to get the version in there. -# -# Revision 1.31 2001/10/17 06:17:26 richard -# Now with readline support :) -# -# Revision 1.30 2001/10/17 06:04:00 richard -# Beginnings of an interactive mode for roundup-admin -# -# Revision 1.29 2001/10/16 03:48:01 richard -# admin tool now complains if a "find" is attempted with a non-link property. -# -# Revision 1.28 2001/10/13 00:07:39 richard -# More help in admin tool. -# -# Revision 1.27 2001/10/11 23:43:04 richard -# Implemented the comma-separated printing option in the admin tool. -# Fixed a typo (more of a vim-o actually :) in mailgw. -# -# Revision 1.26 2001/10/11 05:03:51 richard -# Marked the roundup-admin import/export as experimental since they're not fully -# operational. -# -# Revision 1.25 2001/10/10 04:12:32 richard -# The setup.cfg file is just causing pain. Away it goes. -# -# Revision 1.24 2001/10/10 03:54:57 richard -# Added database importing and exporting through CSV files. -# Uses the csv module from object-craft for exporting if it's available. -# Requires the csv module for importing. -# -# Revision 1.23 2001/10/09 23:36:25 richard -# Spit out command help if roundup-admin command doesn't get an argument. -# -# Revision 1.22 2001/10/09 07:25:59 richard -# Added the Password property type. See "pydoc roundup.password" for -# implementation details. Have updated some of the documentation too. -# -# Revision 1.21 2001/10/05 02:23:24 richard -# . roundup-admin create now prompts for property info if none is supplied -# on the command-line. -# . hyperdb Class getprops() method may now return only the mutable -# properties. -# . Login now uses cookies, which makes it a whole lot more flexible. We can -# now support anonymous user access (read-only, unless there's an -# "anonymous" user, in which case write access is permitted). Login -# handling has been moved into cgi_client.Client.main() -# . The "extended" schema is now the default in roundup init. -# . The schemas have had their page headings modified to cope with the new -# login handling. Existing installations should copy the interfaces.py -# file from the roundup lib directory to their instance home. -# . Incorrectly had a Bizar Software copyright on the cgitb.py module from -# Ping - has been removed. -# . Fixed a whole bunch of places in the CGI interface where we should have -# been returning Not Found instead of throwing an exception. -# . Fixed a deviation from the spec: trying to modify the 'id' property of -# an item now throws an exception. -# -# Revision 1.20 2001/10/04 02:12:42 richard -# Added nicer command-line item adding: passing no arguments will enter an -# interactive more which asks for each property in turn. While I was at it, I -# fixed an implementation problem WRT the spec - I wasn't raising a -# ValueError if the key property was missing from a create(). Also added a -# protected=boolean argument to getprops() so we can list only the mutable -# properties (defaults to yes, which lists the immutables). -# -# Revision 1.19 2001/10/01 06:40:43 richard -# made do_get have the args in the correct order -# -# Revision 1.18 2001/09/18 22:58:37 richard -# -# Added some more help to roundu-admin -# -# Revision 1.17 2001/08/28 05:58:33 anthonybaxter -# added missing 'import' statements. -# -# Revision 1.16 2001/08/12 06:32:36 richard -# using isinstance(blah, Foo) now instead of isFooType -# -# Revision 1.15 2001/08/07 00:24:42 richard -# stupid typo -# -# Revision 1.14 2001/08/07 00:15:51 richard -# Added the copyright/license notice to (nearly) all files at request of -# Bizar Software. -# -# Revision 1.13 2001/08/05 07:44:13 richard -# Instances are now opened by a special function that generates a unique -# module name for the instances on import time. -# -# Revision 1.12 2001/08/03 01:28:33 richard -# Used the much nicer load_package, pointed out by Steve Majewski. -# -# Revision 1.11 2001/08/03 00:59:34 richard -# Instance import now imports the instance using imp.load_module so that -# we can have instance homes of "roundup" or other existing python package -# names. -# -# Revision 1.10 2001/07/30 08:12:17 richard -# Added time logging and file uploading to the templates. -# -# Revision 1.9 2001/07/30 03:52:55 richard -# init help now lists templates and backends -# -# Revision 1.8 2001/07/30 02:37:07 richard -# Freshen is really broken. Commented out. -# -# Revision 1.7 2001/07/30 01:28:46 richard -# Bugfixes -# -# Revision 1.6 2001/07/30 00:57:51 richard -# Now uses getopt, much improved command-line parsing. Much fuller help. Much -# better internal structure. It's just BETTER. :) -# -# Revision 1.5 2001/07/30 00:04:48 richard -# Made the "init" prompting more friendly. -# -# Revision 1.4 2001/07/29 07:01:39 richard -# Added vim command to all source so that we don't get no steenkin' tabs :) -# -# Revision 1.3 2001/07/23 08:45:28 richard -# ok, so now "./roundup-admin init" will ask questions in an attempt to get a -# workable instance_home set up :) -# _and_ anydbm has had its first test :) -# -# Revision 1.2 2001/07/23 08:20:44 richard -# Moved over to using marshal in the bsddb and anydbm backends. -# roundup-admin now has a "freshen" command that'll load/save all nodes (not -# retired - mod hyperdb.Class.list() so it lists retired nodes) -# -# Revision 1.1 2001/07/23 03:46:48 richard -# moving the bin files to facilitate out-of-the-boxness -# -# Revision 1.1 2001/07/22 11:15:45 richard -# More Grande Splite stuff -# -# -# vim: set filetype=python ts=4 sw=4 et si
--- a/roundup-mailgw Wed Feb 06 03:47:17 2002 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,251 +0,0 @@ -#! /usr/bin/python -# -# 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: roundup-mailgw,v 1.18 2001-12-13 00:20:01 richard Exp $ - -# python version check -from roundup import version_check - -import sys, os, re, cStringIO - -from roundup.mailgw import Message - -def do_pipe(handler): - '''Read a message from standard input and pass it to the mail handler. - ''' - handler.main(sys.stdin) - return 0 - -def do_mailbox(handler, filename): - '''Read a series of messages from the specified unix mailbox file and - pass each to the mail handler. - ''' - # open the spool file and lock it - import fcntl, FCNTL - f = open(filename, 'r+') - fcntl.flock(f.fileno(), FCNTL.LOCK_EX) - - # handle and clear the mailbox - try: - from mailbox import UnixMailbox - mailbox = UnixMailbox(f, factory=Message) - # grab one message - message = mailbox.next() - while message: - # call the instance mail handler - handler.handle_Message(message) - message = mailbox.next() - # nuke the file contents - os.ftruncate(f.fileno(), 0) - except: - import traceback - traceback.print_exc() - return 1 - fcntl.flock(f.fileno(), FCNTL.LOCK_UN) - return 0 - -def do_pop(handler, server, user='', password=''): - '''Read a series of messages from the specified POP server. - ''' - import getpass, poplib - if not user: - user = raw_input('User: ') - if not password: - password = getpass.getpass() - - # open a connection to the server and retrieve all messages - server = poplib.POP3(server) - server.user(user) - server.pass_(password) - numMessages = len(server.list()[1]) - for i in range(1, numMessages+1): - # retr: returns - # [ pop response e.g. '+OK 459 octets', - # [ array of message lines ], - # number of octets ] - lines = server.retr(i)[1] - s = cStringIO.StringIO('\n'.join(lines)) - s.seek(0) - handler.handle_Message(Message(s)) - # delete the message - server.dele(i) - - # quit the server to commit changes. - server.quit() - return 0 - -def usage(args, message=None): - if message is not None: - print message - print 'Usage: %s <instance home> [source spec]'%args[0] - print ''' -The roundup mail gateway may be called in one of two ways: - . with an instance home as the only argument, - . with both an instance home and a mail spool file, or - . with both an instance home and a pop server account. - -PIPE: - In the first case, the mail gateway reads a single message from the - standard input and submits the message to the roundup.mailgw module. - -UNIX mailbox: - In the second case, the gateway reads all messages from the mail spool - file and submits each in turn to the roundup.mailgw module. The file is - emptied once all messages have been successfully handled. The file is - specified as: - mailbox /path/to/mailbox - -POP: - In the third case, the gateway reads all messages from the POP server - specified and submits each in turn to the roundup.mailgw module. The - server is specified as: - pop username:password@server - The username and password may be omitted: - pop username@server - pop server - are both valid. The username and/or password will be prompted for if - not supplied on the command-line. -''' - return 1 - -def main(args): - '''Handle the arguments to the program and initialise environment. - ''' - # figure the instance home - if len(args) > 1: - instance_home = args[1] - else: - instance_home = os.environ.get('ROUNDUP_INSTANCE', '') - if not instance_home: - return usage(args) - - # get the instance - import roundup.instance - instance = roundup.instance.open(instance_home) - - # get a mail handler - db = instance.open('admin') - handler = instance.MailGW(instance, db) - - # if there's no more arguments, read a single message from stdin - if len(args) == 2: - return do_pipe(handler) - - # otherwise, figure what sort of mail source to handle - if len(args) < 4: - return usage(args, 'Error: not enough source specification information') - source, specification = args[2:] - if source == 'mailbox': - return do_mailbox(handler, specification) - elif source == 'pop': - m = re.match(r'((?P<user>[^:]+)(:(?P<pass>.+))?@)?(?P<server>.+)', - specification) - if m: - return do_pop(handler, m.group('server'), m.group('user'), - m.group('pass')) - return usage(args, 'Error: pop specification not valid') - - return usage(args, 'Error: The source must be either "mailbox" or "pop"') - -# call main -if __name__ == '__main__': - sys.exit(main(sys.argv)) - -# -# $Log: not supported by cvs2svn $ -# Revision 1.17 2001/12/02 05:06:16 richard -# . We now use weakrefs in the Classes to keep the database reference, so -# the close() method on the database is no longer needed. -# I bumped the minimum python requirement up to 2.1 accordingly. -# . #487480 ] roundup-server -# . #487476 ] INSTALL.txt -# -# I also cleaned up the change message / post-edit stuff in the cgi client. -# There's now a clearly marked "TODO: append the change note" where I believe -# the change note should be added there. The "changes" list will obviously -# have to be modified to be a dict of the changes, or somesuch. -# -# More testing needed. -# -# Revision 1.16 2001/11/30 18:23:55 jhermann -# Cleaned up strange import (less pollution, too) -# -# Revision 1.15 2001/11/30 13:16:37 rochecompaan -# Fixed bug. Mail gateway was not using the extended Message class -# resulting in failed submissions when mails were processed from a Unix -# mailbox -# -# Revision 1.14 2001/11/13 21:44:44 richard -# . re-open the database as the author in mail handling -# -# Revision 1.13 2001/11/09 01:05:55 richard -# Fixed bug #479511 ] mailgw to pop once engelbert gruber tested the POP -# gateway. -# -# Revision 1.12 2001/11/08 05:16:55 richard -# Rolled roundup-popgw into roundup-mailgw. Cleaned mailgw up significantly, -# tested unix mailbox some more. POP still untested. -# -# Revision 1.11 2001/11/07 05:32:58 richard -# More roundup-mailgw usage help. -# -# Revision 1.10 2001/11/07 05:30:11 richard -# Nicer usage message. -# -# Revision 1.9 2001/11/07 05:29:26 richard -# Modified roundup-mailgw so it can read e-mails from a local mail spool -# file. Truncates the spool file after parsing. -# Fixed a couple of small bugs introduced in roundup.mailgw when I started -# the popgw. -# -# Revision 1.8 2001/11/01 22:04:37 richard -# Started work on supporting a pop3-fetching server -# Fixed bugs: -# . bug #477104 ] HTML tag error in roundup-server -# . bug #477107 ] HTTP header problem -# -# Revision 1.7 2001/08/07 00:24:42 richard -# stupid typo -# -# Revision 1.6 2001/08/07 00:15:51 richard -# Added the copyright/license notice to (nearly) all files at request of -# Bizar Software. -# -# Revision 1.5 2001/08/05 07:44:25 richard -# Instances are now opened by a special function that generates a unique -# module name for the instances on import time. -# -# Revision 1.4 2001/08/03 01:28:33 richard -# Used the much nicer load_package, pointed out by Steve Majewski. -# -# Revision 1.3 2001/08/03 00:59:34 richard -# Instance import now imports the instance using imp.load_module so that -# we can have instance homes of "roundup" or other existing python package -# names. -# -# Revision 1.2 2001/07/29 07:01:39 richard -# Added vim command to all source so that we don't get no steenkin' tabs :) -# -# Revision 1.1 2001/07/23 03:46:48 richard -# moving the bin files to facilitate out-of-the-boxness -# -# Revision 1.1 2001/07/22 11:15:45 richard -# More Grande Splite stuff -# -# -# vim: set filetype=python ts=4 sw=4 et si
--- a/roundup-server Wed Feb 06 03:47:17 2002 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,369 +0,0 @@ -#!/usr/bin/python -# -# 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. -# -""" HTTP Server that serves roundup. - -Based on CGIHTTPServer in the Python library. - -$Id: roundup-server,v 1.23 2001-12-15 23:47:07 richard Exp $ - -""" - -# python version check -from roundup import version_check - -import sys, os, urllib, StringIO, traceback, cgi, binascii, getopt, imp -import BaseHTTPServer - -# Roundup modules of use here -from roundup import cgitb, cgi_client -import roundup.instance - -# -## Configuration -# - -# This indicates where the Roundup instance lives -ROUNDUP_INSTANCE_HOMES = { - 'bar': '/tmp/bar', -} - -ROUNDUP_USER = None - - -# Where to log debugging information to. Use an instance of DevNull if you -# don't want to log anywhere. -# TODO: actually use this stuff -#class DevNull: -# def write(self, info): -# pass -#LOG = open('/var/log/roundup.cgi.log', 'a') -#LOG = DevNull() - -# -## end configuration -# - - -class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): - ROUNDUP_INSTANCE_HOMES = ROUNDUP_INSTANCE_HOMES - ROUNDUP_USER = ROUNDUP_USER - - def run_cgi(self): - """ Execute the CGI command. Wrap an innner call in an error - handler so all errors can be caught. - """ - save_stdin = sys.stdin - sys.stdin = self.rfile - try: - self.inner_run_cgi() - except cgi_client.NotFound: - self.send_error(404, self.path) - except cgi_client.Unauthorised: - self.send_error(403, self.path) - except: - # it'd be nice to be able to detect if these are going to have - # any effect... - self.send_response(400) - self.send_header('Content-Type', 'text/html') - self.end_headers() - try: - reload(cgitb) - self.wfile.write(cgitb.breaker()) - self.wfile.write(cgitb.html()) - except: - self.wfile.write("<pre>") - s = StringIO.StringIO() - traceback.print_exc(None, s) - self.wfile.write(cgi.escape(s.getvalue())) - self.wfile.write("</pre>\n") - sys.stdin = save_stdin - - do_GET = do_POST = do_HEAD = send_head = run_cgi - - def index(self): - ''' Print up an index of the available instances - ''' - self.send_response(200) - self.send_header('Content-Type', 'text/html') - self.end_headers() - w = self.wfile.write - w('<html><head><title>Roundup instances index</title></head>\n') - w('<body><h1>Roundup instances index</h1><ol>\n') - for instance in self.ROUNDUP_INSTANCE_HOMES.keys(): - w('<li><a href="%s/index">%s</a>\n'%(urllib.quote(instance), - cgi.escape(instance))) - w('</ol></body></html>') - - def inner_run_cgi(self): - ''' This is the inner part of the CGI handling - ''' - - rest = self.path - i = rest.rfind('?') - if i >= 0: - rest, query = rest[:i], rest[i+1:] - else: - query = '' - - # figure the instance - if rest == '/': - return self.index() - l_path = rest.split('/') - instance_name = urllib.unquote(l_path[1]) - if self.ROUNDUP_INSTANCE_HOMES.has_key(instance_name): - instance_home = self.ROUNDUP_INSTANCE_HOMES[instance_name] - instance = roundup.instance.open(instance_home) - else: - raise cgi_client.NotFound - - # figure out what the rest of the path is - if len(l_path) > 2: - rest = '/'.join(l_path[2:]) - else: - rest = '/' - - # Set up the CGI environment - env = {} - env['INSTANCE_NAME'] = instance_name - env['REQUEST_METHOD'] = self.command - env['PATH_INFO'] = urllib.unquote(rest) - if query: - env['QUERY_STRING'] = query - host = self.address_string() - if self.headers.typeheader is None: - env['CONTENT_TYPE'] = self.headers.type - else: - env['CONTENT_TYPE'] = self.headers.typeheader - length = self.headers.getheader('content-length') - if length: - env['CONTENT_LENGTH'] = length - co = filter(None, self.headers.getheaders('cookie')) - if co: - env['HTTP_COOKIE'] = ', '.join(co) - env['SCRIPT_NAME'] = '' - env['SERVER_NAME'] = self.server.server_name - env['SERVER_PORT'] = str(self.server.server_port) - - decoded_query = query.replace('+', ' ') - - # do the roundup thang - client = instance.Client(instance, self, env) - client.main() - -def usage(message=''): - if message: message = 'Error: %s\n\n'%message - print '''%sUsage: -roundup-server [-n hostname] [-p port] [name=instance home]* - - -n: sets the host name - -p: sets the port to listen on - - name=instance home - Sets the instance home(s) to use. The name is how the instance is - identified in the URL (it's the first part of the URL path). The - instance home is the directory that was identified when you did - "roundup-admin init". You may specify any number of these name=home - pairs on the command-line. For convenience, you may edit the - ROUNDUP_INSTANCE_HOMES variable in the roundup-server file instead. -'''%message - sys.exit(0) - -def main(): - hostname = '' - port = 8080 - try: - # handle the command-line args - try: - optlist, args = getopt.getopt(sys.argv[1:], 'n:p:u:') - except getopt.GetoptError, e: - usage(str(e)) - - user = ROUNDUP_USER - for (opt, arg) in optlist: - if opt == '-n': hostname = arg - elif opt == '-p': port = int(arg) - elif opt == '-u': user = arg - elif opt == '-h': usage() - - if hasattr(os, 'getuid'): - # if root, setuid to the running user - if not os.getuid() and user is not None: - try: - import pwd - except ImportError: - raise ValueError, "Can't change users - no pwd module" - try: - uid = pwd.getpwnam(user)[2] - except KeyError: - raise ValueError, "User %s doesn't exist"%user - os.setuid(uid) - elif os.getuid() and user is not None: - print 'WARNING: ignoring "-u" argument, not root' - - # People can remove this check if they're really determined - if not os.getuid() and user is None: - raise ValueError, "Can't run as root!" - - # handle instance specs - if args: - d = {} - for arg in args: - try: - name, home = arg.split('=') - except ValueError: - raise ValueError, "Instances must be name=home" - d[name] = home - RoundupRequestHandler.ROUNDUP_INSTANCE_HOMES = d - except SystemExit: - raise - except: - exc_type, exc_value = sys.exc_info()[:2] - usage('%s: %s'%(exc_type, exc_value)) - - # we don't want the cgi module interpreting the command-line args ;) - sys.argv = sys.argv[:1] - address = (hostname, port) - httpd = BaseHTTPServer.HTTPServer(address, RoundupRequestHandler) - print 'Roundup server started on', address - httpd.serve_forever() - -if __name__ == '__main__': - main() - -# -# $Log: not supported by cvs2svn $ -# Revision 1.22 2001/12/13 00:20:01 richard -# . Centralised the python version check code, bumped version to 2.1.1 (really -# needs to be 2.1.2, but that isn't released yet :) -# -# Revision 1.21 2001/12/02 05:06:16 richard -# . We now use weakrefs in the Classes to keep the database reference, so -# the close() method on the database is no longer needed. -# I bumped the minimum python requirement up to 2.1 accordingly. -# . #487480 ] roundup-server -# . #487476 ] INSTALL.txt -# -# I also cleaned up the change message / post-edit stuff in the cgi client. -# There's now a clearly marked "TODO: append the change note" where I believe -# the change note should be added there. The "changes" list will obviously -# have to be modified to be a dict of the changes, or somesuch. -# -# More testing needed. -# -# Revision 1.20 2001/11/26 22:55:56 richard -# Feature: -# . Added INSTANCE_NAME to configuration - used in web and email to identify -# the instance. -# . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup -# signature info in e-mails. -# . Some more flexibility in the mail gateway and more error handling. -# . Login now takes you to the page you back to the were denied access to. -# -# Fixed: -# . Lots of bugs, thanks Roché and others on the devel mailing list! -# -# Revision 1.19 2001/11/12 22:51:04 jhermann -# Fixed option & associated error handling -# -# Revision 1.18 2001/11/01 22:04:37 richard -# Started work on supporting a pop3-fetching server -# Fixed bugs: -# . bug #477104 ] HTML tag error in roundup-server -# . bug #477107 ] HTTP header problem -# -# Revision 1.17 2001/10/29 23:55:44 richard -# Fix to CGI top-level index (thanks Juergen Hermann) -# -# Revision 1.16 2001/10/27 00:12:21 richard -# Fixed roundup-server for windows, thanks Juergen Hermann. -# -# Revision 1.15 2001/10/12 02:23:26 richard -# Didn't clean up after myself :) -# -# Revision 1.14 2001/10/12 02:20:32 richard -# server now handles setuid'ing much better -# -# Revision 1.13 2001/10/05 02:23:24 richard -# . roundup-admin create now prompts for property info if none is supplied -# on the command-line. -# . hyperdb Class getprops() method may now return only the mutable -# properties. -# . Login now uses cookies, which makes it a whole lot more flexible. We can -# now support anonymous user access (read-only, unless there's an -# "anonymous" user, in which case write access is permitted). Login -# handling has been moved into cgi_client.Client.main() -# . The "extended" schema is now the default in roundup init. -# . The schemas have had their page headings modified to cope with the new -# login handling. Existing installations should copy the interfaces.py -# file from the roundup lib directory to their instance home. -# . Incorrectly had a Bizar Software copyright on the cgitb.py module from -# Ping - has been removed. -# . Fixed a whole bunch of places in the CGI interface where we should have -# been returning Not Found instead of throwing an exception. -# . Fixed a deviation from the spec: trying to modify the 'id' property of -# an item now throws an exception. -# -# Revision 1.12 2001/09/29 13:27:00 richard -# CGI interfaces now spit up a top-level index of all the instances they can -# serve. -# -# Revision 1.11 2001/08/07 00:24:42 richard -# stupid typo -# -# Revision 1.10 2001/08/07 00:15:51 richard -# Added the copyright/license notice to (nearly) all files at request of -# Bizar Software. -# -# Revision 1.9 2001/08/05 07:44:36 richard -# Instances are now opened by a special function that generates a unique -# module name for the instances on import time. -# -# Revision 1.8 2001/08/03 01:28:33 richard -# Used the much nicer load_package, pointed out by Steve Majewski. -# -# Revision 1.7 2001/08/03 00:59:34 richard -# Instance import now imports the instance using imp.load_module so that -# we can have instance homes of "roundup" or other existing python package -# names. -# -# Revision 1.6 2001/07/29 07:01:39 richard -# Added vim command to all source so that we don't get no steenkin' tabs :) -# -# Revision 1.5 2001/07/24 01:07:59 richard -# Added command-line arg handling to roundup-server so it's more useful -# out-of-the-box. -# -# Revision 1.4 2001/07/23 10:31:45 richard -# disabled the reloading until it can be done properly -# -# Revision 1.3 2001/07/23 08:53:44 richard -# Fixed the ROUNDUPS decl in roundup-server -# Move the installation notes to INSTALL -# -# Revision 1.2 2001/07/23 04:05:05 anthonybaxter -# actually quit if python version wrong -# -# Revision 1.1 2001/07/23 03:46:48 richard -# moving the bin files to facilitate out-of-the-boxness -# -# Revision 1.1 2001/07/22 11:15:45 richard -# More Grande Splite stuff -# -# -# vim: set filetype=python ts=4 sw=4 et si
--- a/roundup/admin.py Wed Feb 06 03:47:17 2002 +0000 +++ b/roundup/admin.py Wed Feb 06 04:05:55 2002 +0000 @@ -16,7 +16,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: admin.py,v 1.6 2002-01-23 07:27:19 grubert Exp $ +# $Id: admin.py,v 1.6.2.1 2002-02-06 04:05:53 richard Exp $ import sys, os, getpass, getopt, re, UserDict, shlex try: @@ -1039,6 +1039,9 @@ # # $Log: not supported by cvs2svn $ +# Revision 1.6 2002/01/23 07:27:19 grubert +# . allow abbreviation of "help" in admin tool too. +# # Revision 1.5 2002/01/21 16:33:19 rochecompaan # You can now use the roundup-admin tool to pack the database #
--- a/roundup/backends/__init__.py Wed Feb 06 03:47:17 2002 +0000 +++ b/roundup/backends/__init__.py Wed Feb 06 04:05:55 2002 +0000 @@ -15,16 +15,18 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: __init__.py,v 1.9 2001-12-12 02:30:51 richard Exp $ +# $Id: __init__.py,v 1.9.2.1 2002-02-06 04:05:54 richard Exp $ __all__ = [] try: - import anydbm, dumbdbm - # dumbdbm in python 2,2b2, 2.1.1 and earlier is seriously broken - assert anydbm._defaultmod != dumbdbm - del anydbm - del dumbdbm + import sys + if not hasattr(sys, 'version_info') or sys.version_info < (2,1,2): + import anydbm, dumbdbm + # dumbdbm only works in python 2.1.2+ + assert anydbm._defaultmod != dumbdbm + del anydbm + del dumbdbm import back_anydbm anydbm = back_anydbm __all__.append('anydbm') @@ -49,6 +51,19 @@ # # $Log: not supported by cvs2svn $ +# Revision 1.10 2002/01/22 07:08:50 richard +# I was certain I'd already done this (there's even a change note in +# CHANGES)... +# +# Revision 1.9 2001/12/12 02:30:51 richard +# I fixed the problems with people whose anydbm was using the dbm module at the +# backend. It turns out the dbm module modifies the file name to append ".db" +# and my check to determine if we're opening an existing or new db just +# tested os.path.exists() on the filename. Well, no longer! We now perform a +# much better check _and_ cope with the anydbm implementation module changing +# too! +# I also fixed the backends __init__ so only ImportError is squashed. +# # Revision 1.8 2001/12/10 22:20:01 richard # Enabled transaction support in the bsddb backend. It uses the anydbm code # where possible, only replacing methods where the db is opened (it uses the
--- a/roundup/backends/back_anydbm.py Wed Feb 06 03:47:17 2002 +0000 +++ b/roundup/backends/back_anydbm.py Wed Feb 06 04:05:55 2002 +0000 @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -#$Id: back_anydbm.py,v 1.21 2002-01-02 02:31:38 richard Exp $ +#$Id: back_anydbm.py,v 1.21.2.1 2002-02-06 04:05:54 richard Exp $ ''' This module defines a backend that saves the hyperdatabase in a database chosen by anydbm. It is guaranteed to always be available in python @@ -26,8 +26,6 @@ import whichdb, anydbm, os, marshal from roundup import hyperdb, date, password -DEBUG=os.environ.get('HYPERDBDEBUG', '') - # # Now the database # @@ -40,9 +38,10 @@ . perhaps detect write collisions (related to above)? """ - def __init__(self, storagelocator, journaltag=None): + def __init__(self, config, journaltag=None): """Open a hyperdatabase given a specifier to some storage. + The 'storagelocator' is obtained from config.DATABASE. The meaning of 'storagelocator' depends on the particular implementation of the hyperdatabase. It could be a file name, a directory path, a socket descriptor for a connection to a @@ -53,7 +52,8 @@ None, the database is opened in read-only mode: the Class.create(), Class.set(), and Class.retire() methods are disabled. """ - self.dir, self.journaltag = storagelocator, journaltag + self.config, self.journaltag = config, journaltag + self.dir = config.DATABASE self.classes = {} self.cache = {} # cache of nodes loaded or created self.dirtynodes = {} # keep track of the dirty nodes by class @@ -69,13 +69,13 @@ def __getattr__(self, classname): """A convenient way of calling self.getclass(classname).""" if self.classes.has_key(classname): - if DEBUG: + if hyperdb.DEBUG: print '__getattr__', (self, classname) return self.classes[classname] raise AttributeError, classname def addclass(self, cl): - if DEBUG: + if hyperdb.DEBUG: print 'addclass', (self, cl) cn = cl.classname if self.classes.has_key(cn): @@ -84,7 +84,7 @@ def getclasses(self): """Return a list of the names of all existing classes.""" - if DEBUG: + if hyperdb.DEBUG: print 'getclasses', (self,) l = self.classes.keys() l.sort() @@ -95,7 +95,7 @@ If 'classname' is not a valid class name, a KeyError is raised. """ - if DEBUG: + if hyperdb.DEBUG: print 'getclass', (self, classname) return self.classes[classname] @@ -105,7 +105,7 @@ def clear(self): '''Delete all database contents ''' - if DEBUG: + if hyperdb.DEBUG: print 'clear', (self,) for cn in self.classes.keys(): for type in 'nodes', 'journals': @@ -119,7 +119,7 @@ ''' grab a connection to the class db that will be used for multiple actions ''' - if DEBUG: + if hyperdb.DEBUG: print 'getclassdb', (self, classname, mode) return self._opendb('nodes.%s'%classname, mode) @@ -127,7 +127,7 @@ '''Low-level database opener that gets around anydbm/dbm eccentricities. ''' - if DEBUG: + if hyperdb.DEBUG: print '_opendb', (self, name, mode) # determine which DB wrote the class file db_type = '' @@ -143,7 +143,7 @@ # new database? let anydbm pick the best dbm if not db_type: - if DEBUG: + if hyperdb.DEBUG: print "_opendb anydbm.open(%r, 'n')"%path return anydbm.open(path, 'n') @@ -154,7 +154,7 @@ raise hyperdb.DatabaseError, \ "Couldn't open database - the required module '%s'"\ "is not available"%db_type - if DEBUG: + if hyperdb.DEBUG: print "_opendb %r.open(%r, %r)"%(db_type, path, mode) return dbm.open(path, mode) @@ -164,7 +164,7 @@ def addnode(self, classname, nodeid, node): ''' add the specified node to its class's db ''' - if DEBUG: + if hyperdb.DEBUG: print 'addnode', (self, classname, nodeid, node) self.newnodes.setdefault(classname, {})[nodeid] = 1 self.cache.setdefault(classname, {})[nodeid] = node @@ -173,7 +173,7 @@ def setnode(self, classname, nodeid, node): ''' change the specified node ''' - if DEBUG: + if hyperdb.DEBUG: print 'setnode', (self, classname, nodeid, node) self.dirtynodes.setdefault(classname, {})[nodeid] = 1 # can't set without having already loaded the node @@ -183,15 +183,15 @@ def savenode(self, classname, nodeid, node): ''' perform the saving of data specified by the set/addnode ''' - if DEBUG: + if hyperdb.DEBUG: print 'savenode', (self, classname, nodeid, node) self.transactions.append((self._doSaveNode, (classname, nodeid, node))) def getnode(self, classname, nodeid, db=None, cache=1): ''' get a node from the database ''' - if DEBUG: - print 'getnode', (self, classname, nodeid, cldb) + if hyperdb.DEBUG: + print 'getnode', (self, classname, nodeid, db) if cache: # try the cache cache = self.cache.setdefault(classname, {}) @@ -211,8 +211,8 @@ def hasnode(self, classname, nodeid, db=None): ''' determine if the database has a given node ''' - if DEBUG: - print 'hasnode', (self, classname, nodeid, cldb) + if hyperdb.DEBUG: + print 'hasnode', (self, classname, nodeid, db) # try the cache cache = self.cache.setdefault(classname, {}) if cache.has_key(nodeid): @@ -225,8 +225,8 @@ return res def countnodes(self, classname, db=None): - if DEBUG: - print 'countnodes', (self, classname, cldb) + if hyperdb.DEBUG: + print 'countnodes', (self, classname, db) # include the new nodes not saved to the DB yet count = len(self.newnodes.get(classname, {})) @@ -237,7 +237,7 @@ return count def getnodeids(self, classname, db=None): - if DEBUG: + if hyperdb.DEBUG: print 'getnodeids', (self, classname, db) # start off with the new nodes res = self.newnodes.get(classname, {}).keys() @@ -292,7 +292,7 @@ 'link' or 'unlink' -- 'params' is (classname, nodeid, propname) 'retire' -- 'params' is None ''' - if DEBUG: + if hyperdb.DEBUG: print 'addjournal', (self, classname, nodeid, action, params) self.transactions.append((self._doSaveJournal, (classname, nodeid, action, params))) @@ -300,7 +300,7 @@ def getjournal(self, classname, nodeid): ''' get the journal for id ''' - if DEBUG: + if hyperdb.DEBUG: print 'getjournal', (self, classname, nodeid) # attempt to open the journal - in some rare cases, the journal may # not exist @@ -318,6 +318,54 @@ res.append((nodeid, date_obj, self.journaltag, action, params)) return res + def pack(self, pack_before): + ''' delete all journal entries before 'pack_before' ''' + if hyperdb.DEBUG: + print 'packjournal', (self, pack_before) + + pack_before = pack_before.get_tuple() + + classes = self.getclasses() + + # TODO: factor this out to method - we're already doing it in + # _opendb. + db_type = '' + path = os.path.join(os.getcwd(), self.dir, classes[0]) + if os.path.exists(path): + db_type = whichdb.whichdb(path) + if not db_type: + raise hyperdb.DatabaseError, "Couldn't identify database type" + elif os.path.exists(path+'.db'): + db_type = 'dbm' + + for classname in classes: + db_name = 'journals.%s'%classname + db = self._opendb(db_name, 'w') + + for key in db.keys(): + journal = marshal.loads(db[key]) + l = [] + last_set_entry = None + for entry in journal: + (nodeid, date_stamp, self.journaltag, action, + params) = entry + if date_stamp > pack_before or action == 'create': + l.append(entry) + elif action == 'set': + # grab the last set entry to keep information on + # activity + last_set_entry = entry + if last_set_entry: + date_stamp = last_set_entry[1] + # if the last set entry was made after the pack date + # then it is already in the list + if date_stamp < pack_before: + l.append(last_set_entry) + db[key] = marshal.dumps(l) + if db_type == 'gdbm': + db.reorganize() + db.close() + # # Basic transaction support @@ -325,7 +373,7 @@ def commit(self): ''' Commit the current transactions. ''' - if DEBUG: + if hyperdb.DEBUG: print 'commit', (self,) # TODO: lock the DB @@ -349,7 +397,7 @@ self.transactions = [] def _doSaveNode(self, classname, nodeid, node): - if DEBUG: + if hyperdb.DEBUG: print '_doSaveNode', (self, classname, nodeid, node) # get the database handle @@ -363,7 +411,7 @@ db[nodeid] = marshal.dumps(node) def _doSaveJournal(self, classname, nodeid, action, params): - if DEBUG: + if hyperdb.DEBUG: print '_doSaveJournal', (self, classname, nodeid, action, params) entry = (nodeid, date.Date().get_tuple(), self.journaltag, action, params) @@ -391,12 +439,13 @@ def rollback(self): ''' Reverse all actions from the current transaction. ''' - if DEBUG: + if hyperdb.DEBUG: print 'rollback', (self, ) for method, args in self.transactions: # delete temporary files if method == self._doStoreFile: - os.remove(args[0]+".tmp") + if os.path.exists(args[0]+".tmp"): + os.remove(args[0]+".tmp") self.cache = {} self.dirtynodes = {} self.newnodes = {} @@ -404,6 +453,52 @@ # #$Log: not supported by cvs2svn $ +#Revision 1.27 2002/01/22 07:21:13 richard +#. fixed back_bsddb so it passed the journal tests +# +#... it didn't seem happy using the back_anydbm _open method, which is odd. +#Yet another occurrance of whichdb not being able to recognise older bsddb +#databases. Yadda yadda. Made the HYPERDBDEBUG stuff more sane in the +#process. +# +#Revision 1.26 2002/01/22 05:18:38 rochecompaan +#last_set_entry was referenced before assignment +# +#Revision 1.25 2002/01/22 05:06:08 rochecompaan +#We need to keep the last 'set' entry in the journal to preserve +#information on 'activity' for nodes. +# +#Revision 1.24 2002/01/21 16:33:20 rochecompaan +#You can now use the roundup-admin tool to pack the database +# +#Revision 1.23 2002/01/18 04:32:04 richard +#Rollback was breaking because a message hadn't actually been written to the file. Needs +#more investigation. +# +#Revision 1.22 2002/01/14 02:20:15 richard +# . changed all config accesses so they access either the instance or the +# config attriubute on the db. This means that all config is obtained from +# instance_config instead of the mish-mash of classes. This will make +# switching to a ConfigParser setup easier too, I hope. +# +#At a minimum, this makes migration a _little_ easier (a lot easier in the +#0.5.0 switch, I hope!) +# +#Revision 1.21 2002/01/02 02:31:38 richard +#Sorry for the huge checkin message - I was only intending to implement #496356 +#but I found a number of places where things had been broken by transactions: +# . modified ROUNDUPDBSENDMAILDEBUG to be SENDMAILDEBUG and hold a filename +# for _all_ roundup-generated smtp messages to be sent to. +# . the transaction cache had broken the roundupdb.Class set() reactors +# . newly-created author users in the mailgw weren't being committed to the db +# +#Stuff that made it into CHANGES.txt (ie. the stuff I was actually working +#on when I found that stuff :): +# . #496356 ] Use threading in messages +# . detectors were being registered multiple times +# . added tests for mailgw +# . much better attaching of erroneous messages in the mail gateway +# #Revision 1.20 2001/12/18 15:30:34 rochecompaan #Fixed bugs: # . Fixed file creation and retrieval in same transaction in anydbm
--- a/roundup/backends/back_bsddb.py Wed Feb 06 03:47:17 2002 +0000 +++ b/roundup/backends/back_bsddb.py Wed Feb 06 04:05:55 2002 +0000 @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -#$Id: back_bsddb.py,v 1.13 2001-12-10 22:20:01 richard Exp $ +#$Id: back_bsddb.py,v 1.13.2.1 2002-02-06 04:05:54 richard Exp $ ''' This module defines a backend that saves the hyperdatabase in BSDDB. ''' @@ -51,6 +51,24 @@ else: return bsddb.btopen(path, 'n') + def _opendb(self, name, mode): + '''Low-level database opener that gets around anydbm/dbm + eccentricities. + ''' + if hyperdb.DEBUG: + print self, '_opendb', (self, name, mode) + # determine which DB wrote the class file + path = os.path.join(os.getcwd(), self.dir, name) + if not os.path.exists(path): + if hyperdb.DEBUG: + print "_opendb bsddb.open(%r, 'n')"%path + return bsddb.btopen(path, 'n') + + # open the database with the correct module + if hyperdb.DEBUG: + print "_opendb bsddb.open(%r, %r)"%(path, mode) + return bsddb.btopen(path, mode) + # # Journal # @@ -91,6 +109,21 @@ # #$Log: not supported by cvs2svn $ +#Revision 1.14 2002/01/22 07:21:13 richard +#. fixed back_bsddb so it passed the journal tests +# +#... it didn't seem happy using the back_anydbm _open method, which is odd. +#Yet another occurrance of whichdb not being able to recognise older bsddb +#databases. Yadda yadda. Made the HYPERDBDEBUG stuff more sane in the +#process. +# +#Revision 1.13 2001/12/10 22:20:01 richard +#Enabled transaction support in the bsddb backend. It uses the anydbm code +#where possible, only replacing methods where the db is opened (it uses the +#btree opener specifically.) +#Also cleaned up some change note generation. +#Made the backends package work with pydoc too. +# #Revision 1.12 2001/11/21 02:34:18 richard #Added a target version field to the extended issue schema #
--- a/roundup/backends/back_bsddb3.py Wed Feb 06 03:47:17 2002 +0000 +++ b/roundup/backends/back_bsddb3.py Wed Feb 06 04:05:55 2002 +0000 @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -#$Id: back_bsddb3.py,v 1.10 2001-11-21 02:34:18 richard Exp $ +#$Id: back_bsddb3.py,v 1.10.2.1 2002-02-06 04:05:54 richard Exp $ import bsddb3, os, marshal from roundup import hyperdb, date, password @@ -26,9 +26,10 @@ class Database(hyperdb.Database): """A database for storing records containing flexible data types.""" - def __init__(self, storagelocator, journaltag=None): + def __init__(self, config, journaltag=None): """Open a hyperdatabase given a specifier to some storage. + The 'storagelocator' is obtained from config.DATABASE. The meaning of 'storagelocator' depends on the particular implementation of the hyperdatabase. It could be a file name, a directory path, a socket descriptor for a connection to a @@ -39,7 +40,8 @@ None, the database is opened in read-only mode: the Class.create(), Class.set(), and Class.retire() methods are disabled. """ - self.dir, self.journaltag = storagelocator, journaltag + self.config, self.journaltag = config, journaltag + self.dir = config.DATABASE self.classes = {} # @@ -201,6 +203,18 @@ # #$Log: not supported by cvs2svn $ +#Revision 1.11 2002/01/14 02:20:15 richard +# . changed all config accesses so they access either the instance or the +# config attriubute on the db. This means that all config is obtained from +# instance_config instead of the mish-mash of classes. This will make +# switching to a ConfigParser setup easier too, I hope. +# +#At a minimum, this makes migration a _little_ easier (a lot easier in the +#0.5.0 switch, I hope!) +# +#Revision 1.10 2001/11/21 02:34:18 richard +#Added a target version field to the extended issue schema +# #Revision 1.9 2001/10/09 23:58:10 richard #Moved the data stringification up into the hyperdb.Class class' get, set #and create methods. This means that the data is also stringified for the
--- a/roundup/cgi_client.py Wed Feb 06 03:47:17 2002 +0000 +++ b/roundup/cgi_client.py Wed Feb 06 04:05:55 2002 +0000 @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: cgi_client.py,v 1.88 2002-01-02 02:31:38 richard Exp $ +# $Id: cgi_client.py,v 1.88.2.1 2002-02-06 04:05:53 richard Exp $ __doc__ = """ WWW request handler (also used in the stand-alone server). @@ -44,21 +44,7 @@ 'anonymous' user exists, the user is logged in using that user (though there is no cookie). This allows them to modify the database, and all modifications are attributed to the 'anonymous' user. - - - Customisation - ------------- - FILTER_POSITION - one of 'top', 'bottom', 'top and bottom' - ANONYMOUS_ACCESS - one of 'deny', 'allow' - ANONYMOUS_REGISTER - one of 'deny', 'allow' - - from the roundup class: - INSTANCE_NAME - defaults to 'Roundup issue tracker' - ''' - FILTER_POSITION = 'bottom' # one of 'top', 'bottom', 'top and bottom' - ANONYMOUS_ACCESS = 'deny' # one of 'deny', 'allow' - ANONYMOUS_REGISTER = 'deny' # one of 'deny', 'allow' def __init__(self, instance, request, env, form=None): self.instance = instance @@ -104,7 +90,7 @@ message = _('<div class="system-msg">%(message)s</div>')%locals() else: message = '' - style = open(os.path.join(self.TEMPLATES, 'style.css')).read() + style = open(os.path.join(self.instance.TEMPLATES, 'style.css')).read() user_name = self.user or '' if self.user == 'admin': admin_links = _(' | <a href="list_classes">Class List</a>' \ @@ -286,7 +272,7 @@ cn = self.classname cl = self.db.classes[cn] self.pagehead(_('%(instancename)s: Index of %(classname)s')%{ - 'classname': cn, 'instancename': self.INSTANCE_NAME}) + 'classname': cn, 'instancename': self.instance.INSTANCE_NAME}) if sort is None: sort = self.index_arg(':sort') if group is None: group = self.index_arg(':group') if filter is None: filter = self.index_arg(':filter') @@ -295,7 +281,7 @@ if show_customization is None: show_customization = self.customization_widget() - index = htmltemplate.IndexTemplate(self, self.TEMPLATES, cn) + index = htmltemplate.IndexTemplate(self, self.instance.TEMPLATES, cn) index.render(filterspec, filter, columns, sort, group, show_customization=show_customization) self.pagefoot() @@ -312,19 +298,19 @@ # don't try to set properties if the user has just logged in if keys and not self.form.has_key('__login_name'): try: - props, changed = parsePropsFromForm(self.db, cl, self.form, - self.nodeid) + props = parsePropsFromForm(self.db, cl, self.form, self.nodeid) # make changes to the node self._changenode(props) # handle linked nodes self._post_editnode(self.nodeid) # and some nice feedback for the user - if changed: + if props: message = _('%(changes)s edited ok')%{'changes': - ', '.join(changed.keys())} + ', '.join(props.keys())} elif self.form.has_key('__note') and self.form['__note'].value: message = _('note added') - elif self.form.has_key('__file'): + elif (self.form.has_key('__file') and + self.form['__file'].filename): message = _('file added') else: message = _('nothing changed') @@ -343,13 +329,33 @@ nodeid = self.nodeid # use the template to display the item - item = htmltemplate.ItemTemplate(self, self.TEMPLATES, self.classname) + item = htmltemplate.ItemTemplate(self, self.instance.TEMPLATES, + self.classname) item.render(nodeid) self.pagefoot() showissue = shownode showmsg = shownode + def _add_assignedto_to_nosy(self, props): + ''' add the assignedto value from the props to the nosy list + ''' + if not props.has_key('assignedto'): + return + assignedto_id = props['assignedto'] + if not props.has_key('nosy'): + # load current nosy + if self.nodeid: + cl = self.db.classes[self.classname] + l = cl.get(self.nodeid, 'nosy') + if assignedto_id in l: + return + props['nosy'] = l + else: + props['nosy'] = [] + if assignedto_id not in props['nosy']: + props['nosy'].append(assignedto_id) + def _changenode(self, props): ''' change the node based on the contents of the form ''' @@ -361,22 +367,28 @@ resolved_id = self.db.status.lookup('resolved') chatting_id = self.db.status.lookup('chatting') current_status = cl.get(self.nodeid, 'status') + if props.has_key('status'): + new_status = props['status'] + else: + # apparently there's a chance that some browsers don't + # send status... + new_status = current_status except KeyError: pass else: - if (props['status'] == unread_id or props['status'] == resolved_id and current_status == resolved_id): + if new_status == unread_id or (new_status == resolved_id + and current_status == resolved_id): props['status'] = chatting_id - # add assignedto to the nosy list - if props.has_key('assignedto'): - assignedto_id = props['assignedto'] - if assignedto_id not in props['nosy']: - props['nosy'].append(assignedto_id) + + self._add_assignedto_to_nosy(props) + # create the message message, files = self._handle_message() if message: props['messages'] = cl.get(self.nodeid, 'messages') + [message] if files: props['files'] = cl.get(self.nodeid, 'files') + files + # make the changes cl.set(self.nodeid, **props) @@ -384,7 +396,7 @@ ''' create a node based on the contents of the form ''' cl = self.db.classes[self.classname] - props, dummy = parsePropsFromForm(self.db, cl, self.form) + props = parsePropsFromForm(self.db, cl, self.form) # set status to 'unread' if not specified - a status of '- no # selection -' doesn't make sense @@ -395,13 +407,9 @@ pass else: props['status'] = unread_id - # add assignedto to the nosy list - if props.has_key('assignedto'): - assignedto_id = props['assignedto'] - if props.has_key('nosy') and assignedto_id not in props['nosy']: - props['nosy'].append(assignedto_id) - else: - props['nosy'] = [assignedto_id] + + self._add_assignedto_to_nosy(props) + # check for messages and files message, files = self._handle_message() if message: @@ -412,7 +420,7 @@ return cl.create(**props) def _handle_message(self): - ''' generate and edit message + ''' generate an edit message ''' # handle file attachments files = [] @@ -433,7 +441,7 @@ props = cl.getprops() note = None # in a nutshell, don't do anything if there's no note or there's no - # nosy + # NOSY if self.form.has_key('__note'): note = self.form['__note'].value if not props.has_key('messages'): @@ -458,8 +466,8 @@ # handle the messageid # TODO: handle inreplyto - messageid = "%s.%s.%s%s-%s"%(time.time(), random.random(), - classname, nodeid, self.MAIL_DOMAIN) + messageid = "<%s.%s.%s@%s>"%(time.time(), random.random(), + self.classname, self.instance.MAIL_DOMAIN) # now create the message, attaching the files content = '\n'.join(m) @@ -549,7 +557,7 @@ self.nodeid = nid self.pagehead('%s: %s'%(self.classname.capitalize(), nid), message) - item = htmltemplate.ItemTemplate(self, self.TEMPLATES, + item = htmltemplate.ItemTemplate(self, self.instance.TEMPLATES, self.classname) item.render(nid) self.pagefoot() @@ -563,7 +571,7 @@ self.classname.capitalize()}, message) # call the template - newitem = htmltemplate.NewItemTemplate(self, self.TEMPLATES, + newitem = htmltemplate.NewItemTemplate(self, self.instance.TEMPLATES, self.classname) newitem.render(self.form) @@ -582,7 +590,7 @@ keys = self.form.keys() if [i for i in keys if i[0] != ':']: try: - props, dummy = parsePropsFromForm(self.db, cl, self.form) + props = parsePropsFromForm(self.db, cl, self.form) nid = cl.create(**props) # handle linked nodes self._post_editnode(nid) @@ -597,7 +605,7 @@ self.classname.capitalize()}, message) # call the template - newitem = htmltemplate.NewItemTemplate(self, self.TEMPLATES, + newitem = htmltemplate.NewItemTemplate(self, self.instance.TEMPLATES, self.classname) newitem.render(self.form) @@ -635,7 +643,7 @@ self.pagehead(_('New %(classname)s')%{'classname': self.classname.capitalize()}, message) - newitem = htmltemplate.NewItemTemplate(self, self.TEMPLATES, + newitem = htmltemplate.NewItemTemplate(self, self.instance.TEMPLATES, self.classname) newitem.render(self.form) self.pagefoot() @@ -662,21 +670,21 @@ num_re = re.compile('^\d+$') if keys: try: - props, changed = parsePropsFromForm(self.db, user, self.form, + props = parsePropsFromForm(self.db, user, self.form, self.nodeid) set_cookie = 0 - if self.nodeid == self.getuid() and changed.has_key('password'): + if props.has_key('password'): password = self.form['password'].value.strip() - if password: - set_cookie = password - else: + if not password: # no password was supplied - don't change it del props['password'] - del changed['password'] + elif self.nodeid == self.getuid(): + # this is the logged-in user's password + set_cookie = password user.set(self.nodeid, **props) # and some feedback for the user message = _('%(changes)s edited ok')%{'changes': - ', '.join(changed.keys())} + ', '.join(props.keys())} except: self.db.rollback() s = StringIO.StringIO() @@ -695,7 +703,7 @@ self.pagehead(_('User: %(user)s')%{'user': node_user}, message) # use the template to display the item - item = htmltemplate.ItemTemplate(self, self.TEMPLATES, 'user') + item = htmltemplate.ItemTemplate(self, self.instance.TEMPLATES, 'user') item.render(self.nodeid) self.pagefoot() @@ -748,7 +756,7 @@ <td><input type="submit" value="Log In"></td></tr> </form> ''')%locals()) - if self.user is None and self.ANONYMOUS_REGISTER == 'deny': + if self.user is None and self.instance.ANONYMOUS_REGISTER == 'deny': self.write('</table>') self.pagefoot() return @@ -833,7 +841,7 @@ # TODO: pre-check the required fields and username key property cl = self.db.user try: - props, dummy = parsePropsFromForm(self.db, cl, self.form) + props = parsePropsFromForm(self.db, cl, self.form) uid = cl.create(**props) except ValueError, message: action = self.form['__destination_url'].value @@ -875,7 +883,6 @@ path)}) self.login() - def main(self): '''Wrap the database accesses so we can close the database cleanly ''' @@ -942,7 +949,7 @@ if action == 'newuser_action': # if we don't have a login and anonymous people aren't allowed to # register, then spit up the login form - if self.ANONYMOUS_REGISTER == 'deny' and self.user is None: + if self.instance.ANONYMOUS_REGISTER == 'deny' and self.user is None: if action == 'login': self.login() # go to the index after login else: @@ -957,7 +964,7 @@ action = 'index' # no login or registration, make sure totally anonymous access is OK - elif self.ANONYMOUS_ACCESS == 'deny' and self.user is None: + elif self.instance.ANONYMOUS_ACCESS == 'deny' and self.user is None: if action == 'login': self.login() # go to the index after login else: @@ -1047,7 +1054,7 @@ message = _('<div class="system-msg">%(message)s</div>')%locals() else: message = '' - style = open(os.path.join(self.TEMPLATES, 'style.css')).read() + style = open(os.path.join(self.instance.TEMPLATES, 'style.css')).read() user_name = self.user or '' if self.user == 'admin': admin_links = _(' | <a href="list_classes">Class List</a>' \ @@ -1098,7 +1105,6 @@ '''Pull properties for the given class out of the form. ''' props = {} - changed = {} keys = form.keys() num_re = re.compile('^\d+$') for key in keys: @@ -1110,9 +1116,17 @@ elif isinstance(proptype, hyperdb.Password): value = password.Password(form[key].value.strip()) elif isinstance(proptype, hyperdb.Date): - value = date.Date(form[key].value.strip()) + value = form[key].value.strip() + if value: + value = date.Date(form[key].value.strip()) + else: + value = None elif isinstance(proptype, hyperdb.Interval): - value = date.Interval(form[key].value.strip()) + value = form[key].value.strip() + if value: + value = date.Interval(form[key].value.strip()) + else: + value = None elif isinstance(proptype, hyperdb.Link): value = form[key].value.strip() # see if it's the "no selection" choice @@ -1149,7 +1163,6 @@ l.append(entry) l.sort() value = l - props[key] = value # get the old value if nodeid: @@ -1160,14 +1173,79 @@ # value if not cl.properties.has_key(key): raise - # if changed, set it - if nodeid and value != existing: - changed[key] = value + # if changed, set it + if value != existing: + props[key] = value + else: props[key] = value - return props, changed + return props # # $Log: not supported by cvs2svn $ +# Revision 1.100 2002/01/16 07:02:57 richard +# . lots of date/interval related changes: +# - more relaxed date format for input +# +# Revision 1.99 2002/01/16 03:02:42 richard +# #503793 ] changing assignedto resets nosy list +# +# Revision 1.98 2002/01/14 02:20:14 richard +# . changed all config accesses so they access either the instance or the +# config attriubute on the db. This means that all config is obtained from +# instance_config instead of the mish-mash of classes. This will make +# switching to a ConfigParser setup easier too, I hope. +# +# At a minimum, this makes migration a _little_ easier (a lot easier in the +# 0.5.0 switch, I hope!) +# +# Revision 1.97 2002/01/11 23:22:29 richard +# . #502437 ] rogue reactor and unittest +# in short, the nosy reactor was modifying the nosy list. That code had +# been there for a long time, and I suspsect it was there because we +# weren't generating the nosy list correctly in other places of the code. +# We're now doing that, so the nosy-modifying code can go away from the +# nosy reactor. +# +# Revision 1.96 2002/01/10 05:26:10 richard +# missed a parsePropsFromForm in last update +# +# Revision 1.95 2002/01/10 03:39:45 richard +# . fixed some problems with web editing and change detection +# +# Revision 1.94 2002/01/09 13:54:21 grubert +# _add_assignedto_to_nosy did set nosy to assignedto only, no adding. +# +# Revision 1.93 2002/01/08 11:57:12 richard +# crying out for real configuration handling... :( +# +# Revision 1.92 2002/01/08 04:12:05 richard +# Changed message-id format to "<%s.%s.%s%s@%s>" so it complies with RFC822 +# +# Revision 1.91 2002/01/08 04:03:47 richard +# I mucked the intent of the code up. +# +# Revision 1.90 2002/01/08 03:56:55 richard +# Oops, missed this before the beta: +# . #495392 ] empty nosy -patch +# +# Revision 1.89 2002/01/07 20:24:45 richard +# *mutter* stupid cutnpaste +# +# Revision 1.88 2002/01/02 02:31:38 richard +# Sorry for the huge checkin message - I was only intending to implement #496356 +# but I found a number of places where things had been broken by transactions: +# . modified ROUNDUPDBSENDMAILDEBUG to be SENDMAILDEBUG and hold a filename +# for _all_ roundup-generated smtp messages to be sent to. +# . the transaction cache had broken the roundupdb.Class set() reactors +# . newly-created author users in the mailgw weren't being committed to the db +# +# Stuff that made it into CHANGES.txt (ie. the stuff I was actually working +# on when I found that stuff :): +# . #496356 ] Use threading in messages +# . detectors were being registered multiple times +# . added tests for mailgw +# . much better attaching of erroneous messages in the mail gateway +# # Revision 1.87 2001/12/23 23:18:49 richard # We already had an admin-specific section of the web heading, no need to add # another one :)
--- a/roundup/cgitb.py Wed Feb 06 03:47:17 2002 +0000 +++ b/roundup/cgitb.py Wed Feb 06 04:05:55 2002 +0000 @@ -1,7 +1,7 @@ # # This module was written by Ka-Ping Yee, <ping@lfw.org>. # -# $Id: cgitb.py,v 1.7 2001-11-22 15:46:42 jhermann Exp $ +# $Id: cgitb.py,v 1.7.2.1 2002-02-06 04:05:53 richard Exp $ __doc__ = """ Extended CGI traceback handler by Ka-Ping Yee, <ping@lfw.org>. @@ -9,6 +9,8 @@ import sys, os, types, string, keyword, linecache, tokenize, inspect, pydoc +from i18n import _ + def breaker(): return ('<body bgcolor="#f0f0ff">' + '<font color="#f0f0ff" size="-5"> > </font> ' + @@ -23,10 +25,10 @@ '<font size=+1><strong>%s</strong>: %s</font>'%(str(etype), str(evalue)), '#ffffff', '#aa55cc', pyver) - head = head + ('<p>A problem occurred while running a Python script. ' + head = head + (_('<p>A problem occurred while running a Python script. ' 'Here is the sequence of function calls leading up to ' 'the error, with the most recent (innermost) call first. ' - 'The exception attributes are:') + 'The exception attributes are:')) indent = '<tt><small>%s</small> </tt>' % (' ' * 5) traceback = [] @@ -48,7 +50,7 @@ <table width="100%%" bgcolor="#d8bbff" cellspacing=0 cellpadding=2 border=0> <tr><td>%s %s</td></tr></table>''' % (link, call) - if file is None: + if index is None or file is None: traceback.append('<p>' + level) continue @@ -73,13 +75,13 @@ if locals.has_key(name): value = pydoc.html.repr(locals[name]) else: - value = '<em>undefined</em>' + value = _('<em>undefined</em>') name = '<strong>%s</strong>' % name else: if frame.f_globals.has_key(name): value = pydoc.html.repr(frame.f_globals[name]) else: - value = '<em>undefined</em>' + value = _('<em>undefined</em>') name = '<em>global</em> <strong>%s</strong>' % name lvals.append('%s = %s' % (name, value)) if lvals: @@ -122,6 +124,19 @@ # # $Log: not supported by cvs2svn $ +# Revision 1.10 2002/01/16 04:49:45 richard +# Handle a special case that the CGI interface tickles. I need to check if +# this needs fixing in python's core. +# +# Revision 1.9 2002/01/08 11:56:24 richard +# missed an import _ +# +# Revision 1.8 2002/01/05 02:22:32 richard +# i18n'ification +# +# Revision 1.7 2001/11/22 15:46:42 jhermann +# Added module docstrings to all modules. +# # Revision 1.6 2001/09/29 13:27:00 richard # CGI interfaces now spit up a top-level index of all the instances they can # serve.
--- a/roundup/date.py Wed Feb 06 03:47:17 2002 +0000 +++ b/roundup/date.py Wed Feb 06 04:05:55 2002 +0000 @@ -15,13 +15,14 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: date.py,v 1.14 2001-11-22 15:46:42 jhermann Exp $ +# $Id: date.py,v 1.14.2.1 2002-02-06 04:05:53 richard Exp $ __doc__ = """ Date, time and time interval handling. """ import time, re, calendar +from i18n import _ class Date: ''' @@ -152,6 +153,8 @@ def __cmp__(self, other): """Compare this date to another date.""" + if other is None: + return 1 for attr in ('year', 'month', 'day', 'hour', 'minute', 'second'): r = cmp(getattr(self, attr), getattr(other, attr)) if r: return r @@ -165,11 +168,13 @@ def pretty(self): ''' print up the date date using a pretty format... ''' - return time.strftime('%e %B %Y', (self.year, self.month, + str = time.strftime('%d %B %Y', (self.year, self.month, self.day, self.hour, self.minute, self.second, 0, 0, 0)) + if str[0] == '0': return ' ' + str[1:] + return str def set(self, spec, offset=0, date_re=re.compile(r''' - (((?P<y>\d\d\d\d)-)?((?P<m>\d\d)-(?P<d>\d\d))?)? # yyyy-mm-dd + (((?P<y>\d\d\d\d)-)?((?P<m>\d\d?)-(?P<d>\d\d?))?)? # yyyy-mm-dd (?P<n>\.)? # . (((?P<H>\d?\d):(?P<M>\d\d))?(:(?P<S>\d\d))?)? # hh:mm:ss (?P<o>.+)? # offset @@ -178,7 +183,8 @@ ''' m = date_re.match(spec) if not m: - raise ValueError, 'Not a date spec: [[yyyy-]mm-dd].[[h]h:mm[:ss]] [offset]' + raise ValueError, _('Not a date spec: [[yyyy-]mm-dd].[[h]h:mm[:ss]]' + '[offset]') info = m.groupdict() # get the current date/time using the offset @@ -252,6 +258,8 @@ def __cmp__(self, other): """Compare this interval to another interval.""" + if other is None: + return 1 for attr in ('year', 'month', 'day', 'hour', 'minute', 'second'): r = cmp(getattr(self, attr), getattr(other, attr)) if r: return r @@ -292,7 +300,8 @@ self.sign = 1 m = interval_re.match(spec) if not m: - raise ValueError, 'Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]' + raise ValueError, _('Not an interval spec: [+-] [#y] [#m] [#w] ' + '[#d] [[[H]H:MM]:SS]') info = m.groupdict() for group, attr in {'y':'year', 'm':'month', 'w':'week', 'd':'day', @@ -309,15 +318,8 @@ def __repr__(self): return '<Interval %s>'%self.__str__() - def pretty(self, threshold=('d', 5)): + def pretty(self): ''' print up the date date using one of these nice formats.. - < 1 minute - < 15 minutes - < 30 minutes - < 1 hour - < 12 hours - < 1 day - otherwise, return None (so a full date may be displayed) ''' if self.year or self.month > 2: return None @@ -325,36 +327,36 @@ days = (self.month * 30) + self.day if days > 28: if int(days/30) > 1: - return '%s months'%int(days/30) + return _('%(number)s months')%{'number': int(days/30)} else: - return '1 month' + return _('1 month') else: - return '%s weeks'%int(days/7) + return _('%(number)s weeks')%{'number': int(days/7)} if self.day > 7: - return '1 week' + return _('1 week') if self.day > 1: - return '%s days'%self.day + return _('%(number)s days')%{'number': self.day} if self.day == 1 or self.hour > 12: - return 'yesterday' + return _('yesterday') if self.hour > 1: - return '%s hours'%self.hour + return _('%(number)s hours')%{'number': self.hour} if self.hour == 1: if self.minute < 15: - return 'an hour' + return _('an hour') quart = self.minute/15 if quart == 2: - return '1 1/2 hours' - return '1 %s/4 hours'%quart + return _('1 1/2 hours') + return _('1 %(number)s/4 hours')%{'number': quart} if self.minute < 1: - return 'just now' + return _('just now') if self.minute == 1: - return '1 minute' + return _('1 minute') if self.minute < 15: - return '%s minutes'%self.minute + return _('%(number)s minutes')%{'number': self.minute} quart = int(self.minute/15) if quart == 2: - return '1/2 an hour' - return '%s/4 hour'%quart + return _('1/2 an hour') + return _('%(number)s/4 hour')%{'number': quart} def get_tuple(self): return (self.year, self.month, self.day, self.hour, self.minute, @@ -383,6 +385,22 @@ # # $Log: not supported by cvs2svn $ +# Revision 1.18 2002/01/23 20:00:50 jhermann +# %e is a UNIXism and not documented for Python +# +# Revision 1.17 2002/01/16 07:02:57 richard +# . lots of date/interval related changes: +# - more relaxed date format for input +# +# Revision 1.16 2002/01/08 11:56:24 richard +# missed an import _ +# +# Revision 1.15 2002/01/05 02:27:00 richard +# I18N'ification +# +# Revision 1.14 2001/11/22 15:46:42 jhermann +# Added module docstrings to all modules. +# # Revision 1.13 2001/09/18 22:58:37 richard # # Added some more help to roundu-admin
--- a/roundup/htmltemplate.py Wed Feb 06 03:47:17 2002 +0000 +++ b/roundup/htmltemplate.py Wed Feb 06 04:05:55 2002 +0000 @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: htmltemplate.py,v 1.49 2001-12-20 15:43:01 rochecompaan Exp $ +# $Id: htmltemplate.py,v 1.49.2.1 2002-02-06 04:05:53 richard Exp $ __doc__ = """ Template engine. @@ -24,6 +24,7 @@ import os, re, StringIO, urllib, cgi, errno import hyperdb, date, password +from i18n import _ # This imports the StructureText functionality for the do_stext function # get it from http://dev.zope.org/Members/jim/StructuredTextWiki/NGReleases @@ -52,7 +53,7 @@ linked nodes (or the ids if the linked class has no key property) ''' if not self.nodeid and self.form is None: - return '[Field: not called from item]' + return _('[Field: not called from item]') propclass = self.properties[property] if self.nodeid: # make sure the property is a valid one @@ -76,20 +77,23 @@ else: value = str(value) elif isinstance(propclass, hyperdb.Password): if value is None: value = '' - else: value = '*encrypted*' + else: value = _('*encrypted*') elif isinstance(propclass, hyperdb.Date): + # this gives "2002-01-17.06:54:39", maybe replace the "." by a " ". value = str(value) elif isinstance(propclass, hyperdb.Interval): value = str(value) elif isinstance(propclass, hyperdb.Link): linkcl = self.db.classes[propclass.classname] k = linkcl.labelprop() - if value: value = str(linkcl.get(value, k)) - else: value = '[unselected]' + if value: + value = linkcl.get(value, k) + else: + value = _('[unselected]') elif isinstance(propclass, hyperdb.Multilink): linkcl = self.db.classes[propclass.classname] k = linkcl.labelprop() - value = ', '.join([linkcl.get(i, k) for i in value]) + value = ', '.join(value) else: s = _('Plain: bad propclass "%(propclass)s"')%locals() if escape: @@ -105,60 +109,141 @@ return s return StructuredText(s,level=1,header=0) - def do_field(self, property, size=None, height=None, showid=0): + def determine_value(self, property): + '''determine the value of a property using the node, form or + filterspec + ''' + propclass = self.properties[property] + if self.nodeid: + value = self.cl.get(self.nodeid, property, None) + if isinstance(propclass, hyperdb.Multilink) and value is None: + return [] + return value + elif self.filterspec is not None: + if isinstance(propclass, hyperdb.Multilink): + return self.filterspec.get(property, []) + else: + return self.filterspec.get(property, '') + # TODO: pull the value from the form + if isinstance(propclass, hyperdb.Multilink): + return [] + else: + return '' + + def make_sort_function(self, classname): + '''Make a sort function for a given class + ''' + linkcl = self.db.classes[classname] + if linkcl.getprops().has_key('order'): + sort_on = 'order' + else: + sort_on = linkcl.labelprop() + def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on): + return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on)) + return sortfunc + + def do_field(self, property, size=None, showid=0): ''' display a property like the plain displayer, but in a text field to be edited + + Note: if you would prefer an option list style display for + link or multilink editing, use menu(). ''' if not self.nodeid and self.form is None and self.filterspec is None: return _('[Field: not called from item]') + + if size is None: + size = 30 + propclass = self.properties[property] - if (isinstance(propclass, hyperdb.Link) or - isinstance(propclass, hyperdb.Multilink)): - linkcl = self.db.classes[propclass.classname] - def sortfunc(a, b, cl=linkcl): - if cl.getprops().has_key('order'): - sort_on = 'order' - else: - sort_on = cl.labelprop() - r = cmp(cl.get(a, sort_on), cl.get(b, sort_on)) - return r - if self.nodeid: - value = self.cl.get(self.nodeid, property, None) - # TODO: remove this from the code ... it's only here for - # handling schema changes, and they should be handled outside - # of this code... - if isinstance(propclass, hyperdb.Multilink) and value is None: - value = [] - elif self.filterspec is not None: - if isinstance(propclass, hyperdb.Multilink): - value = self.filterspec.get(property, []) - else: - value = self.filterspec.get(property, '') - else: - # TODO: pull the value from the form - if isinstance(propclass, hyperdb.Multilink): value = [] - else: value = '' + + # get the value + value = self.determine_value(property) + + # now display if (isinstance(propclass, hyperdb.String) or isinstance(propclass, hyperdb.Date) or isinstance(propclass, hyperdb.Interval)): - size = size or 30 if value is None: value = '' else: - value = cgi.escape(value) + value = cgi.escape(str(value)) value = '"'.join(value.split('"')) s = '<input name="%s" value="%s" size="%s">'%(property, value, size) elif isinstance(propclass, hyperdb.Password): - size = size or 30 s = '<input type="password" name="%s" size="%s">'%(property, size) elif isinstance(propclass, hyperdb.Link): + sortfunc = self.make_sort_function(propclass.classname) + linkcl = self.db.classes[propclass.classname] + options = linkcl.list() + options.sort(sortfunc) + # TODO: make this a field display, not a menu one! l = ['<select name="%s">'%property] k = linkcl.labelprop() if value is None: s = 'selected ' else: s = '' - l.append('<option %svalue="-1">- no selection -</option>'%s) + l.append(_('<option %svalue="-1">- no selection -</option>')%s) + for optionid in options: + option = linkcl.get(optionid, k) + s = '' + if optionid == value: + s = 'selected ' + if showid: + lab = '%s%s: %s'%(propclass.classname, optionid, option) + else: + lab = option + if size is not None and len(lab) > size: + lab = lab[:size-3] + '...' + lab = cgi.escape(lab) + l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab)) + l.append('</select>') + s = '\n'.join(l) + elif isinstance(propclass, hyperdb.Multilink): + sortfunc = self.make_sort_function(propclass.classname) + linkcl = self.db.classes[propclass.classname] + list = linkcl.list() + list.sort(sortfunc) + l = [] + # map the id to the label property + if not showid: + k = linkcl.labelprop() + value = [linkcl.get(v, k) for v in value] + value = cgi.escape(','.join(value)) + s = '<input name="%s" size="%s" value="%s">'%(property, size, value) + else: + s = _('Plain: bad propclass "%(propclass)s"')%locals() + return s + + def do_menu(self, property, size=None, height=None, showid=0): + ''' for a Link property, display a menu of the available choices + ''' + if not self.nodeid and self.form is None and self.filterspec is None: + return _('[Field: not called from item]') + + propclass = self.properties[property] + + # make sure this is a link property + if not (isinstance(propclass, hyperdb.Link) or + isinstance(propclass, hyperdb.Multilink)): + return _('[Menu: not a link]') + + # sort function + sortfunc = self.make_sort_function(propclass.classname) + + # get the value + value = self.determine_value(property) + + # display + if isinstance(propclass, hyperdb.Link): + linkcl = self.db.classes[propclass.classname] + l = ['<select name="%s">'%property] + k = linkcl.labelprop() + s = '' + if value is None: + s = 'selected ' + l.append(_('<option %svalue="-1">- no selection -</option>')%s) options = linkcl.list() options.sort(sortfunc) for optionid in options: @@ -172,67 +257,18 @@ lab = option if size is not None and len(lab) > size: lab = lab[:size-3] + '...' + lab = cgi.escape(lab) l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab)) l.append('</select>') - s = '\n'.join(l) - elif isinstance(propclass, hyperdb.Multilink): - list = linkcl.list() - list.sort(sortfunc) - k = linkcl.labelprop() - l = [] - # special treatment for nosy list - if property == 'nosy': - input_value = [] - else: - input_value = value - for v in value: - lab = linkcl.get(v, k) - if property != 'nosy': - l.append('<a href="issue%s">%s: %s</a>'%(v,v,lab)) - else: - input_value.append(lab) - if size is None: - size = '10' - l.insert(0,'<input name="%s" size="%s" value="%s">'%(property, - size, ','.join(input_value))) - s = "<br>\n".join(l) - else: - s = 'Plain: bad propclass "%s"'%propclass - return s - - def do_menu(self, property, size=None, height=None, showid=0): - ''' for a Link property, display a menu of the available choices - ''' - propclass = self.properties[property] - if self.nodeid: - value = self.cl.get(self.nodeid, property) - else: - # TODO: pull the value from the form - if isinstance(propclass, hyperdb.Multilink): value = [] - else: value = None - if isinstance(propclass, hyperdb.Link): - linkcl = self.db.classes[propclass.classname] - l = ['<select name="%s">'%property] - k = linkcl.labelprop() - s = '' - if value is None: - s = 'selected ' - l.append('<option %svalue="-1">- no selection -</option>'%s) - for optionid in linkcl.list(): - option = linkcl.get(optionid, k) - s = '' - if optionid == value: - s = 'selected ' - l.append('<option %svalue="%s">%s</option>'%(s, optionid, option)) - l.append('</select>') return '\n'.join(l) if isinstance(propclass, hyperdb.Multilink): linkcl = self.db.classes[propclass.classname] - list = linkcl.list() - height = height or min(len(list), 7) + options = linkcl.list() + options.sort(sortfunc) + height = height or min(len(options), 7) l = ['<select multiple name="%s" size="%s">'%(property, height)] k = linkcl.labelprop() - for optionid in list: + for optionid in options: option = linkcl.get(optionid, k) s = '' if optionid in value: @@ -243,10 +279,12 @@ lab = option if size is not None and len(lab) > size: lab = lab[:size-3] + '...' - l.append('<option %svalue="%s">%s</option>'%(s, optionid, option)) + lab = cgi.escape(lab) + l.append('<option %svalue="%s">%s</option>'%(s, optionid, + lab)) l.append('</select>') return '\n'.join(l) - return '[Menu: not a link]' + return _('[Menu: not a link]') #XXX deviates from spec def do_link(self, property=None, is_download=0): @@ -260,20 +298,19 @@ downloaded file name is correct. ''' if not self.nodeid and self.form is None: - return '[Link: not called from item]' + return _('[Link: not called from item]') + + # get the value + value = self.determine_value(property) + if not value: + return _('[no %(propname)s]')%{'propname':property.capitalize()} + propclass = self.properties[property] - if self.nodeid: - value = self.cl.get(self.nodeid, property) - else: - if isinstance(propclass, hyperdb.Multilink): value = [] - elif isinstance(propclass, hyperdb.Link): value = None - else: value = '' if isinstance(propclass, hyperdb.Link): linkname = propclass.classname - if value is None: return '[no %s]'%property.capitalize() linkcl = self.db.classes[linkname] k = linkcl.labelprop() - linkvalue = linkcl.get(value, k) + linkvalue = cgi.escape(linkcl.get(value, k)) if is_download: return '<a href="%s%s/%s">%s</a>'%(linkname, value, linkvalue, linkvalue) @@ -283,10 +320,9 @@ linkname = propclass.classname linkcl = self.db.classes[linkname] k = linkcl.labelprop() - if not value : return '[no %s]'%property.capitalize() l = [] for value in value: - linkvalue = linkcl.get(value, k) + linkvalue = cgi.escape(linkcl.get(value, k)) if is_download: l.append('<a href="%s%s/%s">%s</a>'%(linkname, value, linkvalue, linkvalue)) @@ -294,8 +330,6 @@ l.append('<a href="%s%s">%s</a>'%(linkname, value, linkvalue)) return ', '.join(l) - if isinstance(propclass, hyperdb.String): - if value == '': value = '[no %s]'%property.capitalize() if is_download: return '<a href="%s%s/%s">%s</a>'%(self.classname, self.nodeid, value, value) @@ -307,12 +341,15 @@ the list ''' if not self.nodeid: - return '[Count: not called from item]' + return _('[Count: not called from item]') + propclass = self.properties[property] + if not isinstance(propclass, hyperdb.Multilink): + return _('[Count: not a Multilink]') + + # figure the length then... value = self.cl.get(self.nodeid, property) - if isinstance(propclass, hyperdb.Multilink): - return str(len(value)) - return '[Count: not a Multilink]' + return str(len(value)) # XXX pretty is definitely new ;) def do_reldate(self, property, pretty=0): @@ -322,18 +359,24 @@ with the 'pretty' flag, make it pretty ''' if not self.nodeid and self.form is None: - return '[Reldate: not called from item]' + return _('[Reldate: not called from item]') + propclass = self.properties[property] - if isinstance(not propclass, hyperdb.Date): - return '[Reldate: not a Date]' + if not isinstance(propclass, hyperdb.Date): + return _('[Reldate: not a Date]') + if self.nodeid: value = self.cl.get(self.nodeid, property) else: - value = date.Date('.') + return '' + if not value: + return '' + + # figure the interval interval = value - date.Date('.') if pretty: if not self.nodeid: - return 'now' + return _('now') pretty = interval.pretty() if pretty is None: pretty = value.pretty() @@ -345,21 +388,8 @@ allow you to download files ''' if not self.nodeid: - return '[Download: not called from item]' - propclass = self.properties[property] - value = self.cl.get(self.nodeid, property) - if isinstance(propclass, hyperdb.Link): - linkcl = self.db.classes[propclass.classname] - linkvalue = linkcl.get(value, k) - return '<a href="%s%s">%s</a>'%(linkcl, value, linkvalue) - if isinstance(propclass, hyperdb.Multilink): - linkcl = self.db.classes[propclass.classname] - l = [] - for value in value: - linkvalue = linkcl.get(value, k) - l.append('<a href="%s%s">%s</a>'%(linkcl, value, linkvalue)) - return ', '.join(l) - return '[Download: not a link]' + return _('[Download: not called from item]') + return self.do_link(property, is_download=1) def do_checklist(self, property, **args): @@ -369,7 +399,7 @@ propclass = self.properties[property] if (not isinstance(propclass, hyperdb.Link) and not isinstance(propclass, hyperdb.Multilink)): - return '[Checklist: not a link]' + return _('[Checklist: not a link]') # get our current checkbox state if self.nodeid: @@ -390,7 +420,7 @@ l = [] k = linkcl.labelprop() for optionid in linkcl.list(): - option = linkcl.get(optionid, k) + option = cgi.escape(linkcl.get(optionid, k)) if optionid in value or option in value: checked = 'checked' else: @@ -404,8 +434,8 @@ checked = 'checked' else: checked = '' - l.append('[unselected]:<input type="checkbox" %s name="%s" ' - 'value="-1">'%(checked, property)) + l.append(_('[unselected]:<input type="checkbox" %s name="%s" ' + 'value="-1">')%(checked, property)) return '\n'.join(l) def do_note(self, rows=5, cols=80): @@ -423,10 +453,18 @@ ''' propcl = self.properties[property] if not isinstance(propcl, hyperdb.Multilink): - return '[List: not a Multilink]' - value = self.cl.get(self.nodeid, property) + return _('[List: not a Multilink]') + + value = self.determine_value(property) + if not value: + return '' + + # sort, possibly revers and then re-stringify + value = map(int, value) + value.sort() if reverse: value.reverse() + value = map(str, value) # render the sub-index into a string fp = StringIO.StringIO() @@ -441,22 +479,141 @@ return fp.getvalue() # XXX new function - def do_history(self, **args): + def do_history(self, direction='descending'): ''' list the history of the item + + If "direction" is 'descending' then the most recent event will + be displayed first. If it is 'ascending' then the oldest event + will be displayed first. ''' if self.nodeid is None: - return "[History: node doesn't exist]" + return _("[History: node doesn't exist]") l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>', '<tr class="list-header">', - '<td><span class="list-item"><strong>Date</strong></span></td>', - '<td><span class="list-item"><strong>User</strong></span></td>', - '<td><span class="list-item"><strong>Action</strong></span></td>', - '<td><span class="list-item"><strong>Args</strong></span></td>'] + _('<th align=left><span class="list-item">Date</span></th>'), + _('<th align=left><span class="list-item">User</span></th>'), + _('<th align=left><span class="list-item">Action</span></th>'), + _('<th align=left><span class="list-item">Args</span></th>'), + '</tr>'] + + comments = {} + history = self.cl.history(self.nodeid) + history.sort() + if direction == 'descending': + history.reverse() + for id, evt_date, user, action, args in history: + date_s = str(evt_date).replace("."," ") + arg_s = '' + if action == 'link' and type(args) == type(()): + if len(args) == 3: + linkcl, linkid, key = args + arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid, + linkcl, linkid, key) + else: + arg_s = str(arg) + + elif action == 'unlink' and type(args) == type(()): + if len(args) == 3: + linkcl, linkid, key = args + arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid, + linkcl, linkid, key) + else: + arg_s = str(arg) + + elif type(args) == type({}): + cell = [] + for k in args.keys(): + # try to get the relevant property and treat it + # specially + try: + prop = self.properties[k] + except: + prop = None + if prop is not None: + if args[k] and (isinstance(prop, hyperdb.Multilink) or + isinstance(prop, hyperdb.Link)): + # figure what the link class is + classname = prop.classname + try: + linkcl = self.db.classes[classname] + except KeyError, message: + labelprop = None + comments[classname] = _('''The linked class + %(classname)s no longer exists''')%locals() + labelprop = linkcl.labelprop() - for id, date, user, action, args in self.cl.history(self.nodeid): - l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%( - date, user, action, args)) + if isinstance(prop, hyperdb.Multilink) and \ + len(args[k]) > 0: + ml = [] + for linkid in args[k]: + label = classname + linkid + # if we have a label property, try to use it + # TODO: test for node existence even when + # there's no labelprop! + try: + if labelprop is not None: + label = linkcl.get(linkid, labelprop) + except IndexError: + comments['no_link'] = _('''<strike>The + linked node no longer + exists</strike>''') + ml.append('<strike>%s</strike>'%label) + else: + ml.append('<a href="%s%s">%s</a>'%( + classname, linkid, label)) + cell.append('%s:\n %s'%(k, ',\n '.join(ml))) + elif isinstance(prop, hyperdb.Link) and args[k]: + label = classname + args[k] + # if we have a label property, try to use it + # TODO: test for node existence even when + # there's no labelprop! + if labelprop is not None: + try: + label = linkcl.get(args[k], labelprop) + except IndexError: + comments['no_link'] = _('''<strike>The + linked node no longer + exists</strike>''') + cell.append(' <strike>%s</strike>,\n'%label) + # "flag" this is done .... euwww + label = None + if label is not None: + cell.append('%s: <a href="%s%s">%s</a>\n'%(k, + classname, args[k], label)) + + elif isinstance(prop, hyperdb.Date) and args[k]: + d = date.Date(args[k]) + cell.append('%s: %s'%(k, str(d))) + + elif isinstance(prop, hyperdb.Interval) and args[k]: + d = date.Interval(args[k]) + cell.append('%s: %s'%(k, str(d))) + + elif not args[k]: + cell.append('%s: (no value)\n'%k) + + else: + cell.append('%s: %s\n'%(k, str(args[k]))) + else: + # property no longer exists + comments['no_exist'] = _('''<em>The indicated property + no longer exists</em>''') + cell.append('<em>%s: %s</em>\n'%(k, str(args[k]))) + arg_s = '<br />'.join(cell) + else: + # unkown event!! + comments['unknown'] = _('''<strong><em>This event is not + handled by the history display!</em></strong>''') + arg_s = '<strong><em>' + str(args) + '</em></strong>' + date_s = date_s.replace(' ', ' ') + l.append('<tr><td nowrap valign=top>%s</td><td valign=top>%s</td>' + '<td valign=top>%s</td><td valign=top>%s</td></tr>'%(date_s, + user, action, arg_s)) + if comments: + l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>')) + for entry in comments.values(): + l.append('<tr><td colspan=4>%s</td></tr>'%entry) l.append('</table>') return '\n'.join(l) @@ -465,11 +622,11 @@ ''' add a submit button for the item ''' if self.nodeid: - return '<input type="submit" value="Submit Changes">' + return _('<input type="submit" name="submit" value="Submit Changes">') elif self.form is not None: - return '<input type="submit" value="Submit New Entry">' + return _('<input type="submit" name="submit" value="Submit New Entry">') else: - return '[Submit: not called from item]' + return _('[Submit: not called from item]') # @@ -503,6 +660,7 @@ class IndexTemplate(TemplateFunctions): def __init__(self, client, templates, classname): self.client = client + self.instance = client.instance self.templates = templates self.classname = classname @@ -548,9 +706,9 @@ columns = l # display the filter section - if (show_display_form and hasattr(self.client, 'FILTER_POSITION') and - self.client.FILTER_POSITION in ('top and bottom', 'top')): - w('<form action="index">\n') + if (show_display_form and + self.instance.FILTER_POSITION in ('top and bottom', 'top')): + w('<form action="%s">\n'%self.classname) self.filter_section(filter_template, filter, columns, group, all_filters, all_columns, show_customization) # make sure that the sorting doesn't get lost either @@ -589,7 +747,7 @@ for nodeid in nodeids: # check for a group heading if group_names: - this_group = [self.cl.get(nodeid, name, '[no value]') for name in group_names] + this_group = [self.cl.get(nodeid, name, _('[no value]')) for name in group_names] if this_group != old_group: l = [] for name in group_names: @@ -599,7 +757,8 @@ key = group_cl.getkey() value = self.cl.get(nodeid, name) if value is None: - l.append('[unselected %s]'%prop.classname) + l.append(_('[unselected %(classname)s]')%{ + 'classname': prop.classname}) else: l.append(group_cl.get(self.cl.get(nodeid, name), key)) @@ -609,9 +768,9 @@ for value in self.cl.get(nodeid, name): l.append(group_cl.get(value, key)) else: - value = self.cl.get(nodeid, name, '[no value]') + value = self.cl.get(nodeid, name, _('[no value]')) if value is None: - value = '[empty %s]'%name + value = _('[empty %(name)s]')%locals() else: value = str(value) l.append(value) @@ -629,9 +788,9 @@ w('</table>') # display the filter section - if (show_display_form and hasattr(self.client, 'FILTER_POSITION') and - self.client.FILTER_POSITION in ('top and bottom', 'bottom')): - w('<form action="index">\n') + if (show_display_form and hasattr(self.instance, 'FILTER_POSITION') and + self.instance.FILTER_POSITION in ('top and bottom', 'bottom')): + w('<form action="%s">\n'%self.classname) self.filter_section(filter_template, filter, columns, group, all_filters, all_columns, show_customization) # make sure that the sorting doesn't get lost either @@ -654,12 +813,12 @@ # display the filter section w('<table width=100% border=0 cellspacing=0 cellpadding=2>') w('<tr class="location-bar">') - w(' <th align="left" colspan="2">Filter specification...</th>') + w(_(' <th align="left" colspan="2">Filter specification...</th>')) w('</tr>') replace = IndexTemplateReplace(self.globals, locals(), filter) w(replace.go(template)) w('<tr class="location-bar"><td width="1%%"> </td>') - w('<td><input type="submit" name="action" value="Redisplay"></td></tr>') + w(_('<td><input type="submit" name="action" value="Redisplay"></td></tr>')) w('</table>') # now add in the filter/columns/group/etc config table form @@ -685,9 +844,9 @@ w('<input type="hidden" name=":group" value="%s">' % name) # TODO: The widget style can go into the stylesheet - w('<th align="left" colspan=%s>' + w(_('<th align="left" colspan=%s>' '<input style="height : 1em; width : 1em; font-size: 12pt" type="submit" name="action" value="%s"> View ' - 'customisation...</th></tr>\n'%(len(names)+1, action)) + 'customisation...</th></tr>\n')%(len(names)+1, action)) if not show_customization: w('</table>\n') @@ -700,8 +859,7 @@ # Filter if all_filters: - w('<tr><th width="1%" align=right class="location-bar">' - 'Filters</th>\n') + w(_('<tr><th width="1%" align=right class="location-bar">Filters</th>\n')) for name in names: if name not in all_filters: w('<td> </td>') @@ -715,8 +873,7 @@ # Columns if all_columns: - w('<tr><th width="1%" align=right class="location-bar">' - 'Columns</th>\n') + w(_('<tr><th width="1%" align=right class="location-bar">Columns</th>\n')) for name in names: if name not in all_columns: w('<td> </td>') @@ -729,8 +886,7 @@ w('</tr>\n') # Grouping - w('<tr><th width="1%" align=right class="location-bar">' - 'Grouping</th>\n') + w(_('<tr><th width="1%" align=right class="location-bar">Grouping</th>\n')) for name in names: prop = self.properties[name] if name not in all_columns: @@ -745,7 +901,7 @@ w('<tr class="location-bar"><td width="1%"> </td>') w('<td colspan="%s">'%len(names)) - w('<input type="submit" name="action" value="Redisplay"></td>') + w(_('<input type="submit" name="action" value="Redisplay"></td>')) w('</tr>\n') w('</table>\n') @@ -822,6 +978,7 @@ class ItemTemplate(TemplateFunctions): def __init__(self, client, templates, classname): self.client = client + self.instance = client.instance self.templates = templates self.classname = classname @@ -854,6 +1011,7 @@ class NewItemTemplate(TemplateFunctions): def __init__(self, client, templates, classname): self.client = client + self.instance = client.instance self.templates = templates self.classname = classname @@ -885,6 +1043,98 @@ # # $Log: not supported by cvs2svn $ +# Revision 1.71 2002/01/23 06:15:24 richard +# real (non-string, duh) sorting of lists by node id +# +# Revision 1.70 2002/01/23 05:47:57 richard +# more HTML template cleanup and unit tests +# +# Revision 1.69 2002/01/23 05:10:27 richard +# More HTML template cleanup and unit tests. +# - download() now implemented correctly, replacing link(is_download=1) [fixed in the +# templates, but link(is_download=1) will still work for existing templates] +# +# Revision 1.68 2002/01/22 22:55:28 richard +# . htmltemplate list() wasn't sorting... +# +# Revision 1.67 2002/01/22 22:46:22 richard +# more htmltemplate cleanups and unit tests +# +# Revision 1.66 2002/01/22 06:35:40 richard +# more htmltemplate tests and cleanup +# +# Revision 1.65 2002/01/22 00:12:06 richard +# Wrote more unit tests for htmltemplate, and while I was at it, I polished +# off the implementation of some of the functions so they behave sanely. +# +# Revision 1.64 2002/01/21 03:25:59 richard +# oops +# +# Revision 1.63 2002/01/21 02:59:10 richard +# Fixed up the HTML display of history so valid links are actually displayed. +# Oh for some unit tests! :( +# +# Revision 1.62 2002/01/18 08:36:12 grubert +# . add nowrap to history table date cell i.e. <td nowrap ... +# +# Revision 1.61 2002/01/17 23:04:53 richard +# . much nicer history display (actualy real handling of property types etc) +# +# Revision 1.60 2002/01/17 08:48:19 grubert +# . display superseder as html link in history. +# +# Revision 1.59 2002/01/17 07:58:24 grubert +# . display links a html link in history. +# +# Revision 1.58 2002/01/15 00:50:03 richard +# #502949 ] index view for non-issues and redisplay +# +# Revision 1.57 2002/01/14 23:31:21 richard +# reverted the change that had plain() hyperlinking the link displays - +# that's what link() is for! +# +# Revision 1.56 2002/01/14 07:04:36 richard +# . plain rendering of links in the htmltemplate now generate a hyperlink to +# the linked node's page. +# ... this allows a display very similar to bugzilla's where you can actually +# find out information about the linked node. +# +# Revision 1.55 2002/01/14 06:45:03 richard +# . #502953 ] nosy-like treatment of other multilinks +# ... had to revert most of the previous change to the multilink field +# display... not good. +# +# Revision 1.54 2002/01/14 05:16:51 richard +# The submit buttons need a name attribute or mozilla won't submit without a +# file upload. Yeah, that's bloody obscure. Grr. +# +# Revision 1.53 2002/01/14 04:03:32 richard +# How about that ... date fields have never worked ... +# +# Revision 1.52 2002/01/14 02:20:14 richard +# . changed all config accesses so they access either the instance or the +# config attriubute on the db. This means that all config is obtained from +# instance_config instead of the mish-mash of classes. This will make +# switching to a ConfigParser setup easier too, I hope. +# +# At a minimum, this makes migration a _little_ easier (a lot easier in the +# 0.5.0 switch, I hope!) +# +# Revision 1.51 2002/01/10 10:02:15 grubert +# In do_history: replace "." in date by " " so html wraps more sensible. +# Should this be done in date's string converter ? +# +# Revision 1.50 2002/01/05 02:35:10 richard +# I18N'ification +# +# Revision 1.49 2001/12/20 15:43:01 rochecompaan +# Features added: +# . Multilink properties are now displayed as comma separated values in +# a textbox +# . The add user link is now only visible to the admin user +# . Modified the mail gateway to reject submissions from unknown +# addresses if ANONYMOUS_ACCESS is denied +# # Revision 1.48 2001/12/20 06:13:24 rochecompaan # Bugs fixed: # . Exception handling in hyperdb for strings-that-look-like numbers got
--- a/roundup/hyperdb.py Wed Feb 06 03:47:17 2002 +0000 +++ b/roundup/hyperdb.py Wed Feb 06 04:05:55 2002 +0000 @@ -15,18 +15,19 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: hyperdb.py,v 1.45 2002-01-02 04:18:17 richard Exp $ +# $Id: hyperdb.py,v 1.45.2.1 2002-02-06 04:05:53 richard Exp $ __doc__ = """ Hyperdatabase implementation, especially field types. """ # standard python modules -import cPickle, re, string, weakref +import cPickle, re, string, weakref, os # roundup modules import date, password +DEBUG = os.environ.get('HYPERDBDEBUG', '') # # Types @@ -54,17 +55,24 @@ class Link: """An object designating a Link property that links to a node in a specified class.""" - def __init__(self, classname): + def __init__(self, classname, do_journal='no'): self.classname = classname + self.do_journal = do_journal == 'yes' def __repr__(self): return '<%s to "%s">'%(self.__class__, self.classname) class Multilink: """An object designating a Multilink property that links to nodes in a specified class. + + "classname" indicates the class to link to + + "do_journal" indicates whether the linked-to nodes should have + 'link' and 'unlink' events placed in their journal """ - def __init__(self, classname): + def __init__(self, classname, do_journal='no'): self.classname = classname + self.do_journal = do_journal == 'yes' def __repr__(self): return '<%s to "%s">'%(self.__class__, self.classname) @@ -98,9 +106,11 @@ # flag to set on retired entries RETIRED_FLAG = '__hyperdb_retired' - def __init__(self, storagelocator, journaltag=None): + # XXX deviates from spec: storagelocator is obtained from the config + def __init__(self, config, journaltag=None): """Open a hyperdatabase given a specifier to some storage. + The 'storagelocator' is obtained from config.DATABASE. The meaning of 'storagelocator' depends on the particular implementation of the hyperdatabase. It could be a file name, a directory path, a socket descriptor for a connection to a @@ -202,6 +212,11 @@ ''' raise NotImplementedError + def pack(self, pack_before): + ''' pack the database + ''' + raise NotImplementedError + def commit(self): ''' Commit the current transactions. @@ -307,8 +322,9 @@ propvalues[key] = value # register the link with the newly linked node - self.db.addjournal(link_class, value, 'link', - (self.classname, newid, key)) + if self.properties[key].do_journal: + self.db.addjournal(link_class, value, 'link', + (self.classname, newid, key)) elif isinstance(prop, Multilink): if type(value) != type([]): @@ -334,8 +350,9 @@ if not self.db.hasnode(link_class, id): raise IndexError, '%s has no node %s'%(link_class, id) # register the link with the newly linked node - self.db.addjournal(link_class, id, 'link', - (self.classname, newid, key)) + if self.properties[key].do_journal: + self.db.addjournal(link_class, id, 'link', + (self.classname, newid, key)) elif isinstance(prop, String): if type(value) != type(''): @@ -346,11 +363,11 @@ raise TypeError, 'new property "%s" not a Password'%key elif isinstance(prop, Date): - if not isinstance(value, date.Date): + if value is not None and not isinstance(value, date.Date): raise TypeError, 'new property "%s" not a Date'%key elif isinstance(prop, Interval): - if not isinstance(value, date.Interval): + if value is not None and not isinstance(value, date.Interval): raise TypeError, 'new property "%s" not an Interval'%key # make sure there's data where there needs to be @@ -362,14 +379,17 @@ if isinstance(prop, Multilink): propvalues[key] = [] else: + # TODO: None isn't right here, I think... propvalues[key] = None # convert all data to strings for key, prop in self.properties.items(): if isinstance(prop, Date): - propvalues[key] = propvalues[key].get_tuple() + if propvalues[key] is not None: + propvalues[key] = propvalues[key].get_tuple() elif isinstance(prop, Interval): - propvalues[key] = propvalues[key].get_tuple() + if propvalues[key] is not None: + propvalues[key] = propvalues[key].get_tuple() elif isinstance(prop, Password): propvalues[key] = str(propvalues[key]) @@ -393,18 +413,30 @@ if propname == 'id': return nodeid + # get the property (raises KeyErorr if invalid) + prop = self.properties[propname] + # get the node's dict d = self.db.getnode(self.classname, nodeid, cache=cache) - if not d.has_key(propname) and default is not _marker: - return default - # get the value - prop = self.properties[propname] + if not d.has_key(propname): + if default is _marker: + if isinstance(prop, Multilink): + return [] + else: + # TODO: None isn't right here, I think... + return None + else: + return default # possibly convert the marshalled data to instances if isinstance(prop, Date): + if d[propname] is None: + return None return date.Date(d[propname]) elif isinstance(prop, Interval): + if d[propname] is None: + return None return date.Interval(d[propname]) elif isinstance(prop, Password): p = password.Password() @@ -488,15 +520,16 @@ if not self.db.hasnode(link_class, value): raise IndexError, '%s has no node %s'%(link_class, value) - # register the unlink with the old linked node - if node[key] is not None: - self.db.addjournal(link_class, node[key], 'unlink', - (self.classname, nodeid, key)) + if self.properties[key].do_journal: + # register the unlink with the old linked node + if node[key] is not None: + self.db.addjournal(link_class, node[key], 'unlink', + (self.classname, nodeid, key)) - # register the link with the newly linked node - if value is not None: - self.db.addjournal(link_class, value, 'link', - (self.classname, nodeid, key)) + # register the link with the newly linked node + if value is not None: + self.db.addjournal(link_class, value, 'link', + (self.classname, nodeid, key)) elif isinstance(prop, Multilink): if type(value) != type([]): @@ -517,25 +550,31 @@ value = l propvalues[key] = value - #handle removals - l = node[key] + # handle removals + if node.has_key(key): + l = node[key] + else: + l = [] for id in l[:]: if id in value: continue # register the unlink with the old linked node - self.db.addjournal(link_class, id, 'unlink', - (self.classname, nodeid, key)) + if self.properties[key].do_journal: + self.db.addjournal(link_class, id, 'unlink', + (self.classname, nodeid, key)) l.remove(id) # handle additions for id in value: if not self.db.hasnode(link_class, id): - raise IndexError, '%s has no node %s'%(link_class, id) + raise IndexError, '%s has no node %s'%( + link_class, id) if id in l: continue # register the link with the newly linked node - self.db.addjournal(link_class, id, 'link', - (self.classname, nodeid, key)) + if self.properties[key].do_journal: + self.db.addjournal(link_class, id, 'link', + (self.classname, nodeid, key)) l.append(id) elif isinstance(prop, String): @@ -547,12 +586,12 @@ raise TypeError, 'new property "%s" not a Password'% key propvalues[key] = value = str(value) - elif isinstance(prop, Date): + elif value is not None and isinstance(prop, Date): if not isinstance(value, date.Date): raise TypeError, 'new property "%s" not a Date'% key propvalues[key] = value = value.get_tuple() - elif isinstance(prop, Interval): + elif value is not None and isinstance(prop, Interval): if not isinstance(value, date.Interval): raise TypeError, 'new property "%s" not an Interval'% key propvalues[key] = value = value.get_tuple() @@ -978,7 +1017,7 @@ def __init__(self, cl, nodeid, cache=1): self.__dict__['cl'] = cl self.__dict__['nodeid'] = nodeid - self.cache = cache + self.__dict__['cache'] = cache def keys(self, protected=1): return self.cl.getprops(protected=protected).keys() def values(self, protected=1): @@ -1027,6 +1066,46 @@ # # $Log: not supported by cvs2svn $ +# Revision 1.53 2002/01/22 07:21:13 richard +# . fixed back_bsddb so it passed the journal tests +# +# ... it didn't seem happy using the back_anydbm _open method, which is odd. +# Yet another occurrance of whichdb not being able to recognise older bsddb +# databases. Yadda yadda. Made the HYPERDBDEBUG stuff more sane in the +# process. +# +# Revision 1.52 2002/01/21 16:33:19 rochecompaan +# You can now use the roundup-admin tool to pack the database +# +# Revision 1.51 2002/01/21 03:01:29 richard +# brief docco on the do_journal argument +# +# Revision 1.50 2002/01/19 13:16:04 rochecompaan +# Journal entries for link and multilink properties can now be switched on +# or off. +# +# Revision 1.49 2002/01/16 07:02:57 richard +# . lots of date/interval related changes: +# - more relaxed date format for input +# +# Revision 1.48 2002/01/14 06:32:34 richard +# . #502951 ] adding new properties to old database +# +# Revision 1.47 2002/01/14 02:20:15 richard +# . changed all config accesses so they access either the instance or the +# config attriubute on the db. This means that all config is obtained from +# instance_config instead of the mish-mash of classes. This will make +# switching to a ConfigParser setup easier too, I hope. +# +# At a minimum, this makes migration a _little_ easier (a lot easier in the +# 0.5.0 switch, I hope!) +# +# Revision 1.46 2002/01/07 10:42:23 richard +# oops +# +# Revision 1.45 2002/01/02 04:18:17 richard +# hyperdb docstrings +# # Revision 1.44 2002/01/02 02:31:38 richard # Sorry for the huge checkin message - I was only intending to implement #496356 # but I found a number of places where things had been broken by transactions:
--- a/roundup/mailgw.py Wed Feb 06 03:47:17 2002 +0000 +++ b/roundup/mailgw.py Wed Feb 06 04:05:55 2002 +0000 @@ -73,7 +73,7 @@ an exception, the original message is bounced back to the sender with the explanatory message given in the exception. -$Id: mailgw.py,v 1.47 2002-01-02 02:32:38 richard Exp $ +$Id: mailgw.py,v 1.47.2.1 2002-02-06 04:05:53 richard Exp $ ''' @@ -90,6 +90,9 @@ class MailUsageError(ValueError): pass +class MailUsageHelp(Exception): + pass + class UnAuthorized(Exception): """ Access denied """ @@ -116,7 +119,7 @@ s.seek(0) return Message(s) -subject_re = re.compile(r'(?P<refwd>\s*\W?\s*(fwd|re)\s*\W?\s*)*' +subject_re = re.compile(r'(?P<refwd>\s*\W?\s*(fwd|re|aw)\s*\W?\s*)*' r'\s*(\[(?P<classname>[^\d\s]+)(?P<nodeid>\d+)?\])' r'\s*(?P<title>[^[]+)?(\[(?P<args>.+?)\])?', re.I) @@ -145,6 +148,15 @@ if sendto: try: return self.handle_message(message) + except MailUsageHelp: + # bounce the message back to the sender with the usage message + fulldoc = '\n'.join(string.split(__doc__, '\n')[2:]) + sendto = [sendto[0][1]] + m = [''] + m.append('\n\nMail Gateway Help\n=================') + m.append(fulldoc) + m = self.bounce_message(message, sendto, m, + subject="Mail Gateway Help") except MailUsageError, value: # bounce the message back to the sender with the usage message fulldoc = '\n'.join(string.split(__doc__, '\n')[2:]) @@ -162,8 +174,11 @@ m = self.bounce_message(message, sendto, m) except: # bounce the message back to the sender with the error message - sendto = [sendto[0][1]] + sendto = [sendto[0][1], self.instance.ADMIN_EMAIL] m = [''] + m.append('An unexpected error occurred during the processing') + m.append('of your message. The tracker administrator is being') + m.append('notified.\n') m.append('---- traceback of failure ----') s = cStringIO.StringIO() import traceback @@ -172,7 +187,7 @@ m = self.bounce_message(message, sendto, m) else: # very bad-looking message - we don't even know who sent it - sendto = [self.ADMIN_EMAIL] + sendto = [self.instance.ADMIN_EMAIL] m = ['Subject: badly formed message from mail gateway'] m.append('') m.append('The mail gateway retrieved a message which has no From:') @@ -185,11 +200,11 @@ # now send the message if SENDMAILDEBUG: open(SENDMAILDEBUG, 'w').write('From: %s\nTo: %s\n%s\n'%( - self.ADMIN_EMAIL, ', '.join(sendto), m.getvalue())) + self.instance.ADMIN_EMAIL, ', '.join(sendto), m.getvalue())) else: try: - smtp = smtplib.SMTP(self.MAILHOST) - smtp.sendmail(self.ADMIN_EMAIL, sendto, m.getvalue()) + smtp = smtplib.SMTP(self.instance.MAILHOST) + smtp.sendmail(self.instance.ADMIN_EMAIL, sendto, m.getvalue()) except socket.error, value: raise MailGWError, "Couldn't send error email: "\ "mailhost %s"%value @@ -206,7 +221,7 @@ writer = MimeWriter.MimeWriter(msg) writer.addheader('Subject', subject) writer.addheader('From', '%s <%s>'% (self.instance.INSTANCE_NAME, - self.ISSUE_TRACKER_EMAIL)) + self.instance.ISSUE_TRACKER_EMAIL)) writer.addheader('To', ','.join(sendto)) writer.addheader('MIME-Version', '1.0') part = writer.startmultipartbody('mixed') @@ -228,13 +243,17 @@ w.addheader(header_name, message.getheader(header_name)) # now attach the message body body = w.startbody(content_type) - message.rewindbody() - body.write(message.fp.read()) + try: + message.rewindbody() + except IOError: + body.write("*** couldn't include message body: read from pipe ***") + else: + body.write(message.fp.read()) # attach the original message to the returned message part = writer.nextpart() part.addheader('Content-Disposition','attachment') - part.addheader('Content-Description','Message that caused the error') + part.addheader('Content-Description','Message you sent') part.addheader('Content-Transfer-Encoding', '7bit') body = part.startbody('message/rfc822') body.write(m.getvalue()) @@ -249,6 +268,10 @@ ''' # handle the subject line subject = message.getheader('subject', '') + + if subject.strip() == 'help': + raise MailUsageHelp + m = subject_re.match(subject) if not m: raise MailUsageError, ''' @@ -319,6 +342,7 @@ args = m.group('args') if args: for prop in string.split(args, ';'): + # extract the property name and value try: key, value = prop.split('=') except ValueError, message: @@ -328,6 +352,8 @@ Subject was: "%s" '''%(message, subject) + + # ensure it's a valid property name key = key.strip() try: proptype = properties[key] @@ -337,6 +363,8 @@ Subject was: "%s" '''%(key, subject) + + # convert the string value to a real property value if isinstance(proptype, hyperdb.String): props[key] = value.strip() if isinstance(proptype, hyperdb.Password): @@ -362,32 +390,43 @@ Subject was: "%s" '''%(key, message, subject) elif isinstance(proptype, hyperdb.Link): - link = self.db.classes[proptype.classname] - propkey = link.labelprop(default_to_id=1) + linkcl = self.db.classes[proptype.classname] + propkey = linkcl.labelprop(default_to_id=1) try: - props[key] = link.get(value.strip(), propkey) - except: - props[key] = link.lookup(value.strip()) + props[key] = linkcl.lookup(value) + except KeyError, message: + raise MailUsageError, ''' +Subject argument list contains an invalid value for %s. + +Error was: %s +Subject was: "%s" +'''%(key, message, subject) elif isinstance(proptype, hyperdb.Multilink): - link = self.db.classes[proptype.classname] - propkey = link.labelprop(default_to_id=1) - l = [x.strip() for x in value.split(',')] - for item in l: + # get the linked class + linkcl = self.db.classes[proptype.classname] + propkey = linkcl.labelprop(default_to_id=1) + for item in value.split(','): + item = item.strip() try: - v = link.get(item, propkey) - except: - v = link.lookup(item) + item = linkcl.lookup(item) + except KeyError, message: + raise MailUsageError, ''' +Subject argument list contains an invalid value for %s. + +Error was: %s +Subject was: "%s" +'''%(key, message, subject) if props.has_key(key): - props[key].append(v) + props[key].append(item) else: - props[key] = [v] + props[key] = [item] # # handle the users # # Don't create users if ANONYMOUS_REGISTER is denied - if self.ANONYMOUS_REGISTER == 'deny': + if self.instance.ANONYMOUS_REGISTER == 'deny': create = 0 else: create = 1 @@ -413,7 +452,7 @@ # now update the recipients list recipients = [] - tracker_email = self.ISSUE_TRACKER_EMAIL.lower() + tracker_email = self.instance.ISSUE_TRACKER_EMAIL.lower() for recipient in message.getaddrlist('to') + message.getaddrlist('cc'): r = recipient[1].strip().lower() if r == tracker_email or not r: @@ -427,8 +466,8 @@ inreplyto = message.getheader('in-reply-to') or '' # generate a messageid if there isn't one if not messageid: - messageid = "%s.%s.%s%s-%s"%(time.time(), random.random(), - classname, nodeid, self.MAIL_DOMAIN) + messageid = "<%s.%s.%s%s@%s>"%(time.time(), random.random(), + classname, nodeid, self.instance.MAIL_DOMAIN) # # now handle the body - find the message @@ -448,8 +487,28 @@ subtype = part.gettype() if subtype == 'text/plain' and not content: # add all text/plain parts to the message content + # BUG (in code or comment) only add the first one. if content is None: - content = part.fp.read() + # try name on Content-Type + # maybe add name to non text content ? + name = part.getparam('name') + # assume first part is the mail + encoding = part.getencoding() + if encoding == 'base64': + # BUG: is base64 really used for text encoding or + # are we inserting zip files here. + data = binascii.a2b_base64(part.fp.read()) + elif encoding == 'quoted-printable': + # the quopri module wants to work with files + decoded = cStringIO.StringIO() + quopri.decode(part.fp, decoded) + data = decoded.getvalue() + elif encoding == 'uuencoded': + data = binascii.a2b_uu(part.fp.read()) + else: + # take it as text + data = part.fp.read() + content = data else: content = content + part.fp.read() @@ -477,7 +536,6 @@ elif encoding == 'uuencoded': data = binascii.a2b_uu(part.fp.read()) attachments.append((name, part.gettype(), data)) - if content is None: raise MailUsageError, ''' Roundup requires the submission to be plain text. The message parser could @@ -510,8 +568,23 @@ ''' else: - content = message.fp.read() - + encoding = message.getencoding() + if encoding == 'base64': + # BUG: is base64 really used for text encoding or + # are we inserting zip files here. + data = binascii.a2b_base64(message.fp.read()) + elif encoding == 'quoted-printable': + # the quopri module wants to work with files + decoded = cStringIO.StringIO() + quopri.decode(message.fp, decoded) + data = decoded.getvalue() + elif encoding == 'uuencoded': + data = binascii.a2b_uu(message.fp.read()) + else: + # take it as text + data = message.fp.read() + content = data + summary, content = parseContent(content) # @@ -519,6 +592,8 @@ # files = [] for (name, mime_type, data) in attachments: + if not name: + name = "unnamed" files.append(self.db.file.create(type=mime_type, name=name, content=data)) @@ -542,9 +617,10 @@ except KeyError: pass else: + current_status = cl.get(nodeid, 'status') if (not props.has_key('status') and - properties['status'] == unread_id or - properties['status'] == resolved_id): + current_status == unread_id or + current_status == resolved_id): props['status'] = chatting_id # add nosy in arguments to issue's nosy list @@ -561,12 +637,10 @@ n[nid] = 1 props['nosy'] = n.keys() # add assignedto to the nosy list - try: - assignedto = self.db.user.lookup(props['assignedto']) + if props.has_key('assignedto'): + assignedto = props['assignedto'] if assignedto not in props['nosy']: props['nosy'].append(assignedto) - except: - pass message_id = self.db.msg.create(author=author, recipients=recipients, date=date.Date('.'), summary=summary, @@ -625,10 +699,7 @@ nosy = props.get('nosy', []) n = {} for value in nosy: - if self.db.hasnode('user', value): - nid = value - else: - continue + nid = value if n.has_key(nid): continue n[nid] = 1 props['nosy'] = n.keys() @@ -645,13 +716,7 @@ # add assignedto to the nosy list if properties.has_key('assignedto') and props.has_key('assignedto'): - try: - assignedto = self.db.user.lookup(props['assignedto']) - except KeyError: - raise MailUsageError, ''' -There was a problem with the message you sent: - Assignedto user '%s' doesn't exist -'''%props['assignedto'] + assignedto = props['assignedto'] if not n.has_key(assignedto): props['nosy'].append(assignedto) n[assignedto] = 1 @@ -693,21 +758,100 @@ if not section: continue lines = eol.split(section) - if lines[0] and lines[0][0] in '>|': - continue - if len(lines) > 1 and lines[1] and lines[1][0] in '>|': - continue + if (lines[0] and lines[0][0] in '>|') or (len(lines) > 1 and + lines[1] and lines[1][0] in '>|'): + # see if there's a response somewhere inside this section (ie. + # no blank line between quoted message and response) + for line in lines[1:]: + if line[0] not in '>|': + break + else: + # TODO: people who want to keep quoted bits will want the + # next line... + # l.append(section) + continue + # keep this section - it has reponse stuff in it + if not summary: + # and while we're at it, use the first non-quoted bit as + # our summary + summary = line + lines = lines[lines.index(line):] + section = '\n'.join(lines) + if not summary: + # if we don't have our summary yet use the first line of this + # section summary = lines[0] - l.append(section) - continue - if signature.match(lines[0]): + elif signature.match(lines[0]): break + + # and add the section to the output l.append(section) return summary, '\n\n'.join(l) # # $Log: not supported by cvs2svn $ +# Revision 1.62 2002/02/05 14:15:29 grubert +# . respect encodings in non multipart messages. +# +# Revision 1.61 2002/02/04 09:40:21 grubert +# . add test for multipart messages with first part being encoded. +# +# Revision 1.60 2002/02/01 07:43:12 grubert +# . mailgw checks encoding on first part too. +# +# Revision 1.59 2002/01/23 21:43:23 richard +# tabnuke +# +# Revision 1.58 2002/01/23 21:41:56 richard +# . mailgw failures (unexpected ones) are forwarded to the roundup admin +# +# Revision 1.57 2002/01/22 22:27:43 richard +# . handle stripping of "AW:" from subject line +# +# Revision 1.56 2002/01/22 11:54:45 rochecompaan +# Fixed status change in mail gateway. +# +# Revision 1.55 2002/01/21 10:05:47 rochecompaan +# Feature: +# . the mail gateway now responds with an error message when invalid +# values for arguments are specified for link or multilink properties +# . modified unit test to check nosy and assignedto when specified as +# arguments +# +# Fixed: +# . fixed setting nosy as argument in subject line +# +# Revision 1.54 2002/01/16 09:14:45 grubert +# . if the attachment has no name, name it unnamed, happens with tnefs. +# +# Revision 1.53 2002/01/16 07:20:54 richard +# simple help command for mailgw +# +# Revision 1.52 2002/01/15 00:12:40 richard +# #503340 ] creating issue with [asignedto=p.ohly] +# +# Revision 1.51 2002/01/14 02:20:15 richard +# . changed all config accesses so they access either the instance or the +# config attriubute on the db. This means that all config is obtained from +# instance_config instead of the mish-mash of classes. This will make +# switching to a ConfigParser setup easier too, I hope. +# +# At a minimum, this makes migration a _little_ easier (a lot easier in the +# 0.5.0 switch, I hope!) +# +# Revision 1.50 2002/01/11 22:59:01 richard +# . #502342 ] pipe interface +# +# Revision 1.49 2002/01/10 06:19:18 richard +# followup lines directly after a quoted section were being eaten. +# +# Revision 1.48 2002/01/08 04:12:05 richard +# Changed message-id format to "<%s.%s.%s%s@%s>" so it complies with RFC822 +# +# Revision 1.47 2002/01/02 02:32:38 richard +# ANONYMOUS_ACCESS -> ANONYMOUS_REGISTER +# # Revision 1.46 2002/01/02 02:31:38 richard # Sorry for the huge checkin message - I was only intending to implement #496356 # but I found a number of places where things had been broken by transactions:
--- a/roundup/roundupdb.py Wed Feb 06 03:47:17 2002 +0000 +++ b/roundup/roundupdb.py Wed Feb 06 04:05:55 2002 +0000 @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: roundupdb.py,v 1.36 2002-01-02 02:31:38 richard Exp $ +# $Id: roundupdb.py,v 1.36.2.1 2002-02-06 04:05:54 richard Exp $ __doc__ = """ Extending hyperdb with types specific to issue-tracking. @@ -237,10 +237,6 @@ # XXX deviation from spec - was called ItemClass class IssueClass(Class): - # configuration - MESSAGES_TO_AUTHOR = 'no' - INSTANCE_NAME = 'Roundup issue tracker' - EMAIL_SIGNATURE_POSITION = 'bottom' # Overridden methods: @@ -303,7 +299,7 @@ # possibly send the message to the author, as long as they aren't # anonymous - if (self.MESSAGES_TO_AUTHOR == 'yes' and + if (self.db.config.MESSAGES_TO_AUTHOR == 'yes' and users.get(authid, 'username') != 'anonymous'): sendto.append(authid) r[authid] = 1 @@ -331,8 +327,8 @@ if not messageid: # this is an old message that didn't get a messageid, so # create one - messageid = "%s.%s.%s%s-%s"%(time.time(), random.random(), - self.classname, nodeid, self.MAIL_DOMAIN) + messageid = "<%s.%s.%s%s@%s>"%(time.time(), random.random(), + self.classname, nodeid, self.db.config.MAIL_DOMAIN) messages.set(msgid, messageid=messageid) # update the message's recipients list @@ -356,7 +352,7 @@ m = [''] # put in roundup's signature - if self.EMAIL_SIGNATURE_POSITION == 'top': + if self.db.config.EMAIL_SIGNATURE_POSITION == 'top': m.append(self.email_signature(nodeid, msgid)) # add author information @@ -374,20 +370,21 @@ m.append(change_note) # put in roundup's signature - if self.EMAIL_SIGNATURE_POSITION == 'bottom': + if self.db.config.EMAIL_SIGNATURE_POSITION == 'bottom': m.append(self.email_signature(nodeid, msgid)) # get the files for this message - files = messages.get(msgid, 'files') + message_files = messages.get(msgid, 'files') # create the message message = cStringIO.StringIO() writer = MimeWriter.MimeWriter(message) writer.addheader('Subject', '[%s%s] %s'%(cn, nodeid, title)) writer.addheader('To', ', '.join(sendto)) - writer.addheader('From', '%s <%s>'%(authname, self.ISSUE_TRACKER_EMAIL)) - writer.addheader('Reply-To', '%s <%s>'%(self.INSTANCE_NAME, - self.ISSUE_TRACKER_EMAIL)) + writer.addheader('From', '%s <%s>'%(authname, + self.db.config.ISSUE_TRACKER_EMAIL)) + writer.addheader('Reply-To', '%s <%s>'%(self.db.config.INSTANCE_NAME, + self.db.config.ISSUE_TRACKER_EMAIL)) writer.addheader('MIME-Version', '1.0') if messageid: writer.addheader('Message-Id', messageid) @@ -395,12 +392,12 @@ writer.addheader('In-Reply-To', inreplyto) # attach files - if files: + if message_files: part = writer.startmultipartbody('mixed') part = writer.nextpart() body = part.startbody('text/plain') body.write('\n'.join(m)) - for fileid in files: + for fileid in message_files: name = files.get(fileid, 'name') mime_type = files.get(fileid, 'type') content = files.get(fileid, 'content') @@ -431,13 +428,14 @@ # now try to send the message if SENDMAILDEBUG: open(SENDMAILDEBUG, 'w').write('FROM: %s\nTO: %s\n%s\n'%( - self.ADMIN_EMAIL, ', '.join(sendto), message.getvalue())) + self.db.config.ADMIN_EMAIL,', '.join(sendto),message.getvalue())) else: try: # send the message as admin so bounces are sent there # instead of to roundup - smtp = smtplib.SMTP(self.MAILHOST) - smtp.sendmail(self.ADMIN_EMAIL, sendto, message.getvalue()) + smtp = smtplib.SMTP(self.db.config.MAILHOST) + smtp.sendmail(self.db.config.ADMIN_EMAIL, sendto, + message.getvalue()) except socket.error, value: raise MessageSendError, \ "Couldn't send confirmation email: mailhost %s"%value @@ -448,11 +446,49 @@ def email_signature(self, nodeid, msgid): ''' Add a signature to the e-mail with some useful information ''' - web = self.ISSUE_TRACKER_WEB + 'issue'+ nodeid - email = '"%s" <%s>'%(self.INSTANCE_NAME, self.ISSUE_TRACKER_EMAIL) + web = self.db.config.ISSUE_TRACKER_WEB + 'issue'+ nodeid + email = '"%s" <%s>'%(self.db.config.INSTANCE_NAME, + self.db.config.ISSUE_TRACKER_EMAIL) line = '_' * max(len(web), len(email)) return '%s\n%s\n%s\n%s'%(line, email, web, line) + def generateCreateNote(self, nodeid): + """Generate a create note that lists initial property values + """ + cn = self.classname + cl = self.db.classes[cn] + props = cl.getprops(protected=0) + + # list the values + m = [] + l = props.items() + l.sort() + for propname, prop in l: + value = cl.get(nodeid, propname, None) + # skip boring entries + if not value: + continue + if isinstance(prop, hyperdb.Link): + link = self.db.classes[prop.classname] + if value: + key = link.labelprop(default_to_id=1) + if key: + value = link.get(value, key) + else: + value = '' + elif isinstance(prop, hyperdb.Multilink): + if value is None: value = [] + l = [] + link = self.db.classes[prop.classname] + key = link.labelprop(default_to_id=1) + if key: + value = [link.get(entry, key) for entry in value] + value = ', '.join(value) + m.append('%s: %s'%(propname, value)) + m.insert(0, '----------') + m.insert(0, '') + return '\n'.join(m) + def generateChangeNote(self, nodeid, oldvalues): """Generate a change note that lists property changes """ @@ -478,7 +514,9 @@ # list the changes m = [] - for propname, oldvalue in changed.items(): + l = changed.items() + l.sort() + for propname, oldvalue in l: prop = cl.properties[propname] value = cl.get(nodeid, propname, None) if isinstance(prop, hyperdb.Link): @@ -530,6 +568,45 @@ # # $Log: not supported by cvs2svn $ +# Revision 1.42 2002/01/21 09:55:14 rochecompaan +# Properties in change note are now sorted +# +# Revision 1.41 2002/01/15 00:12:40 richard +# #503340 ] creating issue with [asignedto=p.ohly] +# +# Revision 1.40 2002/01/14 22:21:38 richard +# #503353 ] setting properties in initial email +# +# Revision 1.39 2002/01/14 02:20:15 richard +# . changed all config accesses so they access either the instance or the +# config attriubute on the db. This means that all config is obtained from +# instance_config instead of the mish-mash of classes. This will make +# switching to a ConfigParser setup easier too, I hope. +# +# At a minimum, this makes migration a _little_ easier (a lot easier in the +# 0.5.0 switch, I hope!) +# +# Revision 1.38 2002/01/10 05:57:45 richard +# namespace clobberation +# +# Revision 1.37 2002/01/08 04:12:05 richard +# Changed message-id format to "<%s.%s.%s%s@%s>" so it complies with RFC822 +# +# Revision 1.36 2002/01/02 02:31:38 richard +# Sorry for the huge checkin message - I was only intending to implement #496356 +# but I found a number of places where things had been broken by transactions: +# . modified ROUNDUPDBSENDMAILDEBUG to be SENDMAILDEBUG and hold a filename +# for _all_ roundup-generated smtp messages to be sent to. +# . the transaction cache had broken the roundupdb.Class set() reactors +# . newly-created author users in the mailgw weren't being committed to the db +# +# Stuff that made it into CHANGES.txt (ie. the stuff I was actually working +# on when I found that stuff :): +# . #496356 ] Use threading in messages +# . detectors were being registered multiple times +# . added tests for mailgw +# . much better attaching of erroneous messages in the mail gateway +# # Revision 1.35 2001/12/20 15:43:01 rochecompaan # Features added: # . Multilink properties are now displayed as comma separated values in
--- a/roundup/templatebuilder.py Wed Feb 06 03:47:17 2002 +0000 +++ b/roundup/templatebuilder.py Wed Feb 06 04:05:55 2002 +0000 @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: templatebuilder.py,v 1.13 2001-11-22 15:46:42 jhermann Exp $ +# $Id: templatebuilder.py,v 1.13.2.1 2002-02-06 04:05:54 richard Exp $ import errno, re __doc__ = """ @@ -24,7 +24,7 @@ preamble = """ # Do Not Edit (Unless You Want To) -# This file automagically generated by roundup.htmldata.makeHtmlBase +# This file automagically generated by roundup.templatebuilder.makeHtmlBase # """ @@ -41,7 +41,7 @@ for file in filelist: # skip the backup files created by richard's vim if file[-1] == '~': continue - mangled_name = os.path.basename(re.sub(r'\.', 'DOT', file)) + mangled_name = os.path.basename(file).replace('.','DOT') fd.write('%s = """'%mangled_name) fd.write(re.sub(r'\$((Id|File|Log).*?)\$', r'dollar\1dollar', open(file).read(), re.I)) @@ -89,6 +89,12 @@ # # $Log: not supported by cvs2svn $ +# Revision 1.14 2002/02/05 09:59:05 grubert +# . makeHtmlBase: re.sub under python 2.2 did not replace '.', string.replace does it. +# +# Revision 1.13 2001/11/22 15:46:42 jhermann +# Added module docstrings to all modules. +# # Revision 1.12 2001/11/14 21:35:21 richard # . users may attach files to issues (and support in ext) through the web now #
--- a/roundup/templates/classic/.cvsignore Wed Feb 06 03:47:17 2002 +0000 +++ b/roundup/templates/classic/.cvsignore Wed Feb 06 04:05:55 2002 +0000 @@ -1,1 +1,2 @@ *.pyc +htmlbase.py
--- a/roundup/templates/classic/dbinit.py Wed Feb 06 03:47:17 2002 +0000 +++ b/roundup/templates/classic/dbinit.py Wed Feb 06 04:05:55 2002 +0000 @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: dbinit.py,v 1.13.2.1 2002-01-03 02:12:05 titus Exp $ +# $Id: dbinit.py,v 1.13.2.2 2002-02-06 04:05:54 richard Exp $ import os @@ -35,13 +35,7 @@ class IssueClass(roundupdb.IssueClass): ''' issues need the email information ''' - INSTANCE_NAME = instance_config.INSTANCE_NAME - ISSUE_TRACKER_WEB = instance_config.ISSUE_TRACKER_WEB - ISSUE_TRACKER_EMAIL = instance_config.ISSUE_TRACKER_EMAIL - ADMIN_EMAIL = instance_config.ADMIN_EMAIL - MAILHOST = instance_config.MAILHOST - MESSAGES_TO_AUTHOR = instance_config.MESSAGES_TO_AUTHOR - EMAIL_SIGNATURE_POSITION = instance_config.EMAIL_SIGNATURE_POSITION + pass def open(name=None): @@ -51,7 +45,7 @@ from roundup.hyperdb import String, Password, Date, Link, Multilink # open the database - db = Database(instance_config.get_default_database_dir(), name) + db = Database(instance_config, name) # Now initialise the schema. Must do this each time. pri = Class(db, "priority", @@ -128,6 +122,21 @@ # # $Log: not supported by cvs2svn $ +# Revision 1.14 2002/01/14 02:20:15 richard +# . changed all config accesses so they access either the instance or the +# config attriubute on the db. This means that all config is obtained from +# instance_config instead of the mish-mash of classes. This will make +# switching to a ConfigParser setup easier too, I hope. +# +# At a minimum, this makes migration a _little_ easier (a lot easier in the +# 0.5.0 switch, I hope!) +# +# +# Revision 1.13.2.1 2002/01/03 02:12:05 titus +# +# Initial ConfigParser implementation. +# +# # Revision 1.13 2002/01/02 02:31:38 richard # Sorry for the huge checkin message - I was only intending to implement #496356 # but I found a number of places where things had been broken by transactions:
--- a/roundup/templates/classic/detectors/nosyreaction.py Wed Feb 06 03:47:17 2002 +0000 +++ b/roundup/templates/classic/detectors/nosyreaction.py Wed Feb 06 04:05:55 2002 +0000 @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -#$Id: nosyreaction.py,v 1.9 2001-12-15 19:24:39 rochecompaan Exp $ +#$Id: nosyreaction.py,v 1.9.2.1 2002-02-06 04:05:54 richard Exp $ from roundup import roundupdb @@ -39,6 +39,7 @@ if oldvalues is None: # the action was a create, so use all the messages in the create messages = cl.get(nodeid, 'messages') + change_note = cl.generateCreateNote(nodeid) elif oldvalues.has_key('messages'): # the action was a set (so adding new messages to an existing issue) m = {} @@ -61,39 +62,30 @@ except roundupdb.MessageSendError, message: raise roundupdb.DetectorError, message - # update the nosy list with the recipients from the new messages - nosy = cl.get(nodeid, 'nosy') - n = {} - for nosyid in nosy: n[nosyid] = 1 - change = 0 - # but don't add admin or the anonymous user to the nosy list and - # don't add the author if he just removed himself - for msgid in messages: - authid = db.msg.get(msgid, 'author') - for recipid in db.msg.get(msgid, 'recipients'): - if recipid == '1': continue - if n.has_key(recipid): continue - if db.user.get(recipid, 'username') == 'anonymous': continue - if recipid == authid and not n.has_key(authid): continue - change = 1 - nosy.append(recipid) - if authid == '1': continue - if n.has_key(authid): continue - if db.user.get(authid, 'username') == 'anonymous': continue - change = 1 - # append the author only after issue creation - if oldvalues is None: - nosy.append(authid) - if change: - cl.set(nodeid, nosy=nosy) - - def init(db): db.issue.react('create', nosyreaction) db.issue.react('set', nosyreaction) # #$Log: not supported by cvs2svn $ +#Revision 1.11 2002/01/14 22:21:38 richard +##503353 ] setting properties in initial email +# +#Revision 1.10 2002/01/11 23:22:29 richard +# . #502437 ] rogue reactor and unittest +# in short, the nosy reactor was modifying the nosy list. That code had +# been there for a long time, and I suspsect it was there because we +# weren't generating the nosy list correctly in other places of the code. +# We're now doing that, so the nosy-modifying code can go away from the +# nosy reactor. +# +#Revision 1.9 2001/12/15 19:24:39 rochecompaan +# . Modified cgi interface to change properties only once all changes are +# collected, files created and messages generated. +# . Moved generation of change note to nosyreactors. +# . We now check for changes to "assignedto" to ensure it's added to the +# nosy list. +# #Revision 1.8 2001/12/05 14:26:44 rochecompaan #Removed generation of change note from "sendmessage" in roundupdb.py. #The change note is now generated when the message is created.
--- a/roundup/templates/classic/html/file.index Wed Feb 06 03:47:17 2002 +0000 +++ b/roundup/templates/classic/html/file.index Wed Feb 06 04:05:55 2002 +0000 @@ -1,7 +1,7 @@ -<!-- $Id: file.index,v 1.3 2001-10-21 11:42:15 richard Exp $--> +<!-- $Id: file.index,v 1.3.2.1 2002-02-06 04:05:54 richard Exp $--> <tr> <property name="name"> - <td><display call="link('name', is_download=1)"></td> + <td><display call="download('name')"></td> </property> <property name="type"> <td><display call="plain('type')"></td>
--- a/roundup/templates/classic/html/issue.item Wed Feb 06 03:47:17 2002 +0000 +++ b/roundup/templates/classic/html/issue.item Wed Feb 06 04:05:55 2002 +0000 @@ -16,7 +16,7 @@ <tr bgcolor="ffffea"> <td width=1% nowrap align=right><span class="form-label">Priority</span></td> - <td class="form-text"><display call="field('priority')"></td> + <td class="form-text"><display call="menu('priority')"></td> <td width=1% nowrap align=right><span class="form-label">Status</span></td> <td class="form-text"><display call="menu('status')"></td> </tr> @@ -30,7 +30,7 @@ <tr bgcolor="ffffea"> <td width=1% nowrap align=right><span class="form-label">Assigned To</span></td> - <td class="form-text"><display call="field('assignedto')"></td> + <td class="form-text"><display call="menu('assignedto')"></td> <td> </td> <td> </td> </tr>
--- a/roundup/templates/classic/htmlbase.py Wed Feb 06 03:47:17 2002 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,519 +0,0 @@ - -# Do Not Edit (Unless You Want To) -# This file automagically generated by roundup.htmldata.makeHtmlBase -# -fileDOTindex = """<!-- dollarId: file.index,v 1.3 2001/10/21 11:42:15 richard Exp dollar--> -<tr> - <property name="name"> - <td><display call="link('name', is_download=1)"></td> - </property> - <property name="type"> - <td><display call="plain('type')"></td> - </property> - <property name="creator"> - <td><display call="plain('creator')"></td> - </property> - <property name="creation"> - <td><display call="plain('creation')"></td> - </property> -</tr> -""" - -fileDOTnewitem = """<!-- dollarId: file.newitem,v 1.1 2001/07/30 08:12:17 richard Exp dollar--> -<table border=0 cellspacing=0 cellpadding=2> - -<tr class="strong-header"> - <td colspan=2>File upload details</td> -</td> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">File:</span></td> - <td class="form-text"><input type="file" name="content" size="40"></td> -</tr> - -<tr bgcolor="ffffea"> - <td> </td> - <td class="form-text"><display call="submit()"></td> -</tr> - -</table> -""" - -issueDOTfilter = """<!-- dollarId: issue.filter,v 1.2 2001/07/29 04:07:37 richard Exp dollar--> -<property name="title"> - <tr><th width="1%" align="right" class="location-bar">Title</th> - <td><display call="field('title')"></td></tr> -</property> -<property name="status"> - <tr><th width="1%" align="right" class="location-bar">Status</th> - <td><display call="checklist('status')"></td></tr> -</property> -<property name="priority"> - <tr><th width="1%" align="right" class="location-bar">Priority</th> - <td><display call="checklist('priority')"></td></tr> -</property> -""" - -issueDOTindex = """<!-- dollarId: issue.index,v 1.2 2001/07/29 04:07:37 richard Exp dollar--> -<tr class="row-<display call="plain('status')">"> - <property name="id"> - <td valign="top"><display call="plain('id')"></td> - </property> - <property name="activity"> - <td valign="top"><display call="reldate('activity', pretty=1)"></td> - </property> - <property name="priority"> - <td valign="top"><display call="plain('priority')"></td> - </property> - <property name="title"> - <td valign="top"><display call="link('title')"></td> - </property> - <property name="status"> - <td valign="top"><display call="plain('status')"></td> - </property> - <property name="assignedto"> - <td valign="top"><display call="link('assignedto')"></td> - </property> -</tr> -""" - -issueDOTitem = """<!-- dollarId: issue.item,v 1.4 2001/08/03 01:19:43 richard Exp dollar--> -<table border=0 cellspacing=0 cellpadding=2> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Title</span></td> - <td colspan=3 class="form-text"><display call="field('title', size=80)"></td> -</tr> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Created</span></td> - <td class="form-text"><display call="reldate('creation', pretty=1)"> - (<display call="plain('creator')">)</td> - <td width=1% nowrap align=right><span class="form-label">Last activity</span></td> - <td class="form-text"><display call="reldate('activity', pretty=1)"></td> -</tr> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Priority</span></td> - <td class="form-text"><display call="field('priority')"></td> - <td width=1% nowrap align=right><span class="form-label">Status</span></td> - <td class="form-text"><display call="menu('status')"></td> -</tr> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Superseder</span></td> - <td class="form-text"><display call="field('superseder', size=40, showid=1)"></td> - <td width=1% nowrap align=right><span class="form-label">Nosy List</span></td> - <td class="form-text"><display call="field('nosy')"></td> -</tr> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Assigned To</span></td> - <td class="form-text"><display call="field('assignedto')"></td> - <td> </td> - <td> </td> -</tr> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Change Note</span></td> - <td colspan=3 class="form-text"><display call="note()"></td> -</tr> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">File</span></td> - <td colspan=3 class="form-text"><input type="file" name="__file" size="80"></td> -</tr> - -<tr bgcolor="ffffea"> - <td> </td> - <td colspan=3 class="form-text"><display call="submit()"></td> -</tr> - -<tr class="msg-header"> - <td colspan=4><b>Messages</b></td> -</tr> -<property name="messages"> -<tr> - <td colspan=4><display call="list('messages')"></td> -</tr> -</property> - -<tr class="file-header"> - <td colspan=4><b>Files</b></td> -</tr> -<tr class="form-help"> - <td colspan=4> - <a href="newfile?:multilink=issue<display -call="plain('id')">:files">Attach a file to this issue</a> - </td> -</tr> -<property name="files"> - <tr> - <td colspan=4><display call="list('files')"></td> - </tr> -</property> - -<tr class="history-header"> - <td colspan=4><b>History</b></td> -</tr> -<tr> - <td colspan=4><display call="history()"></td> -</tr> - -</table> - -""" - -msgDOTindex = """<!-- dollarId: msg.index,v 1.2 2001/07/29 04:07:37 richard Exp dollar--> -<tr> - <property name="date"> - <td><display call="link('date')"></td> - </property> - <property name="author"> - <td><display call="plain('author')"></td> - </property> - <property name="summary"> - <td><display call="plain('summary')"></td> - </property> -</tr> -""" - -msgDOTitem = """<!-- dollarId: msg.item,v 1.2 2001/07/29 04:07:37 richard Exp dollar--> -<table border=0 cellspacing=0 cellpadding=2> - -<tr class="strong-header"> - <td colspan=2>Message Information</td> -</td> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Author</span></td> - <td class="form-text"><display call="plain('author')"></td> -</tr> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Recipients</span></td> - <td class="form-text"><display call="plain('recipients')"></td> -</tr> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Date</span></td> - <td class="form-text"><display call="plain('date')"></td> -</tr> - -<tr bgcolor="ffeaff"> - <td colspan=2 class="form-text"> - <pre><display call="plain('content')"></pre> - </td> -</tr> - -<property name="files"> -<tr class="strong-header"><td colspan=2><b>Files</b></td></tr> -<tr><td colspan=2><display call="list('files')"></td></tr> -</property> - -<tr class="strong-header"><td colspan=2><b>History</b></td><tr> -<tr><td colspan=2><display call="history()"></td></tr> - -</table> -""" - -styleDOTcss = """h1 { - font-family: Verdana, Helvetica, sans-serif; - font-size: 18pt; - font-weight: bold; -} - -h2 { - font-family: Verdana, Helvetica, sans-serif; - font-size: 16pt; - font-weight: bold; -} - -h3 { - font-family: Verdana, Helvetica, sans-serif; - font-size: 12pt; - font-weight: bold; -} - -a:hover { - font-family: Verdana, Helvetica, sans-serif; - text-decoration: underline; - color: #333333; -} - -a:link { - font-family: Verdana, Helvetica, sans-serif; - text-decoration: none; - color: #000099; -} - -a { - font-family: Verdana, Helvetica, sans-serif; - text-decoration: none; - color: #000099; -} - -p { - font-family: Verdana, Helvetica, sans-serif; - font-size: 10pt; - color: #333333; -} - -th { - font-family: Verdana, Helvetica, sans-serif; - font-weight: bold; - font-size: 10pt; - color: #333333; -} - -.form-help { - font-family: Verdana, Helvetica, sans-serif; - font-size: 10pt; - color: #333333; -} - -.std-text { - font-family: Verdana, Helvetica, sans-serif; - font-size: 10pt; - color: #333333; -} - -.tab-small { - font-family: Verdana, Helvetica, sans-serif; - font-size: 8pt; - color: #333333; -} - -.location-bar { - background-color: #44bb66; - color: #ffffff; - border: none; -} - -.strong-header { - font-family: Verdana, Helvetica, sans-serif; - font-size: 12pt; - font-weight: bold; - background-color: #000000; - color: #ffffff; -} - -.msg-header { - font-family: Verdana, Helvetica, sans-serif; - font-size: 12pt; - font-weight: bold; - background-color: #EE71AC; - color: #ffffff; -} - -.file-header { - font-family: Verdana, Helvetica, sans-serif; - font-size: 12pt; - font-weight: bold; - background-color: #41BE62; - color: #ffffff; -} - -.history-header { - font-family: Verdana, Helvetica, sans-serif; - font-size: 12pt; - font-weight: bold; - background-color: #739DEE; - color: #ffffff; -} - -.list-header { - background-color: #aaccff; - color: #000000; - border: none; -} - -.list-item { - font-family: Verdana, Helvetica, sans-serif; - font-size: 10pt; -} - -.list-nav { - font-family: Verdana, Helvetica, sans-serif; - font-size: 10pt; - font-weight: bold; -} - -.row-normal { - background-color: #ffffff; - border: none; - -} - -.row-hilite { - background-color: #efefef; - border: none; -} - -.row-unread { - background-color: #ffddd9; - border: none; -} - -.row-in-progress { - background-color: #3ccc50; - border: none; -} - -.row-resolved { - background-color: #aaccff; - border: none; -} - -.row-done-cbb { - background-color: #aaccff; - border: none; -} - -.row-testing { - background-color: #c6ddff; - border: none; -} - -.row-need-eg { - background-color: #ffc7c0; - border: none; -} - -.row-chatting { - background-color: #ffe3c0; - border: none; -} - -.row-deferred { - background-color: #cccccc; - border: none; -} - -.section-bar { - background-color: #707070; - color: #ffffff; - border: 1px solid #404040; -} - -.system-msg { - font-family: Verdana, Helvetica, sans-serif; - font-size: 10pt; - background-color: #ffffff; - border: 1px solid #000000; - margin-bottom: 6px; - margin-top: 6px; - padding: 4px; - width: 100%; - color: #660033; -} - -.form-title { - font-family: Verdana, Helvetica, sans-serif; - font-weight: bold; - font-size: 12pt; - color: #333333; -} - -.form-label { - font-family: Verdana, Helvetica, sans-serif; - font-weight: bold; - font-size: 10pt; - color: #333333; -} - -.form-optional { - font-family: Verdana, Helvetica, sans-serif; - font-weight: bold; - font-style: italic; - font-size: 10pt; - color: #333333; -} - -.form-element { - font-family: Verdana, Helvetica, aans-serif; - font-size: 10pt; - color: #000000; -} - -.form-text { - font-family: Verdana, Helvetica, sans-serif; - font-size: 10pt; - color: #333333; -} - -.form-mono { - font-family: monospace; - font-size: 12px; - text-decoration: none; -} -""" - -userDOTindex = """<!-- dollarId: user.index,v 1.2 2001/07/29 04:07:37 richard Exp dollar--> -<tr> - <property name="username"> - <td><display call="link('username')"></td> - </property> - <property name="realname"> - <td><display call="plain('realname')"></td> - </property> - <property name="organisation"> - <td><display call="plain('organisation')"></td> - </property> - <property name="address"> - <td><display call="plain('address')"></td> - </property> - <property name="phone"> - <td><display call="plain('phone')"></td> - </property> -</tr> -""" - -userDOTitem = """<!-- dollarId: user.item,v 1.2 2001/07/29 04:07:37 richard Exp dollar--> -<table border=0 cellspacing=0 cellpadding=2> - -<tr class="strong-header"> - <td colspan=2>Your Details</td> -</td> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Name</span></td> - <td class="form-text"><display call="field('realname', size=40)"></td> -</tr> -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Login Name</span></td> - <td class="form-text"><display call="field('username', size=40)"></td> -</tr> -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Login Password</span></td> - <td class="form-text"><display call="field('password', size=10)"></td> -</tr> -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Phone</span></td> - <td class="form-text"><display call="field('phone', size=40)"></td> -</tr> -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Organisation</span></td> - <td class="form-text"><display call="field('organisation', size=40)"></td> -</tr> -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">E-mail address</span></td> - <td class="form-text"><display call="field('address', size=40)"></td> -</tr> - -<tr bgcolor="ffffea"> - <td> </td> - <td class="form-text"><display call="submit()"></td> -</tr> - -<tr class="strong-header"> - <td colspan=2><b>History</b></td> -</tr> -<tr> - <td colspan=2><display call="history()"></td> -</tr> - -</table> - -""" -
--- a/roundup/templates/classic/interfaces.py Wed Feb 06 03:47:17 2002 +0000 +++ b/roundup/templates/classic/interfaces.py Wed Feb 06 04:05:55 2002 +0000 @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: interfaces.py,v 1.11 2002-01-02 02:32:38 richard Exp $ +# $Id: interfaces.py,v 1.11.2.1 2002-02-06 04:05:54 richard Exp $ import instance_config from roundup import cgi_client, mailgw @@ -24,24 +24,28 @@ ''' derives basic CGI implementation from the standard module, with any specific extensions ''' - INSTANCE_NAME = instance_config.INSTANCE_NAME - TEMPLATES = instance_config.TEMPLATES - FILTER_POSITION = instance_config.FILTER_POSITION - ANONYMOUS_ACCESS = instance_config.ANONYMOUS_ACCESS - ANONYMOUS_REGISTER = instance_config.ANONYMOUS_REGISTER + pass class MailGW(mailgw.MailGW): ''' derives basic mail gateway implementation from the standard module, with any specific extensions ''' - INSTANCE_NAME = instance_config.INSTANCE_NAME - ISSUE_TRACKER_EMAIL = instance_config.ISSUE_TRACKER_EMAIL - ADMIN_EMAIL = instance_config.ADMIN_EMAIL - MAILHOST = instance_config.MAILHOST - ANONYMOUS_REGISTER = instance_config.ANONYMOUS_REGISTER + pass # # $Log: not supported by cvs2svn $ +# Revision 1.12 2002/01/14 02:20:15 richard +# . changed all config accesses so they access either the instance or the +# config attriubute on the db. This means that all config is obtained from +# instance_config instead of the mish-mash of classes. This will make +# switching to a ConfigParser setup easier too, I hope. +# +# At a minimum, this makes migration a _little_ easier (a lot easier in the +# 0.5.0 switch, I hope!) +# +# Revision 1.11 2002/01/02 02:32:38 richard +# ANONYMOUS_ACCESS -> ANONYMOUS_REGISTER +# # Revision 1.10 2001/12/20 15:43:01 rochecompaan # Features added: # . Multilink properties are now displayed as comma separated values in
--- a/roundup/templates/extended/.cvsignore Wed Feb 06 03:47:17 2002 +0000 +++ b/roundup/templates/extended/.cvsignore Wed Feb 06 04:05:55 2002 +0000 @@ -1,1 +1,2 @@ *.pyc +htmlbase.py
--- a/roundup/templates/extended/dbinit.py Wed Feb 06 03:47:17 2002 +0000 +++ b/roundup/templates/extended/dbinit.py Wed Feb 06 04:05:55 2002 +0000 @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: dbinit.py,v 1.18 2002-01-02 02:31:38 richard Exp $ +# $Id: dbinit.py,v 1.18.2.1 2002-02-06 04:05:54 richard Exp $ import os @@ -35,13 +35,7 @@ class IssueClass(roundupdb.IssueClass): ''' issues need the email information ''' - INSTANCE_NAME = instance_config.INSTANCE_NAME - ISSUE_TRACKER_WEB = instance_config.ISSUE_TRACKER_WEB - ISSUE_TRACKER_EMAIL = instance_config.ISSUE_TRACKER_EMAIL - ADMIN_EMAIL = instance_config.ADMIN_EMAIL - MAILHOST = instance_config.MAILHOST - MESSAGES_TO_AUTHOR = instance_config.MESSAGES_TO_AUTHOR - EMAIL_SIGNATURE_POSITION = instance_config.EMAIL_SIGNATURE_POSITION + pass def open(name=None): @@ -51,7 +45,7 @@ from roundup.hyperdb import String, Password, Date, Link, Multilink # open the database - db = Database(instance_config.DATABASE, name) + db = Database(instance_config, name) # Now initialise the schema. Must do this each time. pri = Class(db, "priority", @@ -179,6 +173,30 @@ # # $Log: not supported by cvs2svn $ +# Revision 1.19 2002/01/14 02:20:15 richard +# . changed all config accesses so they access either the instance or the +# config attriubute on the db. This means that all config is obtained from +# instance_config instead of the mish-mash of classes. This will make +# switching to a ConfigParser setup easier too, I hope. +# +# At a minimum, this makes migration a _little_ easier (a lot easier in the +# 0.5.0 switch, I hope!) +# +# Revision 1.18 2002/01/02 02:31:38 richard +# Sorry for the huge checkin message - I was only intending to implement #496356 +# but I found a number of places where things had been broken by transactions: +# . modified ROUNDUPDBSENDMAILDEBUG to be SENDMAILDEBUG and hold a filename +# for _all_ roundup-generated smtp messages to be sent to. +# . the transaction cache had broken the roundupdb.Class set() reactors +# . newly-created author users in the mailgw weren't being committed to the db +# +# Stuff that made it into CHANGES.txt (ie. the stuff I was actually working +# on when I found that stuff :): +# . #496356 ] Use threading in messages +# . detectors were being registered multiple times +# . added tests for mailgw +# . much better attaching of erroneous messages in the mail gateway +# # Revision 1.17 2001/12/02 05:06:16 richard # . We now use weakrefs in the Classes to keep the database reference, so # the close() method on the database is no longer needed.
--- a/roundup/templates/extended/detectors/nosyreaction.py Wed Feb 06 03:47:17 2002 +0000 +++ b/roundup/templates/extended/detectors/nosyreaction.py Wed Feb 06 04:05:55 2002 +0000 @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -#$Id: nosyreaction.py,v 1.9 2001-12-15 19:24:39 rochecompaan Exp $ +#$Id: nosyreaction.py,v 1.9.2.1 2002-02-06 04:05:54 richard Exp $ from roundup import roundupdb @@ -39,6 +39,7 @@ if oldvalues is None: # the action was a create, so use all the messages in the create messages = cl.get(nodeid, 'messages') + change_note = cl.generateCreateNote(nodeid) elif oldvalues.has_key('messages'): # the action was a set (so adding new messages to an existing issue) m = {} @@ -61,32 +62,6 @@ except roundupdb.MessageSendError, message: raise roundupdb.DetectorError, message - # update the nosy list with the recipients from the new messages - nosy = cl.get(nodeid, 'nosy') - n = {} - for nosyid in nosy: n[nosyid] = 1 - change = 0 - # but don't add admin or the anonymous user to the nosy list and - # don't add the author if he just removed himself - for msgid in messages: - authid = db.msg.get(msgid, 'author') - for recipid in db.msg.get(msgid, 'recipients'): - if recipid == '1': continue - if n.has_key(recipid): continue - if db.user.get(recipid, 'username') == 'anonymous': continue - if recipid == authid and not n.has_key(authid): continue - change = 1 - nosy.append(recipid) - if authid == '1': continue - if n.has_key(authid): continue - if db.user.get(authid, 'username') == 'anonymous': continue - change = 1 - # append the author only after issue creation - if oldvalues is None: - nosy.append(authid) - if change: - cl.set(nodeid, nosy=nosy) - def init(db): db.issue.react('create', nosyreaction) @@ -94,6 +69,24 @@ # #$Log: not supported by cvs2svn $ +#Revision 1.11 2002/01/14 22:21:38 richard +##503353 ] setting properties in initial email +# +#Revision 1.10 2002/01/11 23:22:29 richard +# . #502437 ] rogue reactor and unittest +# in short, the nosy reactor was modifying the nosy list. That code had +# been there for a long time, and I suspsect it was there because we +# weren't generating the nosy list correctly in other places of the code. +# We're now doing that, so the nosy-modifying code can go away from the +# nosy reactor. +# +#Revision 1.9 2001/12/15 19:24:39 rochecompaan +# . Modified cgi interface to change properties only once all changes are +# collected, files created and messages generated. +# . Moved generation of change note to nosyreactors. +# . We now check for changes to "assignedto" to ensure it's added to the +# nosy list. +# #Revision 1.8 2001/12/05 14:26:44 rochecompaan #Removed generation of change note from "sendmessage" in roundupdb.py. #The change note is now generated when the message is created.
--- a/roundup/templates/extended/html/file.index Wed Feb 06 03:47:17 2002 +0000 +++ b/roundup/templates/extended/html/file.index Wed Feb 06 04:05:55 2002 +0000 @@ -1,7 +1,7 @@ -<!-- $Id: file.index,v 1.2 2001-10-21 11:42:15 richard Exp $--> +<!-- $Id: file.index,v 1.2.2.1 2002-02-06 04:05:54 richard Exp $--> <tr> <property name="name"> - <td><display call="link('name', is_download=1)"></td> + <td><display call="download('name')"></td> </property> <property name="type"> <td><display call="plain('type')"></td>
--- a/roundup/templates/extended/html/issue.item Wed Feb 06 03:47:17 2002 +0000 +++ b/roundup/templates/extended/html/issue.item Wed Feb 06 04:05:55 2002 +0000 @@ -1,4 +1,4 @@ -<!-- $Id: issue.item,v 1.8 2001-12-18 05:05:14 richard Exp $--> +<!-- $Id: issue.item,v 1.8.2.1 2002-02-06 04:05:54 richard Exp $--> <table border=0 cellspacing=0 cellpadding=2> <tr class="strong-header"> @@ -36,14 +36,14 @@ <tr bgcolor="ffffea"> <td width=1% nowrap align=right><span class="form-label">Priority</span></td> - <td class="form-text"><display call="field('priority')"></td> + <td class="form-text"><display call="menu('priority')"></td> <td width=1% nowrap align=right><span class="form-label">Status</span></td> <td class="form-text"><display call="menu('status')"></td> </tr> <tr bgcolor="ffffea"> <td width=1% nowrap align=right><span class="form-label">Assigned to</span></td> - <td class="form-text"><display call="field('assignedto')"></td> + <td class="form-text"><display call="menu('assignedto')"></td> <td width=1% nowrap align=right><span class="form-label">Nosy List</span></td> <td class="form-text"><display call="field('nosy')"></td> </tr>
--- a/roundup/templates/extended/html/support.item Wed Feb 06 03:47:17 2002 +0000 +++ b/roundup/templates/extended/html/support.item Wed Feb 06 04:05:55 2002 +0000 @@ -1,4 +1,4 @@ -<!-- $Id: support.item,v 1.3 2001-11-14 21:35:22 richard Exp $--> +<!-- $Id: support.item,v 1.3.2.1 2002-02-06 04:05:54 richard Exp $--> <table border=0 cellspacing=0 cellpadding=2> <tr class="strong-header"> @@ -30,19 +30,19 @@ <td width=1% nowrap align=right><span class="form-label">Empty</span></td> <td class="form-text">XXXX</td> <td width=1% nowrap align=right><span class="form-label">Source</span></td> - <td class="form-text"><display call="field('source')"></td> + <td class="form-text"><display call="menu('source')"></td> </tr> <tr bgcolor="ffffea"> <td width=1% nowrap align=right><span class="form-label">Status</span></td> <td class="form-text"><display call="menu('status')"></td> <td width=1% nowrap align=right><span class="form-label">Rate</span></td> - <td class="form-text"><display call="field('rate')"></td> + <td class="form-text"><display call="menu('rate')"></td> </tr> <tr bgcolor="ffffea"> <td width=1% nowrap align=right><span class="form-label">Assigned To</span></td> - <td class="form-text"><display call="field('assignedto')"></td> + <td class="form-text"><display call="menu('assignedto')"></td> <td width=1% nowrap align=right><span class="form-label">Customer Name</span></td> <td class="form-text"><display call="field('customername')"></td> </tr>
--- a/roundup/templates/extended/html/timelog.item Wed Feb 06 03:47:17 2002 +0000 +++ b/roundup/templates/extended/html/timelog.item Wed Feb 06 04:05:55 2002 +0000 @@ -1,4 +1,4 @@ -<!-- $Id: timelog.item,v 1.1 2001-07-30 08:04:26 richard Exp $--> +<!-- $Id: timelog.item,v 1.1.2.1 2002-02-06 04:05:54 richard Exp $--> <table border=0 cellspacing=0 cellpadding=2> <tr class="strong-header"> @@ -19,7 +19,7 @@ </tr> <tr bgcolor="ffffea"> <td width=1% nowrap align=right><span class="form-label">Performed by</span></td> - <td class="form-text"><display call="field('performedby', size=40)"></td> + <td class="form-text"><display call="menu('performedby', size=40)"></td> </tr> <tr bgcolor="ffffea">
--- a/roundup/templates/extended/htmlbase.py Wed Feb 06 03:47:17 2002 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,728 +0,0 @@ - -# Do Not Edit (Unless You Want To) -# This file automagically generated by roundup.htmldata.makeHtmlBase -# -fileDOTindex = """<!-- dollarId: file.index,v 1.2 2001/10/21 11:42:15 richard Exp dollar--> -<tr> - <property name="name"> - <td><display call="link('name', is_download=1)"></td> - </property> - <property name="type"> - <td><display call="plain('type')"></td> - </property> - <property name="creator"> - <td><display call="plain('creator')"></td> - </property> - <property name="creation"> - <td><display call="plain('creation')"></td> - </property> -</tr> -""" - -fileDOTnewitem = """<!-- dollarId: file.newitem,v 1.1 2001/07/30 08:04:26 richard Exp dollar--> -<table border=0 cellspacing=0 cellpadding=2> - -<tr class="strong-header"> - <td colspan=2>File upload details</td> -</td> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">File:</span></td> - <td class="form-text"><input type="file" name="content" size="40"></td> -</tr> - -<tr bgcolor="ffffea"> - <td> </td> - <td class="form-text"><display call="submit()"></td> -</tr> - -</table> -""" - -issueDOTfilter = """<!-- dollarId: issue.filter,v 1.2 2001/07/30 01:26:59 richard Exp dollar--> -<property name="title"> - <tr><th width="1%" align="right" class="location-bar">Title</th> - <td><display call="field('title')"></td></tr> -</property> -<property name="status"> - <tr><th width="1%" align="right" class="location-bar">Status</th> - <td><display call="checklist('status')"></td></tr> -</property> -<property name="priority"> - <tr><th width="1%" align="right" class="location-bar">Priority</th> - <td><display call="checklist('priority')"></td></tr> -</property> -<property name="platform"> - <tr><th width="1%" align="right" class="location-bar">Platform</th> - <td><display call="checklist('platform')"></td></tr> -</property> -<property name="product"> - <tr><th width="1%" align="right" class="location-bar">Product</th> - <td><display call="checklist('product')"></td></tr> -</property> -<property name="version"> - <tr><th width="1%" align="right" class="location-bar">Version</th> - <td><display call="field('version')"></td></tr> -</property> -<property name="assignedto"> - <tr><th width="1%" align="right" class="location-bar">Assigned to</th> - <td><display call="checklist('assignedto')"></td></tr> -</property> -""" - -issueDOTindex = """<!-- dollarId: issue.index,v 1.3 2001/08/01 05:15:09 richard Exp dollar--> -<tr> - <property name="id"> - <td valign="top"><display call="plain('id')"></td> - </property> - <property name="activity"> - <td valign="top"><display call="reldate('activity', pretty=1)"></td> - </property> - <property name="priority"> - <td valign="top"><display call="plain('priority')"></td> - </property> - <property name="status"> - <td valign="top"><display call="plain('status')"></td> - </property> - <property name="title"> - <td valign="top"><display call="link('title')"></td> - </property> - <property name="platform"> - <td valign="top"><display call="plain('platform')"></td> - </property> - <property name="product"> - <td valign="top"><display call="plain('product')"></td> - </property> - <property name="version"> - <td valign="top"><display call="plain('version')"></td> - </property> - <property name="assignedto"> - <td valign="top"><display call="plain('assignedto')"></td> - </property> -</tr> -""" - -issueDOTitem = """<!-- dollarId: issue.item,v 1.7 2001/11/21 02:34:18 richard Exp dollar--> -<table border=0 cellspacing=0 cellpadding=2> - -<tr class="strong-header"> - <td colspan=4>Item Information</td> -</td> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Title</span></td> - <td colspan=3 class="form-text"><display call="field('title', size=80)"></td> -</tr> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Product</span></td> - <td class="form-text" valign=middle><display call="menu('product')"> - <span class="form-label">version:</span><display call="field('version', 5)"></td> - <td rowspan=2 width=1% nowrap align=right><span class="form-label">Platform</span></td> - <td rowspan=2 class="form-text" valign=middle><display call="checklist('platform')"></td> -</tr> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right> </td> - <td align=left><span class="form-label">Target Version</span> - <display call="field('targetversion', 5)"> - </td> -</tr> - - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Created</span></td> - <td class="form-text"><display call="reldate('creation', pretty=1)"> - (<display call="plain('creator')">)</td> - <td width=1% nowrap align=right><span class="form-label">Last activity</span></td> - <td class="form-text"><display call="reldate('activity', pretty=1)"></td> -</tr> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Priority</span></td> - <td class="form-text"><display call="field('priority')"></td> - <td width=1% nowrap align=right><span class="form-label">Status</span></td> - <td class="form-text"><display call="menu('status')"></td> -</tr> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Assigned to</span></td> - <td class="form-text"><display call="field('assignedto')"></td> - <td width=1% nowrap align=right><span class="form-label">Nosy List</span></td> - <td class="form-text"><display call="field('nosy')"></td> -</tr> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Superseder</span></td> - <td class="form-text"><display call="field('superseder', size=40, showid=1)"></td> - <td width=1% nowrap align=right><span class="form-label">Support call</span></td> - <td class="form-text"><display call="field('supportcall', size=40, showid=1)"></td> -</tr> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Change Note</span></td> - <td colspan=3 class="form-text"><display call="note()"></td> -</tr> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">File</span></td> - <td colspan=3 class="form-text"><input type="file" name="__file" size="80"></td> -</tr> - -<tr bgcolor="ffffea"> - <td> </td> - <td colspan=3 class="form-text"><display call="submit()"></td> -</tr> - -<tr class="strong-header"> - <td colspan=4><b>Messages</b></td> -</tr> -<property name="messages"> -<tr> - <td colspan=4><display call="list('messages')"></td> -</tr> -</property> - -<tr class="strong-header"> - <td colspan=4><b>Files</b></td> -</tr> -<tr class="form-help"> - <td colspan=4> - <a href="newfile?:multilink=issue<display call="plain('id')">:files">Attach a file to this issue</a> - </td> -</tr> -<property name="files"> - <tr> - <td colspan=4><display call="list('files')"></td> - </tr> -</property> - -</table> - -""" - -msgDOTindex = """<!-- dollarId: msg.index,v 1.3 2001/09/27 06:45:58 richard Exp dollar--> -<tr class="row-hilite"> - <property name="date"> - <td><display call="link('date')"></td> - </property> - <property name="author"> - <td><display call="plain('author')"></td> - </property> -</tr> -<tr bgcolor="ffeaff"> - <td colspan=2><pre><display call="plain('content', escape=1)"></pre></td> -</tr> -""" - -msgDOTitem = """<!-- dollarId: msg.item,v 1.1 2001/07/23 04:21:20 richard Exp dollar--> -<table border=0 cellspacing=0 cellpadding=2> - -<tr class="strong-header"> - <td colspan=2>Message Information</td> -</td> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Author</span></td> - <td class="form-text"><display call="plain('author')"></td> -</tr> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Recipients</span></td> - <td class="form-text"><display call="plain('recipients')"></td> -</tr> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Date</span></td> - <td class="form-text"><display call="plain('date')"></td> -</tr> - -<tr bgcolor="ffeaff"> - <td colspan=2 class="form-text"> - <pre><display call="plain('content')"></pre> - </td> -</tr> - -<property name="files"> -<tr class="strong-header"><td colspan=2><b>Files</b></td></tr> -<tr><td colspan=2><display call="list('files')"></td></tr> -</property> - -<tr class="strong-header"><td colspan=2><b>History</b></td><tr> -<tr><td colspan=2><display call="history()"></td></tr> - -</table> -""" - -styleDOTcss = """h1 { - font-family: Verdana, Helvetica, sans-serif; - font-size: 18pt; - font-weight: bold; -} - -h2 { - font-family: Verdana, Helvetica, sans-serif; - font-size: 16pt; - font-weight: bold; -} - -h3 { - font-family: Verdana, Helvetica, sans-serif; - font-size: 12pt; - font-weight: bold; -} - -a:hover { - font-family: Verdana, Helvetica, sans-serif; - text-decoration: underline; - color: #333333; -} - -a:link { - font-family: Verdana, Helvetica, sans-serif; - text-decoration: none; - color: #000099; -} - -a { - font-family: Verdana, Helvetica, sans-serif; - text-decoration: none; - color: #000099; -} - -p { - font-family: Verdana, Helvetica, sans-serif; - font-size: 10pt; - color: #333333; -} - -th { - font-family: Verdana, Helvetica, sans-serif; - font-weight: bold; - font-size: 10pt; - color: #333333; -} - -.form-help { - font-family: Verdana, Helvetica, sans-serif; - font-size: 10pt; - color: #333333; -} - -.std-text { - font-family: Verdana, Helvetica, sans-serif; - font-size: 10pt; - color: #333333; -} - -.tab-small { - font-family: Verdana, Helvetica, sans-serif; - font-size: 8pt; - color: #333333; -} - -.location-bar { - background-color: #efefef; - border: none; -} - -.strong-header { - font-family: Verdana, Helvetica, sans-serif; - font-size: 12pt; - font-weight: bold; - background-color: #000000; - color: #ffffff; -} - -.list-header { - background-color: #c0c0c0; - border: none; -} - -.list-item { - font-family: Verdana, Helvetica, sans-serif; - font-size: 10pt; -} - -.list-nav { - font-family: Verdana, Helvetica, sans-serif; - font-size: 10pt; - font-weight: bold; -} - -.row-normal { - background-color: #ffffff; - border: none; - -} - -.row-hilite { - background-color: #efefef; - border: none; -} - -.section-bar { - background-color: #c0c0c0; - border: none; -} - -.system-msg { - font-family: Verdana, Helvetica, sans-serif; - font-size: 10pt; - background-color: #ffffff; - border: 1px solid #000000; - margin-bottom: 6px; - margin-top: 6px; - padding: 4px; - width: 100%; - color: #660033; -} - -.form-title { - font-family: Verdana, Helvetica, sans-serif; - font-weight: bold; - font-size: 12pt; - color: #333333; -} - -.form-label { - font-family: Verdana, Helvetica, sans-serif; - font-weight: bold; - font-size: 10pt; - color: #333333; -} - -.form-optional { - font-family: Verdana, Helvetica, sans-serif; - font-weight: bold; - font-style: italic; - font-size: 10pt; - color: #333333; -} - -.form-element { - font-family: Verdana, Helvetica, aans-serif; - font-size: 10pt; - color: #000000; -} - -.form-text { - font-family: Verdana, Helvetica, sans-serif; - font-size: 10pt; - color: #333333; -} - -.form-mono { - font-family: monospace; - font-size: 12px; - text-decoration: none; -} -""" - -supportDOTfilter = """<!-- dollarId: support.filter,v 1.1 2001/07/30 01:27:28 richard Exp dollar--> -<property name="title"> - <tr><th width="1%" align="right" class="location-bar">Title</th> - <td><display call="field('title')"></td></tr> -</property> -<property name="status"> - <tr><th width="1%" align="right" class="location-bar">Status</th> - <td><display call="checklist('status')"></td></tr> -</property> -<property name="platform"> - <tr><th width="1%" align="right" class="location-bar">Platform</th> - <td><display call="checklist('platform')"></td></tr> -</property> -<property name="product"> - <tr><th width="1%" align="right" class="location-bar">Product</th> - <td><display call="checklist('product')"></td></tr> -</property> -<property name="version"> - <tr><th width="1%" align="right" class="location-bar">Version</th> - <td><display call="field('version')"></td></tr> -</property> -<property name="source"> - <tr><th width="1%" align="right" class="location-bar">Source</th> - <td><display call="checklist('source')"></td></tr> -</property> -<property name="assignedto"> - <tr><th width="1%" align="right" class="location-bar">Assigned to</th> - <td><display call="checklist('assignedto')"></td></tr> -</property> -<property name="customername"> - <tr><th width="1%" align="right" class="location-bar">Customer name</th> - <td><display call="field('customername')"></td></tr> -</property> -""" - -supportDOTindex = """<!-- dollarId: support.index,v 1.2 2001/08/01 05:15:09 richard Exp dollar--> -<tr> - <property name="id"> - <td valign="top"><display call="plain('id')"></td> - </property> - <property name="activity"> - <td valign="top"><display call="reldate('activity', pretty=1)"></td> - </property> - <property name="status"> - <td valign="top"><display call="plain('status')"></td> - </property> - <property name="title"> - <td valign="top"><display call="link('title')"></td> - </property> - <property name="platform"> - <td valign="top"><display call="plain('platform')"></td> - </property> - <property name="product"> - <td valign="top"><display call="plain('product')"></td> - </property> - <property name="version"> - <td valign="top"><display call="plain('version')"></td> - </property> - <property name="source"> - <td valign="top"><display call="plain('source')"></td> - </property> - <property name="assignedto"> - <td valign="top"><display call="plain('assignedto')"></td> - </property> - <property name="customername"> - <td valign="top"><display call="plain('customername')"></td> - </property> -</tr> -""" - -supportDOTitem = """<!-- dollarId: support.item,v 1.3 2001/11/14 21:35:22 richard Exp dollar--> -<table border=0 cellspacing=0 cellpadding=2> - -<tr class="strong-header"> - <td colspan=4>Item Information</td> -</td> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Title</span></td> - <td colspan=3 class="form-text"><display call="field('title', size=80)"></td> -</tr> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Product</span></td> - <td class="form-text" valign=middle><display call="menu('product')"> - version:<display call="field('version', 5)"></td> - <td width=1% nowrap align=right><span class="form-label">Platform</span></td> - <td class="form-text" valign=middle><display call="checklist('platform')"></td> -</tr> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Created</span></td> - <td class="form-text"><display call="reldate('creation', pretty=1)"> - (<display call="plain('creator')">)</td> - <td width=1% nowrap align=right><span class="form-label">Last activity</span></td> - <td class="form-text"><display call="reldate('activity', pretty=1)"></td> -</tr> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Empty</span></td> - <td class="form-text">XXXX</td> - <td width=1% nowrap align=right><span class="form-label">Source</span></td> - <td class="form-text"><display call="field('source')"></td> -</tr> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Status</span></td> - <td class="form-text"><display call="menu('status')"></td> - <td width=1% nowrap align=right><span class="form-label">Rate</span></td> - <td class="form-text"><display call="field('rate')"></td> -</tr> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Assigned To</span></td> - <td class="form-text"><display call="field('assignedto')"></td> - <td width=1% nowrap align=right><span class="form-label">Customer Name</span></td> - <td class="form-text"><display call="field('customername')"></td> -</tr> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Superseder</span></td> - <td class="form-text"><display call="field('superseder', size=40, showid=1)"></td> - <td width=1% nowrap align=right><span class="form-label">Nosy List</span></td> - <td class="form-text"><display call="field('nosy')"></td> -</tr> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Change Note</span></td> - <td colspan=3 class="form-text"><display call="note()"></td> -</tr> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">File</span></td> - <td colspan=3 class="form-text"><input type="file" name="__file" size="80"></td> -</tr> - -<tr bgcolor="ffffea"> - <td> </td> - <td colspan=3 class="form-text"><display call="submit()"></td> -</tr> - -<tr class="strong-header"> - <td colspan=4><b>Messages</b></td> -</tr> -<property name="messages"> -<tr> - <td colspan=4><display call="list('messages')"></td> -</tr> -</property> - -<tr class="strong-header"> - <td colspan=4><b>Timelog</b></td> -</tr> -<tr class="form-help"> - <td colspan=4> - <a href="newtimelog?:multilink=support<display call="plain('id')">:timelog">Log time against this support call</a> - </td> -</tr> -<property name="timelog"> - <tr> - <td colspan=4><display call="list('timelog')"></td> - </tr> -</property> - -<tr class="strong-header"> - <td colspan=4><b>Files</b></td> -</tr> -<tr class="form-help"> - <td colspan=4> - <a href="newfile?:multilink=support<display call="plain('id')">:files">Attach a file to support call</a> - </td> -</tr> -<property name="files"> - <tr> - <td colspan=4><display call="list('files')"></td> - </tr> -</property> - -</table> - -""" - -timelogDOTindex = """<!-- dollarId: timelog.index,v 1.1 2001/07/30 08:04:26 richard Exp dollar--> -<tr> - <property name="date"> - <td><display call="link('date')"></td> - </property> - <property name="performedby"> - <td><display call="plain('performedby')"></td> - </property> - <property name="time"> - <td><display call="plain('time')"></td> - </property> - <property name="description"> - <td><display call="plain('description')"></td> - </property> -</tr> -""" - -timelogDOTitem = """<!-- dollarId: timelog.item,v 1.1 2001/07/30 08:04:26 richard Exp dollar--> -<table border=0 cellspacing=0 cellpadding=2> - -<tr class="strong-header"> - <td colspan=2>Time log details</td> -</td> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Time spent</span></td> - <td class="form-text"><display call="field('time', size=40)"></td> -</tr> -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Description</span></td> - <td class="form-text"><display call="field('description', size=40)"></td> -</tr> -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Date</span></td> - <td class="form-text"><display call="field('date', size=40)"></td> -</tr> -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Performed by</span></td> - <td class="form-text"><display call="field('performedby', size=40)"></td> -</tr> - -<tr bgcolor="ffffea"> - <td> </td> - <td class="form-text"><display call="submit()"></td> -</tr> - -<tr class="strong-header"> - <td colspan=2><b>History</b></td> -</tr> -<tr> - <td colspan=2><display call="history()"></td> -</tr> - -</table> - -""" - -userDOTindex = """<!-- dollarId: user.index,v 1.1 2001/07/23 04:21:20 richard Exp dollar--> -<tr> - <property name="username"> - <td><display call="link('username')"></td> - </property> - <property name="realname"> - <td><display call="plain('realname')"></td> - </property> - <property name="organisation"> - <td><display call="plain('organisation')"></td> - </property> - <property name="address"> - <td><display call="plain('address')"></td> - </property> - <property name="phone"> - <td><display call="plain('phone')"></td> - </property> -</tr> -""" - -userDOTitem = """<!-- dollarId: user.item,v 1.1 2001/07/23 04:21:20 richard Exp dollar--> -<table border=0 cellspacing=0 cellpadding=2> - -<tr class="strong-header"> - <td colspan=2>Your Details</td> -</td> - -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Name</span></td> - <td class="form-text"><display call="field('realname', size=40)"></td> -</tr> -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Login Name</span></td> - <td class="form-text"><display call="field('username', size=40)"></td> -</tr> -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Login Password</span></td> - <td class="form-text"><display call="field('password', size=10)"></td> -</tr> -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Phone</span></td> - <td class="form-text"><display call="field('phone', size=40)"></td> -</tr> -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">Organisation</span></td> - <td class="form-text"><display call="field('organisation', size=40)"></td> -</tr> -<tr bgcolor="ffffea"> - <td width=1% nowrap align=right><span class="form-label">E-mail address</span></td> - <td class="form-text"><display call="field('address', size=40)"></td> -</tr> - -<tr bgcolor="ffffea"> - <td> </td> - <td class="form-text"><display call="submit()"></td> -</tr> - -<tr class="strong-header"> - <td colspan=2><b>History</b></td> -</tr> -<tr> - <td colspan=2><display call="history()"></td> -</tr> - -</table> - -""" -
--- a/roundup/templates/extended/interfaces.py Wed Feb 06 03:47:17 2002 +0000 +++ b/roundup/templates/extended/interfaces.py Wed Feb 06 04:05:55 2002 +0000 @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: interfaces.py,v 1.15 2002-01-02 02:32:38 richard Exp $ +# $Id: interfaces.py,v 1.15.2.1 2002-02-06 04:05:54 richard Exp $ import instance_config from roundup import cgi_client, mailgw @@ -24,24 +24,28 @@ ''' derives basic CGI implementation from the standard module, with any specific extensions ''' - INSTANCE_NAME = instance_config.INSTANCE_NAME - TEMPLATES = instance_config.TEMPLATES - FILTER_POSITION = instance_config.FILTER_POSITION - ANONYMOUS_ACCESS = instance_config.ANONYMOUS_ACCESS - ANONYMOUS_REGISTER = instance_config.ANONYMOUS_REGISTER + pass class MailGW(mailgw.MailGW): ''' derives basic mail gateway implementation from the standard module, with any specific extensions ''' - INSTANCE_NAME = instance_config.INSTANCE_NAME - ISSUE_TRACKER_EMAIL = instance_config.ISSUE_TRACKER_EMAIL - ADMIN_EMAIL = instance_config.ADMIN_EMAIL - MAILHOST = instance_config.MAILHOST - ANONYMOUS_REGISTER = instance_config.ANONYMOUS_REGISTER + pass # # $Log: not supported by cvs2svn $ +# Revision 1.16 2002/01/14 02:20:15 richard +# . changed all config accesses so they access either the instance or the +# config attriubute on the db. This means that all config is obtained from +# instance_config instead of the mish-mash of classes. This will make +# switching to a ConfigParser setup easier too, I hope. +# +# At a minimum, this makes migration a _little_ easier (a lot easier in the +# 0.5.0 switch, I hope!) +# +# Revision 1.15 2002/01/02 02:32:38 richard +# ANONYMOUS_ACCESS -> ANONYMOUS_REGISTER +# # Revision 1.14 2001/12/20 15:43:01 rochecompaan # Features added: # . Multilink properties are now displayed as comma separated values in
--- a/run_tests Wed Feb 06 03:47:17 2002 +0000 +++ b/run_tests Wed Feb 06 04:05:55 2002 +0000 @@ -9,13 +9,16 @@ # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # -# $Id: run_tests,v 1.3 2002-01-23 20:09:41 jhermann Exp $ +# $Id: run_tests,v 1.3.2.1 2002-02-06 04:05:53 richard Exp $ import test test.go() # # $Log: not supported by cvs2svn $ +# Revision 1.3 2002/01/23 20:09:41 jhermann +# Proper fix for failing test +# # Revision 1.2 2002/01/23 11:08:52 grubert # . run_tests testReldate_date failed if LANG is 'german' #
--- a/setup.py Wed Feb 06 03:47:17 2002 +0000 +++ b/setup.py Wed Feb 06 04:05:55 2002 +0000 @@ -16,19 +16,107 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: setup.py,v 1.26 2001-12-08 07:06:20 jhermann Exp $ +# $Id: setup.py,v 1.26.2.1 2002-02-06 04:05:53 richard Exp $ from distutils.core import setup, Extension from distutils.util import get_platform +from distutils.command.build_scripts import build_scripts +import sys, os, string from glob import glob -import os + from roundup.templatebuilder import makeHtmlBase -print 'Running unit tests...' -import test -test.go() + +############################################################################# +### Build script files +############################################################################# + +class build_scripts_create(build_scripts): + """ Overload the build_scripts command and create the scripts + from scratch, depending on the target platform. + + You have to define the name of your package in an inherited + class (due to the delayed instantiation of command classes + in distutils, this cannot be passed to __init__). + + The scripts are created in an uniform scheme: they start the + run() function in the module + + <packagename>.scripts.<mangled_scriptname> + + The mangling of script names replaces '-' and '/' characters + with '-' and '.', so that they are valid module paths. + """ + package_name = None + + def copy_scripts(self): + """ Create each script listed in 'self.scripts' + """ + if not self.package_name: + raise Exception("You have to inherit build_scripts_create and" + " provide a package name") + + to_module = string.maketrans('-/', '_.') + + self.mkpath(self.build_dir) + for script in self.scripts: + outfile = os.path.join(self.build_dir, os.path.basename(script)) + + #if not self.force and not newer(script, outfile): + # self.announce("not copying %s (up-to-date)" % script) + # continue + + if self.dry_run: + self.announce("would create %s" % outfile) + continue + module = os.path.splitext(os.path.basename(script))[0] + module = string.translate(module, to_module) + script_vars = { + 'python': os.path.normpath(sys.executable), + 'package': self.package_name, + 'module': module, + } + + self.announce("creating %s" % outfile) + file = open(outfile, 'w') + + try: + if sys.platform == "win32": + file.write('@echo off\n' + '%(python)s -c "from %(package)s.scripts.%(module)s import run; run()" %%$\n' + % script_vars) + else: + file.write('#! %(python)s\n' + 'from %(package)s.scripts.%(module)s import run\n' + 'run()\n' + % script_vars) + finally: + file.close() + + +class build_scripts_roundup(build_scripts_create): + package_name = 'roundup' + + +def scriptname(path): + """ Helper for building a list of script names from a list of + module files. + """ + script = os.path.splitext(os.path.basename(path))[0] + script = string.replace(script, '_', '-') + if sys.platform == "win32": + script = script + ".bat" + return script + +# build list of scripts from their implementation modules +roundup_scripts = map(scriptname, glob('roundup/scripts/[!_]*.py')) + + +############################################################################# +### Main setup stuff +############################################################################# def isTemplateDir(dir): return dir[0] != '.' and dir != 'CVS' and os.path.isdir(dir) \ @@ -39,6 +127,7 @@ packagelist = [ 'roundup', 'roundup.backends', + 'roundup.scripts', 'roundup.templates' ] installdatafiles = [ @@ -63,19 +152,40 @@ setup( name = "roundup", - version = "0.3.0", + version = "0.4.0", description = "Roundup issue tracking system.", author = "Richard Jones", author_email = "richard@users.sourceforge.net", url = 'http://sourceforge.net/projects/roundup/', packages = packagelist, - scripts = ['roundup-admin', 'roundup-mailgw', 'roundup-server'], + + # Override certain command classes with our own ones + cmdclass = { + 'build_scripts': build_scripts_roundup, + }, + scripts = roundup_scripts, + data_files = installdatafiles ) # # $Log: not supported by cvs2svn $ +# Revision 1.30 2002/01/29 20:07:15 jhermann +# Conversion to generated script stubs +# +# Revision 1.29 2002/01/23 06:05:36 richard +# prep work for release +# +# Revision 1.28 2002/01/11 03:24:15 richard +# minor changes for 0.4.0b2 +# +# Revision 1.27 2002/01/05 02:09:46 richard +# make setup abort if tests fail +# +# Revision 1.26 2001/12/08 07:06:20 jhermann +# Install html template files to share/roundup/templates +# # Revision 1.25 2001/11/21 23:42:54 richard # Some version number and documentation fixes. #
--- a/test/__init__.py Wed Feb 06 03:47:17 2002 +0000 +++ b/test/__init__.py Wed Feb 06 04:05:55 2002 +0000 @@ -15,14 +15,14 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: __init__.py,v 1.9 2002-01-02 02:31:38 richard Exp $ +# $Id: __init__.py,v 1.9.2.1 2002-02-06 04:05:55 richard Exp $ import unittest import os, tempfile os.environ['SENDMAILDEBUG'] = tempfile.mktemp() import test_dates, test_schema, test_db, test_multipart, test_mailsplit -import test_init, test_token, test_mailgw +import test_init, test_token, test_mailgw, test_htmltemplate def go(): suite = unittest.TestSuite(( @@ -34,12 +34,54 @@ test_mailsplit.suite(), test_mailgw.suite(), test_token.suite(), + test_htmltemplate.suite(), )) runner = unittest.TextTestRunner() - runner.run(suite) + result = runner.run(suite) + return result.wasSuccessful() # # $Log: not supported by cvs2svn $ +# Revision 1.15 2002/01/22 00:12:20 richard +# oops +# +# Revision 1.14 2002/01/22 00:12:06 richard +# Wrote more unit tests for htmltemplate, and while I was at it, I polished +# off the implementation of some of the functions so they behave sanely. +# +# Revision 1.13 2002/01/21 11:05:48 richard +# New tests for htmltemplate (well, it's a beginning) +# +# Revision 1.12 2002/01/14 06:53:28 richard +# had commented out some tests +# +# Revision 1.11 2002/01/14 02:20:15 richard +# . changed all config accesses so they access either the instance or the +# config attriubute on the db. This means that all config is obtained from +# instance_config instead of the mish-mash of classes. This will make +# switching to a ConfigParser setup easier too, I hope. +# +# At a minimum, this makes migration a _little_ easier (a lot easier in the +# 0.5.0 switch, I hope!) +# +# Revision 1.10 2002/01/05 02:09:46 richard +# make setup abort if tests fail +# +# Revision 1.9 2002/01/02 02:31:38 richard +# Sorry for the huge checkin message - I was only intending to implement #496356 +# but I found a number of places where things had been broken by transactions: +# . modified ROUNDUPDBSENDMAILDEBUG to be SENDMAILDEBUG and hold a filename +# for _all_ roundup-generated smtp messages to be sent to. +# . the transaction cache had broken the roundupdb.Class set() reactors +# . newly-created author users in the mailgw weren't being committed to the db +# +# Stuff that made it into CHANGES.txt (ie. the stuff I was actually working +# on when I found that stuff :): +# . #496356 ] Use threading in messages +# . detectors were being registered multiple times +# . added tests for mailgw +# . much better attaching of erroneous messages in the mail gateway +# # Revision 1.8 2001/12/31 05:09:20 richard # Added better tokenising to roundup-admin - handles spaces and stuff. Can # use quoting or backslashes. See the roundup.token pydoc.
--- a/test/test_dates.py Wed Feb 06 03:47:17 2002 +0000 +++ b/test/test_dates.py Wed Feb 06 04:05:55 2002 +0000 @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: test_dates.py,v 1.7 2001-08-13 23:01:53 richard Exp $ +# $Id: test_dates.py,v 1.7.2.1 2002-02-06 04:05:55 richard Exp $ import unittest, time @@ -35,6 +35,10 @@ ae = self.assertEqual date = Date("2000-04-17") ae(str(date), '2000-04-17.00:00:00') + date = Date("2000-4-7") + ae(str(date), '2000-04-07.00:00:00') + date = Date("2000-4-17") + ae(str(date), '2000-04-17.00:00:00') date = Date("01-25") y, m, d, x, x, x, x, x, x = time.gmtime(time.time()) ae(str(date), '%s-01-25.00:00:00'%y) @@ -83,6 +87,13 @@ # # $Log: not supported by cvs2svn $ +# Revision 1.8 2002/01/16 07:02:57 richard +# . lots of date/interval related changes: +# - more relaxed date format for input +# +# Revision 1.7 2001/08/13 23:01:53 richard +# fixed a 2.1-ism +# # Revision 1.6 2001/08/07 00:24:43 richard # stupid typo #
--- a/test/test_db.py Wed Feb 06 03:47:17 2002 +0000 +++ b/test/test_db.py Wed Feb 06 04:05:55 2002 +0000 @@ -15,13 +15,14 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: test_db.py,v 1.12 2001-12-17 03:52:48 richard Exp $ +# $Id: test_db.py,v 1.12.2.1 2002-02-06 04:05:55 richard Exp $ import unittest, os, shutil from roundup.hyperdb import String, Password, Link, Multilink, Date, \ Interval, Class, DatabaseError from roundup.roundupdb import FileClass +from roundup import date def setupSchema(db, create): status = Class(db, "status", name=String()) @@ -33,7 +34,7 @@ status.create(name="resolved") Class(db, "user", username=String(), password=Password()) Class(db, "issue", title=String(), status=Link("status"), - nosy=Multilink("user")) + nosy=Multilink("user"), deadline=Date(), foo=Interval()) FileClass(db, "file", name=String(), type=String()) db.commit() @@ -41,15 +42,29 @@ def tearDown(self): if os.path.exists('_test_dir'): shutil.rmtree('_test_dir') - + +class config: + DATABASE='_test_dir' + MAILHOST = 'localhost' + MAIL_DOMAIN = 'fill.me.in.' + INSTANCE_NAME = 'Roundup issue tracker' + ISSUE_TRACKER_EMAIL = 'issue_tracker@%s'%MAIL_DOMAIN + ISSUE_TRACKER_WEB = 'http://some.useful.url/' + ADMIN_EMAIL = 'roundup-admin@%s'%MAIL_DOMAIN + FILTER_POSITION = 'bottom' # one of 'top', 'bottom', 'top and bottom' + ANONYMOUS_ACCESS = 'deny' # either 'deny' or 'allow' + ANONYMOUS_REGISTER = 'deny' # either 'deny' or 'allow' + MESSAGES_TO_AUTHOR = 'no' # either 'yes' or 'no' + EMAIL_SIGNATURE_POSITION = 'bottom' + class anydbmDBTestCase(MyTestCase): def setUp(self): from roundup.backends import anydbm # remove previous test, ignore errors - if os.path.exists('_test_dir'): - shutil.rmtree('_test_dir') - os.makedirs('_test_dir/files') - self.db = anydbm.Database('_test_dir', 'test') + if os.path.exists(config.DATABASE): + shutil.rmtree(config.DATABASE) + os.makedirs(config.DATABASE + '/files') + self.db = anydbm.Database(config, 'test') setupSchema(self.db, 1) def testChanges(self): @@ -62,9 +77,19 @@ props = self.db.issue.getprops() keys = props.keys() keys.sort() - self.assertEqual(keys, ['fixer', 'id', 'nosy', 'status', 'title']) + self.assertEqual(keys, ['deadline', 'fixer', 'foo', 'id', 'nosy', + 'status', 'title']) self.db.issue.set('5', status='2') self.db.issue.get('5', "status") + + a = self.db.issue.get('5', "deadline") + self.db.issue.set('5', deadline=date.Date()) + self.assertNotEqual(a, self.db.issue.get('5', "deadline")) + + a = self.db.issue.get('5', "foo") + self.db.issue.set('5', foo=date.Interval('-1d')) + self.assertNotEqual(a, self.db.issue.get('5', "foo")) + self.db.status.get('2', "name") self.db.issue.get('5', "title") self.db.issue.find(status = self.db.status.lookup("in-progress")) @@ -165,6 +190,67 @@ ar(IndexError, self.db.issue.set, '1', title='foo', status='1', nosy=['10']) + def testJournals(self): + self.db.issue.addprop(fixer=Link("user", do_journal='yes')) + self.db.user.create(username="mary") + self.db.user.create(username="pete") + self.db.issue.create(title="spam", status='1') + self.db.commit() + + # journal entry for issue create + journal = self.db.getjournal('issue', '1') + self.assertEqual(1, len(journal)) + (nodeid, date_stamp, journaltag, action, params) = journal[0] + self.assertEqual(nodeid, '1') + self.assertEqual(journaltag, 'test') + self.assertEqual(action, 'create') + keys = params.keys() + keys.sort() + self.assertEqual(keys, ['deadline', 'fixer', 'foo', 'nosy', + 'status', 'title']) + self.assertEqual(None,params['deadline']) + self.assertEqual(None,params['fixer']) + self.assertEqual(None,params['foo']) + self.assertEqual([],params['nosy']) + self.assertEqual('1',params['status']) + self.assertEqual('spam',params['title']) + + # journal entry for link + journal = self.db.getjournal('user', '1') + self.assertEqual(1, len(journal)) + self.db.issue.set('1', fixer='1') + self.db.commit() + journal = self.db.getjournal('user', '1') + self.assertEqual(2, len(journal)) + (nodeid, date_stamp, journaltag, action, params) = journal[1] + self.assertEqual('1', nodeid) + self.assertEqual('test', journaltag) + self.assertEqual('link', action) + self.assertEqual(('issue', '1', 'fixer'), params) + + # journal entry for unlink + self.db.issue.set('1', fixer='2') + self.db.commit() + journal = self.db.getjournal('user', '1') + self.assertEqual(3, len(journal)) + (nodeid, date_stamp, journaltag, action, params) = journal[2] + self.assertEqual('1', nodeid) + self.assertEqual('test', journaltag) + self.assertEqual('unlink', action) + self.assertEqual(('issue', '1', 'fixer'), params) + + def testPack(self): + self.db.issue.create(title="spam", status='1') + self.db.commit() + self.db.issue.set('1', status='2') + self.db.commit() + self.db.issue.set('1', status='3') + self.db.commit() + pack_before = date.Date(". + 1d") + self.db.pack(pack_before) + journal = self.db.getjournal('issue', '1') + self.assertEqual(2, len(journal)) + def testRetire(self): pass @@ -173,12 +259,12 @@ def setUp(self): from roundup.backends import anydbm # remove previous test, ignore errors - if os.path.exists('_test_dir'): - shutil.rmtree('_test_dir') - os.makedirs('_test_dir/files') - db = anydbm.Database('_test_dir', 'test') + if os.path.exists(config.DATABASE): + shutil.rmtree(config.DATABASE) + os.makedirs(config.DATABASE + '/files') + db = anydbm.Database(config, 'test') setupSchema(db, 1) - self.db = anydbm.Database('_test_dir') + self.db = anydbm.Database(config) setupSchema(self.db, 0) def testExceptions(self): @@ -195,22 +281,22 @@ def setUp(self): from roundup.backends import bsddb # remove previous test, ignore errors - if os.path.exists('_test_dir'): - shutil.rmtree('_test_dir') - os.makedirs('_test_dir/files') - self.db = bsddb.Database('_test_dir', 'test') + if os.path.exists(config.DATABASE): + shutil.rmtree(config.DATABASE) + os.makedirs(config.DATABASE + '/files') + self.db = bsddb.Database(config, 'test') setupSchema(self.db, 1) class bsddbReadOnlyDBTestCase(anydbmReadOnlyDBTestCase): def setUp(self): from roundup.backends import bsddb # remove previous test, ignore errors - if os.path.exists('_test_dir'): - shutil.rmtree('_test_dir') - os.makedirs('_test_dir/files') - db = bsddb.Database('_test_dir', 'test') + if os.path.exists(config.DATABASE): + shutil.rmtree(config.DATABASE) + os.makedirs(config.DATABASE + '/files') + db = bsddb.Database(config, 'test') setupSchema(db, 1) - self.db = bsddb.Database('_test_dir') + self.db = bsddb.Database(config) setupSchema(self.db, 0) @@ -218,22 +304,22 @@ def setUp(self): from roundup.backends import bsddb3 # remove previous test, ignore errors - if os.path.exists('_test_dir'): - shutil.rmtree('_test_dir') - os.makedirs('_test_dir/files') - self.db = bsddb3.Database('_test_dir', 'test') + if os.path.exists(config.DATABASE): + shutil.rmtree(config.DATABASE) + os.makedirs(config.DATABASE + '/files') + self.db = bsddb3.Database(config, 'test') setupSchema(self.db, 1) class bsddb3ReadOnlyDBTestCase(anydbmReadOnlyDBTestCase): def setUp(self): from roundup.backends import bsddb3 # remove previous test, ignore errors - if os.path.exists('_test_dir'): - shutil.rmtree('_test_dir') - os.makedirs('_test_dir/files') - db = bsddb3.Database('_test_dir', 'test') + if os.path.exists(config.DATABASE): + shutil.rmtree(config.DATABASE) + os.makedirs(config.DATABASE + '/files') + db = bsddb3.Database(config, 'test') setupSchema(db, 1) - self.db = bsddb3.Database('_test_dir') + self.db = bsddb3.Database(config) setupSchema(self.db, 0) @@ -260,6 +346,45 @@ # # $Log: not supported by cvs2svn $ +# Revision 1.18 2002/01/22 07:21:13 richard +# . fixed back_bsddb so it passed the journal tests +# +# ... it didn't seem happy using the back_anydbm _open method, which is odd. +# Yet another occurrance of whichdb not being able to recognise older bsddb +# databases. Yadda yadda. Made the HYPERDBDEBUG stuff more sane in the +# process. +# +# Revision 1.17 2002/01/22 05:06:09 rochecompaan +# We need to keep the last 'set' entry in the journal to preserve +# information on 'activity' for nodes. +# +# Revision 1.16 2002/01/21 16:33:20 rochecompaan +# You can now use the roundup-admin tool to pack the database +# +# Revision 1.15 2002/01/19 13:16:04 rochecompaan +# Journal entries for link and multilink properties can now be switched on +# or off. +# +# Revision 1.14 2002/01/16 07:02:57 richard +# . lots of date/interval related changes: +# - more relaxed date format for input +# +# Revision 1.13 2002/01/14 02:20:15 richard +# . changed all config accesses so they access either the instance or the +# config attriubute on the db. This means that all config is obtained from +# instance_config instead of the mish-mash of classes. This will make +# switching to a ConfigParser setup easier too, I hope. +# +# At a minimum, this makes migration a _little_ easier (a lot easier in the +# 0.5.0 switch, I hope!) +# +# Revision 1.12 2001/12/17 03:52:48 richard +# Implemented file store rollback. As a bonus, the hyperdb is now capable of +# storing more than one file per node - if a property name is supplied, +# the file is called designator.property. +# I decided not to migrate the existing files stored over to the new naming +# scheme - the FileClass just doesn't specify the property name. +# # Revision 1.11 2001/12/10 23:17:20 richard # Added transaction tests to test_db #
--- a/test/test_htmltemplate.py Wed Feb 06 03:47:17 2002 +0000 +++ b/test/test_htmltemplate.py Wed Feb 06 04:05:55 2002 +0000 @@ -8,7 +8,7 @@ # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # -# $Id: test_htmltemplate.py,v 1.8 2002-02-06 03:47:16 richard Exp $ +# $Id: test_htmltemplate.py,v 1.8.2.1 2002-02-06 04:05:55 richard Exp $ import unittest, cgi, time @@ -314,6 +314,9 @@ # # $Log: not supported by cvs2svn $ +# Revision 1.8 2002/02/06 03:47:16 richard +# . #511586 ] unittest FAIL: testReldate_date +# # Revision 1.7 2002/01/23 20:09:41 jhermann # Proper fix for failing test #
--- a/test/test_mailgw.py Wed Feb 06 03:47:17 2002 +0000 +++ b/test/test_mailgw.py Wed Feb 06 04:05:55 2002 +0000 @@ -8,7 +8,7 @@ # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # -# $Id: test_mailgw.py,v 1.1 2002-01-02 02:31:38 richard Exp $ +# $Id: test_mailgw.py,v 1.1.2.1 2002-02-06 04:05:55 richard Exp $ import unittest, cStringIO, tempfile, os, shutil, errno, imp, sys @@ -33,6 +33,8 @@ self.db = self.instance.open('sekrit') self.db.user.create(username='Chef', address='chef@bork.bork.bork') self.db.user.create(username='richard', address='richard@test') + self.db.user.create(username='mary', address='mary@test') + self.db.user.create(username='john', address='john@test') def tearDown(self): if os.path.exists(os.environ['SENDMAILDEBUG']): @@ -65,21 +67,21 @@ From: Chef <chef@bork.bork.bork To: issue_tracker@fill.me.in. Message-Id: <dummy_test_message_id> -Subject: [issue] Testing... +Subject: [issue] Testing... [nosy=mary; assignedto=richard] This is a test submission of a new issue. ''') handler = self.instance.MailGW(self.instance, self.db) # TODO: fix the damn config - this is apalling - self.instance.IssueClass.MESSAGES_TO_AUTHOR = 'yes' + self.db.config.MESSAGES_TO_AUTHOR = 'yes' handler.main(message) self.assertEqual(open(os.environ['SENDMAILDEBUG']).read(), '''FROM: roundup-admin@fill.me.in. -TO: chef@bork.bork.bork +TO: chef@bork.bork.bork, mary@test, richard@test Content-Type: text/plain Subject: [issue1] Testing... -To: chef@bork.bork.bork +To: chef@bork.bork.bork, mary@test, richard@test From: Chef <issue_tracker@fill.me.in.> Reply-To: Roundup issue tracker <issue_tracker@fill.me.in.> MIME-Version: 1.0 @@ -90,11 +92,18 @@ This is a test submission of a new issue. + +---------- +assignedto: richard +messages: 1 +nosy: mary, Chef, richard +status: unread +title: Testing... ___________________________________________________ "Roundup issue tracker" <issue_tracker@fill.me.in.> http://some.useful.url/issue1 ___________________________________________________ -''', 'Generated message not correct') +''') def testFollowup(self): self.testNewIssue() @@ -104,20 +113,19 @@ To: issue_tracker@fill.me.in. Message-Id: <followup_dummy_id> In-Reply-To: <dummy_test_message_id> -Subject: [issue1] Testing... +Subject: [issue1] Testing... [assignedto=mary; nosy=john] This is a followup ''') handler = self.instance.MailGW(self.instance, self.db) - # TODO: fix the damn config - this is apalling handler.main(message) self.assertEqual(open(os.environ['SENDMAILDEBUG']).read(), '''FROM: roundup-admin@fill.me.in. -TO: chef@bork.bork.bork +TO: chef@bork.bork.bork, mary@test, john@test Content-Type: text/plain Subject: [issue1] Testing... -To: chef@bork.bork.bork +To: chef@bork.bork.bork, mary@test, john@test From: richard <issue_tracker@fill.me.in.> Reply-To: Roundup issue tracker <issue_tracker@fill.me.in.> MIME-Version: 1.0 @@ -129,6 +137,147 @@ This is a followup + +---------- +assignedto: -> mary +nosy: +mary, john +status: unread -> chatting +___________________________________________________ +"Roundup issue tracker" <issue_tracker@fill.me.in.> +http://some.useful.url/issue1 +___________________________________________________ +''', 'Generated message not correct') + + def testFollowup2(self): + self.testNewIssue() + message = cStringIO.StringIO('''Content-Type: text/plain; + charset="iso-8859-1" +From: mary <mary@test> +To: issue_tracker@fill.me.in. +Message-Id: <followup_dummy_id> +In-Reply-To: <dummy_test_message_id> +Subject: [issue1] Testing... + +This is a second followup +''') + handler = self.instance.MailGW(self.instance, self.db) + handler.main(message) + self.assertEqual(open(os.environ['SENDMAILDEBUG']).read(), +'''FROM: roundup-admin@fill.me.in. +TO: chef@bork.bork.bork, richard@test +Content-Type: text/plain +Subject: [issue1] Testing... +To: chef@bork.bork.bork, richard@test +From: mary <issue_tracker@fill.me.in.> +Reply-To: Roundup issue tracker <issue_tracker@fill.me.in.> +MIME-Version: 1.0 +Message-Id: <followup_dummy_id> +In-Reply-To: <dummy_test_message_id> + + +mary <mary@test> added the comment: + +This is a second followup + + +---------- +status: unread -> chatting +___________________________________________________ +"Roundup issue tracker" <issue_tracker@fill.me.in.> +http://some.useful.url/issue1 +___________________________________________________ +''', 'Generated message not correct') + + def testEnc01(self): + self.testNewIssue() + message = cStringIO.StringIO('''Content-Type: text/plain; + charset="iso-8859-1" +From: mary <mary@test> +To: issue_tracker@fill.me.in. +Message-Id: <followup_dummy_id> +In-Reply-To: <dummy_test_message_id> +Subject: [issue1] Testing... +Content-Type: text/plain; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +A message with encoding (encoded oe =F6) + +''') + handler = self.instance.MailGW(self.instance, self.db) + handler.main(message) + message_data = open(os.environ['SENDMAILDEBUG']).read() + self.assertEqual(message_data, +'''FROM: roundup-admin@fill.me.in. +TO: chef@bork.bork.bork, richard@test +Content-Type: text/plain +Subject: [issue1] Testing... +To: chef@bork.bork.bork, richard@test +From: mary <issue_tracker@fill.me.in.> +Reply-To: Roundup issue tracker <issue_tracker@fill.me.in.> +MIME-Version: 1.0 +Message-Id: <followup_dummy_id> +In-Reply-To: <dummy_test_message_id> + + +mary <mary@test> added the comment: + +A message with encoding (encoded oe ö) + +---------- +status: unread -> chatting +___________________________________________________ +"Roundup issue tracker" <issue_tracker@fill.me.in.> +http://some.useful.url/issue1 +___________________________________________________ +''', 'Generated message not correct') + + + def testMultipartEnc01(self): + self.testNewIssue() + message = cStringIO.StringIO('''Content-Type: text/plain; + charset="iso-8859-1" +From: mary <mary@test> +To: issue_tracker@fill.me.in. +Message-Id: <followup_dummy_id> +In-Reply-To: <dummy_test_message_id> +Subject: [issue1] Testing... +Content-Type: multipart/mixed; + boundary="----_=_NextPart_000_01" + +This message is in MIME format. Since your mail reader does not understand +this format, some or all of this message may not be legible. + +------_=_NextPart_000_01 +Content-Type: text/plain; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +A message with first part encoded (encoded oe =F6) + +''') + handler = self.instance.MailGW(self.instance, self.db) + handler.main(message) + message_data = open(os.environ['SENDMAILDEBUG']).read() + self.assertEqual(message_data, +'''FROM: roundup-admin@fill.me.in. +TO: chef@bork.bork.bork, richard@test +Content-Type: text/plain +Subject: [issue1] Testing... +To: chef@bork.bork.bork, richard@test +From: mary <issue_tracker@fill.me.in.> +Reply-To: Roundup issue tracker <issue_tracker@fill.me.in.> +MIME-Version: 1.0 +Message-Id: <followup_dummy_id> +In-Reply-To: <dummy_test_message_id> + + +mary <mary@test> added the comment: + +A message with first part encoded (encoded oe ö) + +---------- +status: unread -> chatting ___________________________________________________ "Roundup issue tracker" <issue_tracker@fill.me.in.> http://some.useful.url/issue1 @@ -140,12 +289,70 @@ def suite(): l = [unittest.makeSuite(MailgwTestCase, 'test'), - unittest.makeSuite(ExtMailgwTestCase, 'test')] + unittest.makeSuite(ExtMailgwTestCase, 'test') + ] return unittest.TestSuite(l) # # $Log: not supported by cvs2svn $ +# Revision 1.9 2002/02/05 14:15:29 grubert +# . respect encodings in non multipart messages. +# +# Revision 1.8 2002/02/04 09:40:21 grubert +# . add test for multipart messages with first part being encoded. +# +# Revision 1.7 2002/01/22 11:54:45 rochecompaan +# Fixed status change in mail gateway. +# +# Revision 1.6 2002/01/21 10:05:48 rochecompaan +# Feature: +# . the mail gateway now responds with an error message when invalid +# values for arguments are specified for link or multilink properties +# . modified unit test to check nosy and assignedto when specified as +# arguments +# +# Fixed: +# . fixed setting nosy as argument in subject line +# +# Revision 1.5 2002/01/15 00:12:40 richard +# #503340 ] creating issue with [asignedto=p.ohly] +# +# Revision 1.4 2002/01/14 07:12:15 richard +# removed file writing from tests... +# +# Revision 1.3 2002/01/14 02:20:15 richard +# . changed all config accesses so they access either the instance or the +# config attriubute on the db. This means that all config is obtained from +# instance_config instead of the mish-mash of classes. This will make +# switching to a ConfigParser setup easier too, I hope. +# +# At a minimum, this makes migration a _little_ easier (a lot easier in the +# 0.5.0 switch, I hope!) +# +# Revision 1.2 2002/01/11 23:22:29 richard +# . #502437 ] rogue reactor and unittest +# in short, the nosy reactor was modifying the nosy list. That code had +# been there for a long time, and I suspsect it was there because we +# weren't generating the nosy list correctly in other places of the code. +# We're now doing that, so the nosy-modifying code can go away from the +# nosy reactor. +# +# Revision 1.1 2002/01/02 02:31:38 richard +# Sorry for the huge checkin message - I was only intending to implement #496356 +# but I found a number of places where things had been broken by transactions: +# . modified ROUNDUPDBSENDMAILDEBUG to be SENDMAILDEBUG and hold a filename +# for _all_ roundup-generated smtp messages to be sent to. +# . the transaction cache had broken the roundupdb.Class set() reactors +# . newly-created author users in the mailgw weren't being committed to the db +# +# Stuff that made it into CHANGES.txt (ie. the stuff I was actually working +# on when I found that stuff :): +# . #496356 ] Use threading in messages +# . detectors were being registered multiple times +# . added tests for mailgw +# . much better attaching of erroneous messages in the mail gateway +# # # #
--- a/test/test_mailsplit.py Wed Feb 06 03:47:17 2002 +0000 +++ b/test/test_mailsplit.py Wed Feb 06 04:05:55 2002 +0000 @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: test_mailsplit.py,v 1.8 2001-10-28 23:22:28 richard Exp $ +# $Id: test_mailsplit.py,v 1.8.2.1 2002-02-06 04:05:55 richard Exp $ import unittest, cStringIO @@ -89,6 +89,18 @@ self.assertEqual(summary, 'testing') self.assertEqual(content, 'testing\n\ntesting\n\ntesting') + def testSimpleFollowup(self): + s = '''>hello\ntesting''' + summary, content = parseContent(s) + self.assertEqual(summary, 'testing') + self.assertEqual(content, 'testing') + + def testSimpleFollowupParas(self): + s = '''>hello\ntesting\n\ntesting\n\ntesting''' + summary, content = parseContent(s) + self.assertEqual(summary, 'testing') + self.assertEqual(content, 'testing\n\ntesting\n\ntesting') + def testEmpty(self): s = '' summary, content = parseContent(s) @@ -111,6 +123,12 @@ # # $Log: not supported by cvs2svn $ +# Revision 1.9 2002/01/10 06:19:20 richard +# followup lines directly after a quoted section were being eaten. +# +# Revision 1.8 2001/10/28 23:22:28 richard +# fixed bug #474749 ] Indentations lost +# # Revision 1.7 2001/10/23 00:57:32 richard # Removed debug print from mailsplit test. #
--- a/test/test_schema.py Wed Feb 06 03:47:17 2002 +0000 +++ b/test/test_schema.py Wed Feb 06 04:05:55 2002 +0000 @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: test_schema.py,v 1.6 2001-12-03 21:33:39 richard Exp $ +# $Id: test_schema.py,v 1.6.2.1 2002-02-06 04:05:55 richard Exp $ import unittest, os, shutil @@ -23,15 +23,29 @@ from roundup.hyperdb import String, Password, Link, Multilink, Date, \ Interval, Class +class config: + DATABASE='_test_dir' + MAILHOST = 'localhost' + MAIL_DOMAIN = 'fill.me.in.' + INSTANCE_NAME = 'Roundup issue tracker' + ISSUE_TRACKER_EMAIL = 'issue_tracker@%s'%MAIL_DOMAIN + ISSUE_TRACKER_WEB = 'http://some.useful.url/' + ADMIN_EMAIL = 'roundup-admin@%s'%MAIL_DOMAIN + FILTER_POSITION = 'bottom' # one of 'top', 'bottom', 'top and bottom' + ANONYMOUS_ACCESS = 'deny' # either 'deny' or 'allow' + ANONYMOUS_REGISTER = 'deny' # either 'deny' or 'allow' + MESSAGES_TO_AUTHOR = 'no' # either 'yes' or 'no' + EMAIL_SIGNATURE_POSITION = 'bottom' + class SchemaTestCase(unittest.TestCase): def setUp(self): class Database(anydbm.Database): pass # remove previous test, ignore errors - if os.path.exists('_test_dir'): - shutil.rmtree('_test_dir') - os.mkdir('_test_dir') - self.db = Database('_test_dir', 'test') + if os.path.exists(config.DATABASE): + shutil.rmtree(config.DATABASE) + os.makedirs(config.DATABASE + '/files') + self.db = Database(config, 'test') self.db.clear() def tearDown(self): @@ -75,6 +89,18 @@ # # $Log: not supported by cvs2svn $ +# Revision 1.7 2002/01/14 02:20:15 richard +# . changed all config accesses so they access either the instance or the +# config attriubute on the db. This means that all config is obtained from +# instance_config instead of the mish-mash of classes. This will make +# switching to a ConfigParser setup easier too, I hope. +# +# At a minimum, this makes migration a _little_ easier (a lot easier in the +# 0.5.0 switch, I hope!) +# +# Revision 1.6 2001/12/03 21:33:39 richard +# Fixes so the tests use commit and not close +# # Revision 1.5 2001/10/09 07:25:59 richard # Added the Password property type. See "pydoc roundup.password" for # implementation details. Have updated some of the documentation too.
