[Python-checkins] r76545 - in tracker/roundup-src: CHANGES.txt demo.py doc/FAQ.txt doc/acknowledgements.txt doc/announcement.txt doc/developers.txt doc/features.txt doc/installation.txt doc/upgrading.txt doc/xmlrpc.txt locale/de.mo locale/de.po locale/fr.po roundup/__init__.py roundup/actions.py roundup/admin.py roundup/backends/__init__.py roundup/backends/back_postgresql.py roundup/backends/back_sqlite.py roundup/backends/indexer_common.py roundup/backends/indexer_dbm.py roundup/backends/indexer_rdbms.py roundup/backends/indexer_xapian.py roundup/cgi/PageTemplates/Expressions.py roundup/cgi/actions.py roundup/cgi/apache.py roundup/cgi/cgitb.py roundup/cgi/client.py roundup/cgi/form_parser.py roundup/cgi/templating.py roundup/configuration.py roundup/instance.py roundup/mailer.py roundup/mailgw.py roundup/roundupdb.py roundup/scripts/roundup_mailgw.py roundup/scripts/roundup_server.py roundup/xmlrpc.py setup.py share/roundup/templates/classic/html/_generic.item.html share/roundup/templates/classic/html/page.html share/roundup/templates/classic/html/style.css share/roundup/templates/classic/schema.py share/roundup/templates/minimal/html/_generic.item.html share/roundup/templates/minimal/html/page.html share/roundup/templates/minimal/html/user.item.html share/roundup/templates/minimal/schema.py test/db_test_base.py test/test_anypy_hashlib.py test/test_indexer.py test/test_mailgw.py test/test_xmlrpc.py

martin.v.loewis python-checkins at python.org
Fri Nov 27 12:58:05 CET 2009


Author: martin.v.loewis
Date: Fri Nov 27 12:58:04 2009
New Revision: 76545

Log:
Upgrade to 1.4.10, taken from
http://pypi.python.org/pypi/roundup/1.4.10.


Added:
   tracker/roundup-src/locale/de.mo   (contents, props changed)
Modified:
   tracker/roundup-src/CHANGES.txt
   tracker/roundup-src/demo.py
   tracker/roundup-src/doc/FAQ.txt
   tracker/roundup-src/doc/acknowledgements.txt
   tracker/roundup-src/doc/announcement.txt
   tracker/roundup-src/doc/developers.txt
   tracker/roundup-src/doc/features.txt
   tracker/roundup-src/doc/installation.txt
   tracker/roundup-src/doc/upgrading.txt
   tracker/roundup-src/doc/xmlrpc.txt
   tracker/roundup-src/locale/de.po
   tracker/roundup-src/locale/fr.po
   tracker/roundup-src/roundup/__init__.py
   tracker/roundup-src/roundup/actions.py
   tracker/roundup-src/roundup/admin.py
   tracker/roundup-src/roundup/backends/__init__.py
   tracker/roundup-src/roundup/backends/back_postgresql.py
   tracker/roundup-src/roundup/backends/back_sqlite.py
   tracker/roundup-src/roundup/backends/indexer_common.py
   tracker/roundup-src/roundup/backends/indexer_dbm.py
   tracker/roundup-src/roundup/backends/indexer_rdbms.py
   tracker/roundup-src/roundup/backends/indexer_xapian.py
   tracker/roundup-src/roundup/cgi/PageTemplates/Expressions.py
   tracker/roundup-src/roundup/cgi/actions.py
   tracker/roundup-src/roundup/cgi/apache.py
   tracker/roundup-src/roundup/cgi/cgitb.py
   tracker/roundup-src/roundup/cgi/client.py
   tracker/roundup-src/roundup/cgi/form_parser.py
   tracker/roundup-src/roundup/cgi/templating.py
   tracker/roundup-src/roundup/configuration.py
   tracker/roundup-src/roundup/instance.py
   tracker/roundup-src/roundup/mailer.py
   tracker/roundup-src/roundup/mailgw.py
   tracker/roundup-src/roundup/roundupdb.py
   tracker/roundup-src/roundup/scripts/roundup_mailgw.py
   tracker/roundup-src/roundup/scripts/roundup_server.py
   tracker/roundup-src/roundup/xmlrpc.py
   tracker/roundup-src/setup.py
   tracker/roundup-src/share/roundup/templates/classic/html/_generic.item.html
   tracker/roundup-src/share/roundup/templates/classic/html/page.html
   tracker/roundup-src/share/roundup/templates/classic/html/style.css
   tracker/roundup-src/share/roundup/templates/classic/schema.py
   tracker/roundup-src/share/roundup/templates/minimal/html/_generic.item.html
   tracker/roundup-src/share/roundup/templates/minimal/html/page.html
   tracker/roundup-src/share/roundup/templates/minimal/html/user.item.html
   tracker/roundup-src/share/roundup/templates/minimal/schema.py
   tracker/roundup-src/test/db_test_base.py
   tracker/roundup-src/test/test_anypy_hashlib.py
   tracker/roundup-src/test/test_indexer.py
   tracker/roundup-src/test/test_mailgw.py
   tracker/roundup-src/test/test_xmlrpc.py

Modified: tracker/roundup-src/CHANGES.txt
==============================================================================
--- tracker/roundup-src/CHANGES.txt	(original)
+++ tracker/roundup-src/CHANGES.txt	Fri Nov 27 12:58:04 2009
@@ -1,12 +1,45 @@
 This file contains the changes to the Roundup system over time. The entries
 are given with the most recent entry first.
 
-2009-03-?? 1.4.9 (r??)
+2009-10-09 1.4.10 (r4374)
 
 Fixes:
+- Minor update of doc/developers.txt to point to the new resources
+  on www.roundup-tracker.org (Bernhard Reiter)
+- Small CSS improvements regaring the search box (thanks Thomas Arendsan Hein)
+  (issue 2550589)
+- Indexers behaviour made more consistent regarding length of indexed words
+  and stopwords (thanks Thomas Arendsen Hein, Bernhard Reiter)(issue 2550584)
+- fixed typos in the installation instructions (thanks Thomas Arendsen Hein)
+  (issue 2550573) 
+- New config option csv_field_size: Pythons csv module (which is used
+  for export/import) has a new field size limit starting with python2.5.
+  We now issue a warning during export if the limit is too small and use
+  the csv_field_size configuration during import to set the limit for
+  the csv module.
+- Small fix for CGI-handling of XMLRPC requests for python2.4, this
+  worked only for 2.5 and beyond due to a change in the xmlrpc interface
+  in python
+- Document filter method of xmlrpc interface
+- Fix interaction of SSL and XMLRPC, now XMLRPC works with SSL
 
+2009-08-10 1.4.9 (r4346)
+
+Fixes:
 - fixed action taken in response to invalid GET request
 - fixed classic tracker template to submit POST requests when appropriate
+- fix problems with french and german locale files (issue 2550546)
+- Run each message of the mail-gateway in a separate transaction,
+  see http://thread.gmane.org/gmane.comp.bug-tracking.roundup.user/9500
+- fix problem with bounce-message if incoming mail has insufficient
+  privilege, e.g., user not existing (issue 2550534)
+- fix construction of individual messages to nosy recipents with
+  attachments (issue 2550568)
+- re-order sqlite imports to handle multiple installed versions (issue
+  2550570)
+- don't show entire history by default
+  (fixes http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=540629)
+- remove use of string exception
 
 
 2009-03-18 1.4.8 (r4209)

Modified: tracker/roundup-src/demo.py
==============================================================================
--- tracker/roundup-src/demo.py	(original)
+++ tracker/roundup-src/demo.py	Fri Nov 27 12:58:04 2009
@@ -2,7 +2,6 @@
 #
 # Copyright (c) 2003 Richard Jones (richard at mechanicalcat.net)
 #
-# $Id: demo.py,v 1.26 2007-08-28 22:37:45 jpend Exp $
 
 import errno
 import os
@@ -10,6 +9,7 @@
 import sys
 import urlparse
 from glob import glob
+import getopt
 
 from roundup import configuration
 from roundup.scripts import roundup_server
@@ -23,9 +23,10 @@
         backend:
             database backend name
         template:
-            full path to the tracker template directory
+            tracker template
 
     """
+
     from roundup import init, instance, password, backends
 
     # set up the config for this tracker
@@ -45,14 +46,15 @@
     if module.db_exists(config):
         module.db_nuke(config)
 
-    init.install(home, template)
+    template_dir = os.path.join('share', 'roundup', 'templates', template)
+    init.install(home, template_dir)
     # don't have email flying around
-    os.remove(os.path.join(home, 'detectors', 'nosyreaction.py'))
-    try:
-        os.remove(os.path.join(home, 'detectors', 'nosyreaction.pyc'))
-    except os.error, error:
-        if error.errno != errno.ENOENT:
-            raise
+    nosyreaction = os.path.join(home, 'detectors', 'nosyreaction.py')
+    if os.path.exists(nosyreaction):
+        os.remove(nosyreaction)
+    nosyreaction += 'c'
+    if os.path.exists(nosyreaction):
+        os.remove(nosyreaction)
     init.write_select_db(home, backend)
 
     # figure basic params for server
@@ -86,8 +88,13 @@
 
     # add the "demo" user
     db = tracker.open('admin')
-    db.user.create(username='demo', password=password.Password('demo'),
-        realname='Demo User', roles='User')
+    # FIXME: Move tracker-specific demo initialization into the tracker templates.
+    if (template == 'minimal'):
+        db.user.create(username='demo', password=password.Password('demo'),
+                       roles='User')
+    else:
+        db.user.create(username='demo', password=password.Password('demo'),
+                       realname='Demo User', roles='User')
     db.commit()
     db.close()
 
@@ -116,21 +123,59 @@
     sys.argv = sys.argv[:1] + ['-p', str(port), 'demo=' + home]
     roundup_server.run(success_message=success_message)
 
-def demo_main():
+
+def usage(msg = ''):
+
+    if msg: print msg
+    print 'Usage: %s [options] [nuke]'%sys.argv[0]
+    print """
+Options:
+ -h                -- print this help message
+ -t template       -- specify the tracker template to use
+ -b backend        -- specify the database backend to use
+"""
+
+
+def main():
     """Run a demo server for users to play with for instant gratification.
 
     Sets up the web service on localhost. Disables nosy lists.
     """
+
+    try:
+        opts, args = getopt.getopt(sys.argv[1:], 't:b:h')
+    except getopt.GetoptError, e:
+        usage(str(e))
+        return 1
+
     home = os.path.abspath('demo')
-    if not os.path.exists(home) or (sys.argv[-1] == 'nuke'):
-        if len(sys.argv) > 2:
-            backend = sys.argv[-2]
-        else:
-            backend = 'anydbm'
-        install_demo(home, backend, os.path.join('share', 'roundup', 'templates', 'classic'))
+    nuke = args and args[0] == 'nuke'
+    if not os.path.exists(home) or nuke:
+        backend = 'anydbm'
+        template = 'classic'
+        for opt, arg in opts:
+            if opt == '-h':
+                usage()
+                return 0
+            elif opt == '-t':
+                template = arg
+            elif opt == '-b':
+                backend = arg
+        if (len(args) > 1 or
+            (len(args) == 1 and args[0] != 'nuke')):
+            usage()
+            return 1
+
+        install_demo(home, backend, template)
+    elif opts:
+        print "Error: Arguments are not allowed when running an existing demo."
+        print "       Use the 'nuke' command to start over."
+        sys.exit(1)
+
     run_demo(home)
 
+
 if __name__ == '__main__':
-    demo_main()
+    sys.exit(main())
 
 # vim: set filetype=python sts=4 sw=4 et si :

Modified: tracker/roundup-src/doc/FAQ.txt
==============================================================================
--- tracker/roundup-src/doc/FAQ.txt	(original)
+++ tracker/roundup-src/doc/FAQ.txt	Fri Nov 27 12:58:04 2009
@@ -96,8 +96,18 @@
 How do I run Roundup through SSL (HTTPS)?
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-You should proxy through apache and use its SSL service. See the previous
-question on how to proxy through apache.
+The preferred way of using SSL is to proxy through apache and use its
+SSL service. See the previous question on how to proxy through apache.
+
+The standalone roundup-server now also has SSL support which is still
+considered experimental. For details refer to the documentation of
+roundup server, in particular to the generated configuration file
+generated with ::
+
+    roundup-server --save-config
+
+that describes the needed option in detail. With the standalone server
+now XMLRPC over SSL works, too.
 
 
 Roundup runs very slowly on my XP machine when accessed from the Internet
@@ -107,7 +117,7 @@
 performing the request. You can turn off the resolution of the names
 when it's so slow like this. To do so, edit the module
 roundup/scripts/roundup_server.py around line 77 to add the following
-to the RoundupRequestHandler class:
+to the RoundupRequestHandler class::
 
      def address_string(self):
          return self.client_address[0]
@@ -133,7 +143,8 @@
 But I just want a select/option list for ....
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-Really easy... edit ``html/issue.item``. For 'nosy', change line 53 from::
+Really easy... edit ``html/issue.item.html``. For ``nosy``, change the line
+(around line 69) from::
 
   <span tal:replace="structure context/nosy/field" />
 
@@ -141,11 +152,7 @@
 
   <span tal:replace="structure context/nosy/menu" />
 
-For 'assigned to', change line 61 from::
-
-  <td tal:content="structure context/assignedto/field">assignedto menu</td>
-
-to::
+For ``assigned to``, this is already done around line 77::
 
   <td tal:content="structure context/assignedto/menu">assignedto menu</td>
 
@@ -156,7 +163,7 @@
 
 Thats a little harder (but only a little ;^)
 
-Again, edit ``html/issue.item``. For nosy, change line 53 from:
+Again, edit ``html/issue.item``. For nosy, change line (around line 69) from::
 
   <span tal:replace="structure context/nosy/field" />
 

Modified: tracker/roundup-src/doc/acknowledgements.txt
==============================================================================
--- tracker/roundup-src/doc/acknowledgements.txt	(original)
+++ tracker/roundup-src/doc/acknowledgements.txt	Fri Nov 27 12:58:04 2009
@@ -51,7 +51,6 @@
 Engelbert Gruber,
 Bruce Guenter,
 Tamás Gulácsi,
-Thomas Arendsen Hein,
 Juergen Hermann,
 Tobias Herp,
 Uwe Hoffmann,
@@ -120,6 +119,7 @@
 Jon C. Thomason
 Mike Thompson,
 Michael Twomey,
+Joseph E. Trent,
 Karl Ulbrich,
 Martin Uzak,
 Darryl VanDorp,

Modified: tracker/roundup-src/doc/announcement.txt
==============================================================================
--- tracker/roundup-src/doc/announcement.txt	(original)
+++ tracker/roundup-src/doc/announcement.txt	Fri Nov 27 12:58:04 2009
@@ -1,30 +1,28 @@
-I'm proud to release version 1.4.8 of Roundup.
+I'm proud to release version 1.4.10 of Roundup which fixes some bugs:
 
-This release fixes some regressions:
-
-- bug introduced into hyperdb filter (issue 2550505)
-- bug introduced into CVS export and view (issue 2550529)
-- bugs introduced in the migration to the email package (issue 2550531)
-
-And adds a couple of other fixes:
-
-- handle bogus pagination values (issue 2550530)
-- fix TLS handling with some SMTP servers (issues 2484879 and 1912923)
-
-
-Though some new features made it in also:
-
-- Provide a "no selection" option in web interface selection widgets
-- Debug logging now uses the logging module rather than print
-- Allow CGI frontend to serve XMLRPC requests.
-- Added XMLRPC actions, as well as bridging CGI actions to XMLRPC actions.
-- Optimized large file serving via mod_python / sendfile().
-- Support resuming downloads for (large) files.
+- Minor update of doc/developers.txt to point to the new resources
+  on www.roundup-tracker.org (Bernhard Reiter)
+- Small CSS improvements regaring the search box (thanks Thomas Arendsan Hein)
+  (issue 2550589)
+- Indexers behaviour made more consistent regarding length of indexed words
+  and stopwords (thanks Thomas Arendsen Hein, Bernhard Reiter)(issue 2550584)
+- fixed typos in the installation instructions (thanks Thomas Arendsen Hein)
+  (issue 2550573) 
+- New config option csv_field_size: Pythons csv module (which is used
+  for export/import) has a new field size limit starting with python2.5.
+  We now issue a warning during export if the limit is too small and use
+  the csv_field_size configuration during import to set the limit for
+  the csv module.
+- Small fix for CGI-handling of XMLRPC requests for python2.4, this
+  worked only for 2.5 and beyond due to a change in the xmlrpc interface
+  in python
+- Document filter method of xmlrpc interface
+- Fix interaction of SSL and XMLRPC, now XMLRPC works with SSL
 
 If you're upgrading from an older version of Roundup you *must* follow
 the "Software Upgrade" guidelines given in the maintenance documentation.
 
-Roundup requires python 2.3 or later for correct operation.
+Roundup requires python 2.3 or later (but not 3+) for correct operation.
 
 To give Roundup a try, just download (see below), unpack and run::
 
@@ -58,9 +56,9 @@
 The system will facilitate communication among the participants by managing
 discussions and notifying interested parties when issues are edited. One of
 the major design goals for Roundup that it be simple to get going. Roundup
-is therefore usable "out of the box" with any python 2.3+ installation. It
-doesn't even need to be "installed" to be operational, though a
-disutils-based install script is provided.
+is therefore usable "out of the box" with any python 2.3+ (but not 3+)
+installation. It doesn't even need to be "installed" to be operational,
+though an install script is provided.
 
 It comes with two issue tracker templates (a classic bug/feature tracker and
 a minimal skeleton) and four database back-ends (anydbm, sqlite, mysql

Modified: tracker/roundup-src/doc/developers.txt
==============================================================================
--- tracker/roundup-src/doc/developers.txt	(original)
+++ tracker/roundup-src/doc/developers.txt	Fri Nov 27 12:58:04 2009
@@ -19,64 +19,30 @@
 
 - roundup-dev mailing list at
   http://lists.sourceforge.net/mailman/listinfo/roundup-devel
-- Sourceforge's issue trackers at
-  https://sourceforge.net/tracker/?group_id=31577
+- The issue tracker running at
+  http://issues.roundup-tracker.org/
+
+Website, wiki, issue tracker
+----------------------------
+
+1. Log into <username>,roundup at shell.sourceforge.net
+2. cd /home/groups/r/ro/roundup
+3. follow instructions in README.txt
+
 
 Small Changes
 -------------
 
-Most small changes can be submitted through the `feature tracker`_, with
+Most small changes can be submitted through the issue tracker, with
 patches attached that give context diffs of the affected source.
 
 
-CVS Access
+SVN Access
 ----------
 
-To get CVS access, contact richard at users.sourceforge.net.
-
-CVS stuff:
-
-1. to tag a release (eg. the pre-release of 0.5.0)::
-
-    cvs tag release-0-5-0-pr1
-
-1. to make a branch (eg. branching for code freeze/release)::
-
-    cvs co -d maint-0-5 -r release-0-5-0-pr1 roundup
-    cd maint-0-5 
-    cvs tag -b maint-0-5
-
-2. to check out a branch (eg. the maintenance branch for 0.5.x)::
-
-    cvs co -d maint-0-5 -r maint-0-5
-
-3. to merge changes from the maintenance branch to the trunk, in the
-   directory containing the HEAD checkout::
-
-    cvs up -j maint-0-5
-
-   though this is highly discouraged, as it generally creates a whole swag
-   of conflicts :(
-
-Standard tag names:
-
-*release-maj-min-patch[-sub]*
-  Release of the major.minor.patch release, possibly a beta or pre-release,
-  in which case *sub* will be one of "b*N*" or "pr*N*".
-*maint-maj-min*
-  Maintenance branch for the major.minor release. Patch releases are tagged in
-  this branch.
-
-Typically, release happen like this:
+See http://www.roundup-tracker.org/code.html.
+For all other questions ask on the development mailinglist.
 
-1. work progresses in the HEAD branch until milestones are met,
-2. a series of beta releases are tagged in the HEAD until the code is
-   stable enough to freeze,
-3. the pre-release is tagged in the HEAD, with the resultant code branched
-   to the maintenance branch for that release,
-4. bugs in the release are patched in the maintenance branch, and the final
-   and patch releases are tagged there, and
-5. further major work happens in the HEAD.
 
 Project Rules
 -------------
@@ -86,7 +52,7 @@
 
 - 80 column width code
 - 4-space indentations
-- All modules must have a CVS Id line near the top
+- All modules must have an Id line near the top
 
 Other project rules:
 
@@ -94,7 +60,7 @@
   where there's missing documentation) and changes to tracker configuration
   must be logged in the upgrading document.
 - subscribe to roundup-checkins to receive checkin notifications from the
-  other developers with CVS access
+  other developers with write access to the source-code repository.
 - discuss any changes with the other developers on roundup-dev. If nothing
   else, this makes sure there's no rude shocks
 - write unit tests for changes you make (where possible), and ensure that
@@ -144,8 +110,8 @@
    message translators.
 
 4. Translated Message Files are compiled into binary form (_`MO` files)
-   and stored in ``locale`` directory (but not kept in the `Roundup
-   CVS`_ repository, as they may be easily made from PO files).
+   and stored in ``locale`` directory (but not kept in the source code
+   repository, as they may be easily made from PO files).
    See `Compiling Message Catalogs`_ section.
 
 5. Roundup installer creates runtime locale structure on the file
@@ -346,8 +312,9 @@
 This means that you need both `GNU gettext`_ tools and `PO utilities`_
 to build the Message Template File yourself.
 
-Latest Message Template File is kept in `Roundup CVS`_ and distributed
-with `Roundup Source`_.  If you wish to rebuild the template yourself,
+Latest Message Template File is kept in the source code repository 
+and distributed with `Roundup Source`_.  
+If you wish to rebuild the template yourself,
 make sure that you have both ``xpot`` and ``xgettext`` installed and
 just run ``gmake`` (or ``make``, if you are on a `GNU`_ system like
 `linux`_ or `cygwin`_) in the ``locale`` directory.
@@ -451,7 +418,6 @@
     http://vim.sourceforge.net/scripts/script.php?script_id=695
 .. _PO utilities: http://po-utils.progiciels-bpi.ca/
 .. _poEdit: http://poedit.sourceforge.net/
-.. _Roundup CVS: http://sourceforge.net/cvs/?group_id=31577
 .. _Roundup Source:
 .. _Roundup source distribution:
 .. _Roundup binary distribution:
@@ -464,5 +430,4 @@
    http://dev.zope.org/Wikis/DevSite/Projects/ZPT/TALES%20Specification%201.3
 .. _vim: http://www.vim.org/
 .. _ZPTInternationalizationSupport: http://dev.zope.org/Wikis/DevSite/Projects/ComponentArchitecture/ZPTInternationalizationSupport
-.. _feature tracker: http://sourceforge.net/tracker/?group_id=31577&atid=402791
 

Modified: tracker/roundup-src/doc/features.txt
==============================================================================
--- tracker/roundup-src/doc/features.txt	(original)
+++ tracker/roundup-src/doc/features.txt	Fri Nov 27 12:58:04 2009
@@ -12,7 +12,7 @@
  - two templates included in the distribution for you to base your tracker on
  - play with the demo, customise it and then use *it* as the template for
    your production tracker
- - requires *no* additional support software - python (2.3+) is
+ - requires *no* additional support software - python (2.3+ but not 3+) is
    enough to get you going
  - easy to set up higher-performance storage backends like sqlite_,
    mysql_ and postgresql_

Modified: tracker/roundup-src/doc/installation.txt
==============================================================================
--- tracker/roundup-src/doc/installation.txt	(original)
+++ tracker/roundup-src/doc/installation.txt	Fri Nov 27 12:58:04 2009
@@ -30,8 +30,8 @@
 Prerequisites
 =============
 
-Roundup requires Python 2.3 or newer with a functioning anydbm
-module. Download the latest version from http://www.python.org/.
+Roundup requires Python 2.3 or newer (but not Python 3) with a functioning
+anydbm module. Download the latest version from http://www.python.org/.
 It is highly recommended that users install the latest patch version
 of python as these contain many fixes to serious bugs.
 
@@ -441,14 +441,14 @@
   export LD_PRELOAD
 
 Next, you have to add Roundup trackers configuration to apache config.
-Roundup apache interface uses two options specified with ``PythonOption``
-directives:
+Roundup apache interface uses the following options specified with
+``PythonOption`` directives:
 
   TrackerHome:
     defines the tracker home directory - the directory that was specified
     when you did ``roundup-admin init``.  This option is required.
 
-  TrackerLaguage:
+  TrackerLanguage:
     defines web user interface language.  mod_python applications do not
     receive OS environment variables in the same way as command-line
     programs, so the language cannot be selected by setting commonly
@@ -482,7 +482,7 @@
 interface (default).
 
 Static files from ``html`` directory are served by apache itself - this
-is quickier and generally more robust than doing that from python.
+is quicker and generally more robust than doing that from python.
 Everything else is aliased to dummy (non-existing) ``py`` file,
 which is handled by mod_python and our roundup module.
 

Modified: tracker/roundup-src/doc/upgrading.txt
==============================================================================
--- tracker/roundup-src/doc/upgrading.txt	(original)
+++ tracker/roundup-src/doc/upgrading.txt	Fri Nov 27 12:58:04 2009
@@ -16,6 +16,18 @@
 Migrating from 1.4.x to 1.4.9
 =============================
 
+Customized MailGW Class
+-----------------------
+
+If you have customized the MailGW class in your tracker: The new MailGW
+class opens the database for each message in the method handle_message
+(instance.open) instead of passing the opened database as a parameter to
+the MailGW constructor. The old handle_message has been renamed to
+_handle_message. The new method opens the database and wraps the call to
+the old method into a try/finally.
+
+Your customized MailGW class needs to mirror this behavior.
+
 Fix the "remove" button in issue files and messages lists
 ---------------------------------------------------------
 

Modified: tracker/roundup-src/doc/xmlrpc.txt
==============================================================================
--- tracker/roundup-src/doc/xmlrpc.txt	(original)
+++ tracker/roundup-src/doc/xmlrpc.txt	Fri Nov 27 12:58:04 2009
@@ -64,6 +64,13 @@
         Set the values of an existing item in the tracker as specified by
         ``designator``. The new values are specified in ``arg_1`` through
         ``arg_N``. The arguments are name=value pairs (e.g. ``status='3'``).
+
+filter  arguments: *classname, list or None, attributes*
+        
+        list can be None (requires ``allow_none=True`` when
+        instantiating the ServerProxy) to indicate search for all values,
+        or a list of ids. The attributes are given as a dictionary of
+        name value pairs to search for.
 ======= ====================================================================
 
 sample python client
@@ -71,7 +78,7 @@
 ::
 
         >>> import xmlrpclib
-        >>> roundup_server = xmlrpclib.ServerProxy('http://username:password@localhost:8000')
+        >>> roundup_server = xmlrpclib.ServerProxy('http://username:password@localhost:8000', allow_none=True)
         >>> roundup_server.list('user')
         ['admin', 'anonymous', 'demo']
         >>> roundup_server.list('issue', 'id')
@@ -85,4 +92,11 @@
         {'status' : '3' }
         >>> roundup_server.create('issue', "title='another bug'", "status=2")
         '2'
-
+        >>> roundup_server.filter('user',None,{'username':'adm'})
+        ['1']
+        >>> roundup_server.filter('user',['1','2'],{'username':'adm'})
+        ['1']
+        >>> roundup_server.filter('user',['2'],{'username':'adm'})
+        []
+        >>> roundup_server.filter('user',[],{'username':'adm'})
+        []

Added: tracker/roundup-src/locale/de.mo
==============================================================================
Binary file. No diff available.

Modified: tracker/roundup-src/locale/de.po
==============================================================================
--- tracker/roundup-src/locale/de.po	(original)
+++ tracker/roundup-src/locale/de.po	Fri Nov 27 12:58:04 2009
@@ -10,7 +10,7 @@
 "Project-Id-Version: Roundup 1.4.6\n"
 "Report-Msgid-Bugs-To: roundup-devel at lists.sourceforge.net\n"
 "POT-Creation-Date: 2009-03-12 11:58+0200\n"
-"PO-Revision-Date: 2009-03-12 18:05Westeuropäische Normalzeit\n"
+"PO-Revision-Date: 2009-03-12 18:05+0200\n"
 "Last-Translator: Tobias Herp <tobias.herp at gmx.de>\n"
 "Language-Team: German Translators <roundup-devel at lists.sourceforge.net>\n"
 "MIME-Version: 1.0\n"

Modified: tracker/roundup-src/locale/fr.po
==============================================================================
--- tracker/roundup-src/locale/fr.po	(original)
+++ tracker/roundup-src/locale/fr.po	Fri Nov 27 12:58:04 2009
@@ -16,7 +16,7 @@
 "Last-Translator: Stéphane Raimbault <stephane.raimbault at gmail.com>\n"
 "Language-Team: GNOME French Team <gnomefr at traduc.org>\n"
 "MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=utf-8\n"
+"Content-Type: text/plain; charset=ISO-8859-1\n"
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=n>1;\n"
 

Modified: tracker/roundup-src/roundup/__init__.py
==============================================================================
--- tracker/roundup-src/roundup/__init__.py	(original)
+++ tracker/roundup-src/roundup/__init__.py	Fri Nov 27 12:58:04 2009
@@ -68,6 +68,6 @@
 '''
 __docformat__ = 'restructuredtext'
 
-__version__ = '1.4.8'
+__version__ = '1.4.10'
 
 # vim: set filetype=python ts=4 sw=4 et si

Modified: tracker/roundup-src/roundup/actions.py
==============================================================================
--- tracker/roundup-src/roundup/actions.py	(original)
+++ tracker/roundup-src/roundup/actions.py	Fri Nov 27 12:58:04 2009
@@ -64,5 +64,5 @@
         if not self.db.security.hasPermission('Edit', self.db.getuid(),
                                               classname=classname, itemid=itemid):
             raise Unauthorised(self._('You do not have permission to '
-                                      '%(action)s the %(classname)s class.')%info)
+                                      'retire the %(classname)s class.')%classname)
             

Modified: tracker/roundup-src/roundup/admin.py
==============================================================================
--- tracker/roundup-src/roundup/admin.py	(original)
+++ tracker/roundup-src/roundup/admin.py	Fri Nov 27 12:58:04 2009
@@ -1099,6 +1099,9 @@
         if not os.path.exists(dir):
             os.makedirs(dir)
 
+        # maximum csv field length exceeding configured size?
+        max_len = self.db.config.CSV_FIELD_SIZE
+
         # do all the classes specified
         for classname in classes:
             cl = self.get_class(classname)
@@ -1121,7 +1124,18 @@
                 if self.verbose:
                     sys.stdout.write('\rExporting %s - %s'%(classname, nodeid))
                     sys.stdout.flush()
-                writer.writerow(cl.export_list(propnames, nodeid))
+                node = cl.getnode(nodeid)
+                exp = cl.export_list(propnames, nodeid)
+                lensum = sum ([len (repr(node[p])) for p in propnames])
+                # for a safe upper bound of field length we add
+                # difference between CSV len and sum of all field lengths
+                d = sum ([len(x) for x in exp]) - lensum
+                assert (d > 0)
+                for p in propnames:
+                    ll = len(repr(node[p])) + d
+                    if ll > max_len:
+                        max_len = ll
+                writer.writerow(exp)
                 if export_files and hasattr(cl, 'export_files'):
                     cl.export_files(dir, nodeid)
 
@@ -1136,6 +1150,9 @@
             journals = csv.writer(jf, colon_separated)
             map(journals.writerow, cl.export_journals())
             jf.close()
+        if max_len > self.db.config.CSV_FIELD_SIZE:
+            print >> sys.stderr, \
+                "Warning: config csv_field_size should be at least %s"%max_len
         return 0
 
     def do_exporttables(self, args):
@@ -1177,6 +1194,9 @@
             raise UsageError, _('Not enough arguments supplied')
         from roundup import hyperdb
 
+        if hasattr (csv, 'field_size_limit'):
+            csv.field_size_limit(self.db.config.CSV_FIELD_SIZE)
+
         # directory to import from
         dir = args[0]
 
@@ -1212,7 +1232,7 @@
                 if hasattr(cl, 'import_files'):
                     cl.import_files(dir, nodeid)
                 maxid = max(maxid, int(nodeid))
-            print
+            print >> sys.stdout
             f.close()
 
             # import the journals
@@ -1222,7 +1242,7 @@
             f.close()
 
             # set the id counter
-            print 'setting', classname, maxid+1
+            print >> sys.stdout, 'setting', classname, maxid+1
             self.db.setid(classname, str(maxid+1))
 
         self.db_uncommitted = True

Modified: tracker/roundup-src/roundup/backends/__init__.py
==============================================================================
--- tracker/roundup-src/roundup/backends/__init__.py	(original)
+++ tracker/roundup-src/roundup/backends/__init__.py	Fri Nov 27 12:58:04 2009
@@ -31,7 +31,7 @@
     'mysql': ('MySQLdb',),
     'postgresql': ('psycopg',),
     'tsearch2': ('psycopg',),
-    'sqlite': ('pysqlite', 'pysqlite2', 'sqlite3', '_sqlite3'),
+    'sqlite': ('pysqlite', 'pysqlite2', 'sqlite3', '_sqlite3', 'sqlite'),
 }
 
 def get_backend(name):

Modified: tracker/roundup-src/roundup/backends/back_postgresql.py
==============================================================================
--- tracker/roundup-src/roundup/backends/back_postgresql.py	(original)
+++ tracker/roundup-src/roundup/backends/back_postgresql.py	Fri Nov 27 12:58:04 2009
@@ -9,7 +9,7 @@
 '''Postgresql backend via psycopg for Roundup.'''
 __docformat__ = 'restructuredtext'
 
-import os, shutil, popen2, time
+import os, shutil, time
 try:
     import psycopg
     from psycopg import QuotedString

Modified: tracker/roundup-src/roundup/backends/back_sqlite.py
==============================================================================
--- tracker/roundup-src/roundup/backends/back_sqlite.py	(original)
+++ tracker/roundup-src/roundup/backends/back_sqlite.py	Fri Nov 27 12:58:04 2009
@@ -14,8 +14,8 @@
 from roundup.backends import rdbms_common
 sqlite_version = None
 try:
-    import sqlite
-    sqlite_version = 1
+    import sqlite3 as sqlite
+    sqlite_version = 3
 except ImportError:
     try:
         from pysqlite2 import dbapi2 as sqlite
@@ -24,8 +24,8 @@
                 '- %s found'%sqlite.version)
         sqlite_version = 2
     except ImportError:
-        import sqlite3 as sqlite
-        sqlite_version = 3
+        import sqlite
+        sqlite_version = 1
 
 def db_exists(config):
     return os.path.exists(os.path.join(config.DATABASE, 'db'))
@@ -109,10 +109,10 @@
             conn = sqlite.connect(db, timeout=30)
             conn.row_factory = sqlite.Row
 
-        # sqlite3 wants us to store Unicode in the db but that's not what's
-        # been done historically and it's definitely not what the other
-        # backends do, so we'll stick with UTF-8
-        if sqlite_version == 3:
+        # pysqlite2 / sqlite3 want us to store Unicode in the db but
+        # that's not what's been done historically and it's definitely
+        # not what the other backends do, so we'll stick with UTF-8
+        if sqlite_version in (2, 3):
             conn.text_factory = str
 
         cursor = conn.cursor()

Modified: tracker/roundup-src/roundup/backends/indexer_common.py
==============================================================================
--- tracker/roundup-src/roundup/backends/indexer_common.py	(original)
+++ tracker/roundup-src/roundup/backends/indexer_common.py	Fri Nov 27 12:58:04 2009
@@ -22,6 +22,10 @@
         self.stopwords = set(STOPWORDS)
         for word in db.config[('main', 'indexer_stopwords')]:
             self.stopwords.add(word)
+        # Do not index anything longer than 25 characters since that'll be
+        # gibberish (encoded text or somesuch) or shorter than 2 characters
+        self.minlength = 2
+        self.maxlength = 25
 
     def is_stopword(self, word):
         return word in self.stopwords

Modified: tracker/roundup-src/roundup/backends/indexer_dbm.py
==============================================================================
--- tracker/roundup-src/roundup/backends/indexer_dbm.py	(original)
+++ tracker/roundup-src/roundup/backends/indexer_dbm.py	Fri Nov 27 12:58:04 2009
@@ -135,14 +135,12 @@
         # case insensitive
         text = str(text).upper()
 
-        # Split the raw text, losing anything longer than 25 characters
-        # since that'll be gibberish (encoded text or somesuch) or shorter
-        # than 3 characters since those short words appear all over the
-        # place
-        return re.findall(r'\b\w{2,25}\b', text)
+        # Split the raw text
+        return re.findall(r'\b\w{%d,%d}\b' % (self.minlength, self.maxlength),
+                          text)
 
-    # we override this to ignore not 2 < word < 25 and also to fix a bug -
-    # the (fail) case.
+    # we override this to ignore too short and too long words
+    # and also to fix a bug - the (fail) case.
     def find(self, wordlist):
         '''Locate files that match ALL the words in wordlist
         '''
@@ -152,10 +150,12 @@
         entries = {}
         hits = None
         for word in wordlist:
-            if not 2 < len(word) < 25:
+            if not self.minlength <= len(word) <= self.maxlength:
                 # word outside the bounds of what we index - ignore
                 continue
             word = word.upper()
+            if self.is_stopword(word):
+                continue
             entry = self.words.get(word)    # For each word, get index
             entries[word] = entry           #   of matching files
             if not entry:                   # Nothing for this one word (fail)

Modified: tracker/roundup-src/roundup/backends/indexer_rdbms.py
==============================================================================
--- tracker/roundup-src/roundup/backends/indexer_rdbms.py	(original)
+++ tracker/roundup-src/roundup/backends/indexer_rdbms.py	Fri Nov 27 12:58:04 2009
@@ -66,11 +66,11 @@
         # ok, find all the unique words in the text
         text = unicode(text, "utf-8", "replace").upper()
         wordlist = [w.encode("utf-8")
-            for w in re.findall(r'(?u)\b\w{2,25}\b', text)]
+            for w in re.findall(r'(?u)\b\w{%d,%d}\b'
+                                % (self.minlength, self.maxlength), text)]
         words = set()
         for word in wordlist:
             if self.is_stopword(word): continue
-            if len(word) > 25: continue
             words.add(word)
 
         # for each word, add an entry in the db
@@ -86,7 +86,9 @@
         if not wordlist:
             return []
 
-        l = [word.upper() for word in wordlist if 26 > len(word) > 2]
+        l = [word.upper() for word in wordlist
+             if self.minlength <= len(word) <= self.maxlength]
+        l = [word for word in l if not self.is_stopword(word)]
 
         if not l:
             return []

Modified: tracker/roundup-src/roundup/backends/indexer_xapian.py
==============================================================================
--- tracker/roundup-src/roundup/backends/indexer_xapian.py	(original)
+++ tracker/roundup-src/roundup/backends/indexer_xapian.py	Fri Nov 27 12:58:04 2009
@@ -88,7 +88,9 @@
         doc.set_data(identifier)
         doc.add_posting(identifier, 0)
 
-        for match in re.finditer(r'\b\w{2,25}\b', text.upper()):
+        for match in re.finditer(r'\b\w{%d,%d}\b'
+                                 % (self.minlength, self.maxlength),
+                                 text.upper()):
             word = match.group(0)
             if self.is_stopword(word):
                 continue
@@ -112,8 +114,10 @@
         enquire = xapian.Enquire(database)
         stemmer = xapian.Stem("english")
         terms = []
-        for term in [word.upper() for word in wordlist if 26 > len(word) > 2]:
-            terms.append(stemmer(term.upper()))
+        for term in [word.upper() for word in wordlist
+                          if self.minlength <= len(word) <= self.maxlength]:
+            if not self.is_stopword(term):
+                terms.append(stemmer(term))
         query = xapian.Query(xapian.Query.OP_AND, terms)
 
         enquire.set_query(query)

Modified: tracker/roundup-src/roundup/cgi/PageTemplates/Expressions.py
==============================================================================
--- tracker/roundup-src/roundup/cgi/PageTemplates/Expressions.py	(original)
+++ tracker/roundup-src/roundup/cgi/PageTemplates/Expressions.py	Fri Nov 27 12:58:04 2009
@@ -53,7 +53,8 @@
 try:
     from zExceptions import Unauthorized
 except ImportError:
-    Unauthorized = "Unauthorized"
+    class Unauthorized(Exception):
+        pass
 
 def acquisition_security_filter(orig, inst, name, v, real_validate):
     if real_validate(orig, inst, name, v):

Modified: tracker/roundup-src/roundup/cgi/actions.py
==============================================================================
--- tracker/roundup-src/roundup/cgi/actions.py	(original)
+++ tracker/roundup-src/roundup/cgi/actions.py	Fri Nov 27 12:58:04 2009
@@ -544,9 +544,25 @@
         Base behaviour is to check the user can edit this class. No additional
         property checks are made.
         """
+
         if not classname :
             classname = self.client.classname
-        return self.hasPermission('Create', classname=classname)
+        
+        if not self.hasPermission('Create', classname=classname):
+            return 0
+
+        # Check Edit permission for each property, to avoid being able
+        # to set restricted ones on new item creation
+        for key in props:
+            if not self.hasPermission('Edit', classname=classname,
+                                      property=key):
+                # We restrict by default and special-case allowed properties
+                if key == 'date' or key == 'content':
+                    continue
+                elif key == 'author' and props[key] == self.userid:
+                    continue
+                return 0
+        return 1
 
 class EditItemAction(EditCommon):
     def lastUserActivity(self):
@@ -648,11 +664,6 @@
                 % str(message))
             return
 
-        # guard against new user creation that would bypass security checks
-        for key in props:
-            if 'user' in key:
-                return
-
         # handle the props - edit or create
         try:
             # when it hits the None element, it'll set self.nodeid
@@ -814,7 +825,7 @@
 
 class RegisterAction(RegoCommon, EditCommon):
     name = 'register'
-    permissionType = 'Create'
+    permissionType = 'Register'
 
     def handle(self):
         """Attempt to create a new user based on the contents of the form

Modified: tracker/roundup-src/roundup/cgi/apache.py
==============================================================================
--- tracker/roundup-src/roundup/cgi/apache.py	(original)
+++ tracker/roundup-src/roundup/cgi/apache.py	Fri Nov 27 12:58:04 2009
@@ -10,21 +10,10 @@
 # This module operates with only one tracker
 # and must be placed in the tracker directory.
 #
-# History (most recent first):
-# 11-jul-2004 [als] added 'TrackerLanguage' option;
-#                   pass message translator to the tracker client instance
-# 04-jul-2004 [als] tracker lookup moved from module global to request handler;
-#                   use PythonOption TrackerHome (configured in apache)
-#                   to open the tracker
-# 06-may-2004 [als] use cgi.FieldStorage from Python library
-#                   instead of mod_python FieldStorage
-# 29-apr-2004 [als] created
-
-__version__ = "$Revision: 1.6 $"[11:-2]
-__date__ = "$Date: 2006-11-09 00:36:21 $"[7:-2]
 
 import cgi
 import os
+import threading
 
 from mod_python import apache
 
@@ -83,6 +72,15 @@
 
         return self._req.sendfile(filename, offset, len)
 
+__tracker_cache = {}
+"""A cache of optimized tracker instances.
+ 
+The keys are strings giving the directories containing the trackers.
+The values are tracker instances."""
+
+__tracker_cache_lock = threading.Lock()
+"""A lock used to guard access to the cache."""
+
 
 def handler(req):
     """HTTP request handler"""
@@ -94,12 +92,31 @@
         _timing = ""
     _debug = _options.get("TrackerDebug", "no")
     _debug = _debug.lower() not in ("no", "false")
-    if not (_home and os.path.isdir(_home)):
-        apache.log_error(
-            "PythonOption TrackerHome missing or invalid for %(uri)s"
-            % {'uri': req.uri})
-        return apache.HTTP_INTERNAL_SERVER_ERROR
-    _tracker = roundup.instance.open(_home, not _debug)
+
+    # We do not need to take a lock here (the fast path) because reads
+    # from dictionaries are atomic.
+    if not _debug and _home in __tracker_cache:
+        _tracker = __tracker_cache[_home]
+    else:
+        if not (_home and os.path.isdir(_home)):
+            apache.log_error(
+                "PythonOption TrackerHome missing or invalid for %(uri)s"
+                % {'uri': req.uri})
+            return apache.HTTP_INTERNAL_SERVER_ERROR
+        if _debug:
+            _tracker = roundup.instance.open(_home, optimize=0)
+        else:
+            __tracker_cache_lock.acquire()
+            try:
+                # The tracker may have been added while we were acquiring
+                # the lock.
+                if _home in __tracker_cache:
+                    _tracker = __tracker_cache[home]
+                else:
+                    _tracker = roundup.instance.open(_home, optimize=1)
+                    __tracker_cache[_home] = _tracker
+            finally:
+                __tracker_cache_lock.release()
     # create environment
     # Note: cookies are read from HTTP variables, so we need all HTTP vars
     req.add_common_vars()

Modified: tracker/roundup-src/roundup/cgi/cgitb.py
==============================================================================
--- tracker/roundup-src/roundup/cgi/cgitb.py	(original)
+++ tracker/roundup-src/roundup/cgi/cgitb.py	Fri Nov 27 12:58:04 2009
@@ -37,7 +37,10 @@
 
 def niceDict(indent, dict):
     l = []
-    for k,v in dict.items():
+    keys = dict.keys()
+    keys.sort()
+    for k in keys:
+        v = dict[k]
         l.append('<tr><td><strong>%s</strong></td><td>%s</td></tr>'%(k,
             cgi.escape(repr(v))))
     return '\n'.join(l)

Modified: tracker/roundup-src/roundup/cgi/client.py
==============================================================================
--- tracker/roundup-src/roundup/cgi/client.py	(original)
+++ tracker/roundup-src/roundup/cgi/client.py	Fri Nov 27 12:58:04 2009
@@ -387,7 +387,6 @@
                                            self.translator,
                                            allow_none=True)
         output = handler.dispatch(input)
-        self.db.commit()
 
         self.setHeader("Content-Type", "text/xml")
         self.setHeader("Content-Length", str(len(output)))
@@ -490,13 +489,23 @@
                 self.additional_headers['Location'] = str(url)
                 self.response_code = 302
             self.write_html('Redirecting to <a href="%s">%s</a>'%(url, url))
+        except LoginError, message:
+            # The user tried to log in, but did not provide a valid
+            # username and password.  If we support HTTP
+            # authorization, send back a response that will cause the
+            # browser to prompt the user again.
+            if self.instance.config.WEB_HTTP_AUTH:
+                self.response_code = httplib.UNAUTHORIZED
+                realm = self.instance.config.TRACKER_NAME
+                self.setHeader("WWW-Authenticate",
+                               "Basic realm=\"%s\"" % realm)
+            else:
+                self.response_code = httplib.FORBIDDEN
+            self.renderFrontPage(message)
         except Unauthorised, message:
             # users may always see the front page
             self.response_code = 403
-            self.classname = self.nodeid = None
-            self.template = ''
-            self.error_message.append(message)
-            self.write_html(self.renderContext())
+            self.renderFrontPage(message)
         except NotModified:
             # send the 304 response
             self.response_code = 304
@@ -516,11 +525,22 @@
             self.error_message.append(self._('Form Error: ') + str(e))
             self.write_html(self.renderContext())
         except:
-            if self.instance.config.WEB_DEBUG:
-                self.write_html(cgitb.html(i18n=self.translator))
+            # Something has gone badly wrong.  Therefore, we should
+            # make sure that the response code indicates failure.
+            if self.response_code == httplib.OK:
+                self.response_code = httplib.INTERNAL_SERVER_ERROR
+            # Help the administrator work out what went wrong.
+            html = ("<h1>Traceback</h1>"
+                    + cgitb.html(i18n=self.translator)
+                    + ("<h1>Environment Variables</h1><table>%s</table>"
+                       % cgitb.niceDict("", self.env)))
+            if not self.instance.config.WEB_DEBUG:
+                exc_info = sys.exc_info()
+                subject = "Error: %s" % exc_info[1]
+                self.send_html_to_admin(subject, html)
+                self.write_html(self._(error_message))
             else:
-                self.mailer.exception_message(self.exception_data())
-                return self.write_html(self._(error_message))
+                self.write_html(html)
 
     def exception_data(self):
         result = ''
@@ -700,7 +720,7 @@
                         login.verifyLogin(username, password)
                     except LoginError, err:
                         self.make_user_anonymous()
-                        raise Unauthorised, err
+                        raise
                     user = username
 
         # if user was not set by http authorization, try session lookup
@@ -755,6 +775,10 @@
             else:
                 self.db.close()
                 self.db = self.instance.open(username)
+                # The old session API refers to the closed database;
+                # we can no longer use it.
+                self.session_api = Session(self)
+ 
 
     def determine_context(self, dre=re.compile(r'([^\d]+)0*(\d+)')):
         """Determine the context of this page from the URL:
@@ -873,7 +897,12 @@
             raise NotFound, str(designator)
         classname, nodeid = m.group(1), m.group(2)
 
-        klass = self.db.getclass(classname)
+        try:
+            klass = self.db.getclass(classname)
+        except KeyError:
+            # The classname was not valid.
+            raise NotFound, str(designator)
+            
 
         # make sure we have the appropriate properties
         props = klass.getprops()
@@ -980,6 +1009,25 @@
             self.additional_headers['Content-Length'] = str(len(content))
             self.write(content)
 
+    def send_html_to_admin(self, subject, content):
+
+        to = [self.mailer.config.ADMIN_EMAIL]
+        message = self.mailer.get_standard_message(to, subject)
+        # delete existing content-type headers
+        del message['Content-type']
+        message['Content-type'] = 'text/html; charset=utf-8'
+        message.set_payload(content)
+        encode_quopri(message)
+        self.mailer.smtp_send(to, str(message))
+    
+    def renderFrontPage(self, message):
+        """Return the front page of the tracker."""
+    
+        self.classname = self.nodeid = None
+        self.template = ''
+        self.error_message.append(message)
+        self.write_html(self.renderContext())
+
     def renderContext(self):
         """ Return a PageTemplate for the named page
         """
@@ -1026,16 +1074,8 @@
             try:
                 # If possible, send the HTML page template traceback
                 # to the administrator.
-                to = [self.mailer.config.ADMIN_EMAIL]
                 subject = "Templating Error: %s" % exc_info[1]
-                content = cgitb.pt_html()
-                message = self.mailer.get_standard_message(to, subject)
-                # delete existing content-type headers
-                del message['Content-type']
-                message['Content-type'] = 'text/html; charset=utf-8'
-                message.set_payload(content)
-                encode_quopri(message)
-                self.mailer.smtp_send(to, str(message))
+                self.send_html_to_admin(subject, cgitb.pt_html())
                 # Now report the error to the user.
                 return self._(self.error_message)
             except:
@@ -1361,7 +1401,7 @@
             # RFC 2616 14.13: Content-Length
             #
             # Tell the client how much data we are providing.
-            self.setHeader("Content-Length", length)
+            self.setHeader("Content-Length", str(length))
             # Send the HTTP header.
             self.header()
         # If the client doesn't actually want the body, or if we are

Modified: tracker/roundup-src/roundup/cgi/form_parser.py
==============================================================================
--- tracker/roundup-src/roundup/cgi/form_parser.py	(original)
+++ tracker/roundup-src/roundup/cgi/form_parser.py	Fri Nov 27 12:58:04 2009
@@ -427,7 +427,7 @@
                     value = existing
                     # Sort the value in the same order used by
                     # Multilink.from_raw.
-                    value.sort(key = lambda x: int(x))
+                    value.sort(lambda x, y: cmp(int(x),int(y)))
 
             elif value == '':
                 # other types should be None'd if there's no value
@@ -491,7 +491,7 @@
                 # The canonical order (given in Multilink.from_raw) is
                 # by the numeric value of the IDs.
                 if isinstance(proptype, hyperdb.Multilink):
-                    existing.sort(key = lambda x: int(x))
+                    existing.sort(lambda x, y: cmp(int(x),int(y)))
 
                 # "missing" existing values may not be None
                 if not existing:

Modified: tracker/roundup-src/roundup/cgi/templating.py
==============================================================================
--- tracker/roundup-src/roundup/cgi/templating.py	(original)
+++ tracker/roundup-src/roundup/cgi/templating.py	Fri Nov 27 12:58:04 2009
@@ -183,12 +183,28 @@
             return self.templates[src]
 
         # compile the template
-        self.templates[src] = pt = RoundupPageTemplate()
+        pt = RoundupPageTemplate()
         # use pt_edit so we can pass the content_type guess too
         content_type = mimetypes.guess_type(filename)[0] or 'text/html'
         pt.pt_edit(open(src).read(), content_type)
         pt.id = filename
         pt.mtime = stime
+        # Add it to the cache.  We cannot do this until the template
+        # is fully initialized, as we could otherwise have a race
+        # condition when running with multiple threads:
+        #
+        # 1. Thread A notices the template is not in the cache,
+        #    adds it, but has not yet set "mtime".
+        #
+        # 2. Thread B notices the template is in the cache, checks
+        #    "mtime" (above) and crashes.
+        #
+        # Since Python dictionary access is atomic, as long as we
+        # insert "pt" only after it is fully initialized, we avoid
+        # this race condition.  It's possible that two separate
+        # threads will both do the work of initializing the template,
+        # but the risk of wasted work is offset by avoiding a lock.
+        self.templates[src] = pt
         return pt
 
     def __getitem__(self, name):
@@ -420,17 +436,19 @@
         except KeyError:
             pass
 
+def cgi_escape_attrs(**attrs):
+    return ' '.join(['%s="%s"'%(k,cgi.escape(str(v), True))
+        for k,v in attrs.items()])
+
 def input_html4(**attrs):
     """Generate an 'input' (html4) element with given attributes"""
     _set_input_default_args(attrs)
-    return '<input %s>'%' '.join(['%s="%s"'%(k,cgi.escape(str(v), True))
-        for k,v in attrs.items()])
+    return '<input %s>'%cgi_escape_attrs(**attrs)
 
 def input_xhtml(**attrs):
     """Generate an 'input' (xhtml) element with given attributes"""
     _set_input_default_args(attrs)
-    return '<input %s/>'%' '.join(['%s="%s"'%(k,cgi.escape(str(v), True))
-        for k,v in attrs.items()])
+    return '<input %s/>'%cgi_escape_attrs(**attrs)
 
 class HTMLInputMixin:
     """ requires a _client property """
@@ -880,7 +898,8 @@
         # XXX do this
         return []
 
-    def history(self, direction='descending', dre=re.compile('^\d+$')):
+    def history(self, direction='descending', dre=re.compile('^\d+$'),
+            limit=None):
         if not self.is_view_ok():
             return self._('[hidden]')
 
@@ -912,6 +931,10 @@
         history.sort()
         history.reverse()
 
+        # restrict the volume
+        if limit:
+            history = history[:limit]
+
         timezone = self._db.getUserTimezone()
         l = []
         comments = {}
@@ -1268,7 +1291,9 @@
             return self._db.security.hasPermission('Edit', self._client.userid,
                 self._classname, self._name, self._nodeid)
         return self._db.security.hasPermission('Create', self._client.userid,
-            self._classname, self._name)
+            self._classname, self._name) or \
+            self._db.security.hasPermission('Register', self._client.userid,
+                                            self._classname, self._name)
 
     def is_view_ok(self):
         """ Is the user allowed to View the current class?
@@ -1457,8 +1482,7 @@
 
             value = '&quot;'.join(value.split('"'))
         name = self._formname
-        passthrough_args = ' '.join(['%s="%s"' % (k, cgi.escape(str(v), True))
-            for k,v in kwargs.items()])
+        passthrough_args = cgi_escape_attrs(**kwargs)
         return ('<textarea %(passthrough_args)s name="%(name)s" id="%(name)s"'
                 ' rows="%(rows)s" cols="%(cols)s">'
                  '%(value)s</textarea>') % locals()
@@ -1496,7 +1520,7 @@
             return ''
         return self._('*encrypted*')
 
-    def field(self, size=30):
+    def field(self, size=30, **kwargs):
         """ Render a form edit field for the property.
 
             If not editable, just display the value via plain().
@@ -1504,7 +1528,8 @@
         if not self.is_edit_ok():
             return self.plain(escape=1)
 
-        return self.input(type="password", name=self._formname, size=size)
+        return self.input(type="password", name=self._formname, size=size,
+                          **kwargs)
 
     def confirm(self, size=30):
         """ Render a second form edit field for the property, used for
@@ -1533,7 +1558,7 @@
 
         return str(self._value)
 
-    def field(self, size=30):
+    def field(self, size=30, **kwargs):
         """ Render a form edit field for the property.
 
             If not editable, just display the value via plain().
@@ -1545,7 +1570,8 @@
         if value is None:
             value = ''
 
-        return self.input(name=self._formname, value=value, size=size)
+        return self.input(name=self._formname, value=value, size=size,
+                          **kwargs)
 
     def __int__(self):
         """ Return an int of me
@@ -1569,7 +1595,7 @@
             return ''
         return self._value and self._("Yes") or self._("No")
 
-    def field(self):
+    def field(self, **kwargs):
         """ Render a form edit field for the property
 
             If not editable, just display the value via plain().
@@ -1585,15 +1611,17 @@
         checked = value and "checked" or ""
         if value:
             s = self.input(type="radio", name=self._formname, value="yes",
-                checked="checked")
+                checked="checked", **kwargs)
             s += self._('Yes')
-            s +=self.input(type="radio", name=self._formname, value="no")
+            s +=self.input(type="radio", name=self._formname,  value="no",
+                           **kwargs)
             s += self._('No')
         else:
-            s = self.input(type="radio", name=self._formname, value="yes")
+            s = self.input(type="radio", name=self._formname,  value="yes",
+                           **kwargs)
             s += self._('Yes')
             s +=self.input(type="radio", name=self._formname, value="no",
-                checked="checked")
+                checked="checked", **kwargs)
             s += self._('No')
         return s
 
@@ -1651,7 +1679,8 @@
         return DateHTMLProperty(self._client, self._classname, self._nodeid,
             self._prop, self._formname, ret)
 
-    def field(self, size=30, default=None, format=_marker, popcal=True):
+    def field(self, size=30, default=None, format=_marker, popcal=True,
+              **kwargs):
         """Render a form edit field for the property
 
         If not editable, just display the value via plain().
@@ -1686,7 +1715,8 @@
         elif isinstance(value, str) or isinstance(value, unicode):
             # most likely erroneous input to be passed back to user
             if isinstance(value, unicode): value = value.encode('utf8')
-            return self.input(name=self._formname, value=value, size=size)
+            return self.input(name=self._formname, value=value, size=size,
+                              **kwargs)
         else:
             raw_value = value
 
@@ -1706,7 +1736,8 @@
             if format is not self._marker:
                 value = value.pretty(format)
 
-        s = self.input(name=self._formname, value=value, size=size)
+        s = self.input(name=self._formname, value=value, size=size,
+                       **kwargs)
         if popcal:
             s += self.popcal()
         return s
@@ -1801,7 +1832,7 @@
 
         return self._value.pretty()
 
-    def field(self, size=30):
+    def field(self, size=30, **kwargs):
         """ Render a form edit field for the property
 
             If not editable, just display the value via plain().
@@ -1813,7 +1844,8 @@
         if value is None:
             value = ''
 
-        return self.input(name=self._formname, value=value, size=size)
+        return self.input(name=self._formname, value=value, size=size,
+                          **kwargs)
 
 class LinkHTMLProperty(HTMLProperty):
     """ Link HTMLProperty
@@ -1866,7 +1898,7 @@
             value = cgi.escape(value)
         return value
 
-    def field(self, showid=0, size=None):
+    def field(self, showid=0, size=None, **kwargs):
         """ Render a form edit field for the property
 
             If not editable, just display the value via plain().
@@ -1884,10 +1916,11 @@
                 value = linkcl.get(self._value, k)
             else:
                 value = self._value
-        return self.input(name=self._formname, value=value, size=size)
+        return self.input(name=self._formname, value=value, size=size,
+                          **kwargs)
 
     def menu(self, size=None, height=None, showid=0, additional=[], value=None,
-            sort_on=None, **conditions):
+             sort_on=None, html_kwargs = {}, **conditions):
         """ Render a form select list for this property
 
             "size" is used to limit the length of the list labels
@@ -1920,7 +1953,8 @@
             value = None
 
         linkcl = self._db.getclass(self._prop.classname)
-        l = ['<select name="%s">'%self._formname]
+        l = ['<select %s>'%cgi_escape_attrs(name = self._formname,
+                                            **html_kwargs)]
         k = linkcl.labelprop(1)
         s = ''
         if value is None:
@@ -2086,7 +2120,7 @@
             value = cgi.escape(value)
         return value
 
-    def field(self, size=30, showid=0):
+    def field(self, size=30, showid=0, **kwargs):
         """ Render a form edit field for the property
 
             If not editable, just display the value via plain().
@@ -2103,10 +2137,11 @@
             k = linkcl.labelprop(1)
             value = lookupKeys(linkcl, k, value)
         value = ','.join(value)
-        return self.input(name=self._formname, size=size, value=value)
+        return self.input(name=self._formname, size=size, value=value,
+                          **kwargs)
 
     def menu(self, size=None, height=None, showid=0, additional=[],
-             value=None, sort_on=None, **conditions):
+             value=None, sort_on=None, html_kwargs = {}, **conditions):
         """ Render a form <select> list for this property.
 
             "size" is used to limit the length of the list labels
@@ -2159,7 +2194,9 @@
                 # The "no selection" option.
                 height += 1
             height = min(height, 7)
-        l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
+        l = ['<select multiple %s>'%cgi_escape_attrs(name = self._formname,
+                                                     size = height,
+                                                     **html_kwargs)]
         k = linkcl.labelprop(1)
 
         if value:

Modified: tracker/roundup-src/roundup/configuration.py
==============================================================================
--- tracker/roundup-src/roundup/configuration.py	(original)
+++ tracker/roundup-src/roundup/configuration.py	Fri Nov 27 12:58:04 2009
@@ -530,6 +530,13 @@
             "stop-words (eg. A,AND,ARE,AS,AT,BE,BUT,BY, ...)"),
         (OctalNumberOption, "umask", "02",
             "Defines the file creation mode mask."),
+        (IntegerNumberOption, 'csv_field_size', '131072',
+            "Maximum size of a csv-field during import. Roundups export\n"
+            "format is a csv (comma separated values) variant. The csv\n"
+            "reader has a limit on the size of individual fields\n"
+            "starting with python 2.5. Set this to a higher value if you\n"
+            "get the error 'Error: field larger than field limit' during\n"
+            "import."),
     )),
     ("tracker", (
         (Option, "name", "Roundup issue tracker",

Modified: tracker/roundup-src/roundup/instance.py
==============================================================================
--- tracker/roundup-src/roundup/instance.py	(original)
+++ tracker/roundup-src/roundup/instance.py	Fri Nov 27 12:58:04 2009
@@ -46,6 +46,10 @@
         """
         self.tracker_home = tracker_home
         self.optimize = optimize
+        # if set, call schema_hook after executing schema.py will get
+        # same variables (in particular db) as schema.py main purpose is
+        # for regression tests
+        self.schema_hook = None
         self.config = configuration.CoreConfig(tracker_home)
         self.actions = {}
         self.cgi_actions = {}
@@ -106,6 +110,8 @@
         if self.optimize:
             # execute preloaded schema object
             exec(self.schema, vars)
+            if callable (self.schema_hook):
+                self.schema_hook(**vars)
             # use preloaded detectors
             detectors = self.detectors
         else:
@@ -114,6 +120,8 @@
                 sys.path.insert(1, libdir)
             # execute the schema file
             self._load_python('schema.py', vars)
+            if callable (self.schema_hook):
+                self.schema_hook(**vars)
             # reload extensions and detectors
             for extension in self.get_extensions('extensions'):
                 extension(self)
@@ -128,6 +136,27 @@
         # or this is the first time the database is opened,
         # do database upgrade checks
         if not (self.optimize and self.db_open):
+            # As a consistency check, ensure that every link property is
+            # pointing at a defined class.  Otherwise, the schema is
+            # internally inconsistent.  This is an important safety
+            # measure as it protects against an accidental schema change
+            # dropping a table while there are still links to the table;
+            # once the table has been dropped, there is no way to get it
+            # back, so it is important to drop it only if we are as sure
+            # as possible that it is no longer needed.
+            classes = db.getclasses()
+            for classname in classes:
+                cl = db.getclass(classname)
+                for propname, prop in cl.getprops().iteritems():
+                    if not isinstance(prop, (hyperdb.Link,
+                                             hyperdb.Multilink)):
+                        continue
+                    linkto = prop.classname
+                    if linkto not in classes:
+                        raise ValueError, \
+                            ("property %s.%s links to non-existent class %s"
+                             % (classname, propname, linkto))
+
             db.post_init()
             self.db_open = 1
         return db

Modified: tracker/roundup-src/roundup/mailer.py
==============================================================================
--- tracker/roundup-src/roundup/mailer.py	(original)
+++ tracker/roundup-src/roundup/mailer.py	Fri Nov 27 12:58:04 2009
@@ -1,7 +1,6 @@
 """Sending Roundup-specific mail over SMTP.
 """
 __docformat__ = 'restructuredtext'
-# $Id: mailer.py,v 1.22 2008-07-21 01:44:58 richard Exp $
 
 import time, quopri, os, socket, smtplib, re, sys, traceback, email
 
@@ -75,8 +74,7 @@
         if multipart:
             message = MIMEMultipart()
         else:
-            message = Message()
-            message.set_type('text/plain')
+            message = MIMEText("")
             message.set_charset(charset)
 
         try:
@@ -103,8 +101,6 @@
         # finally, an aid to debugging problems
         message['X-Roundup-Version'] = __version__
 
-        message['MIME-Version'] = '1.0'
-
         return message
 
     def standard_message(self, to, subject, content, author=None):
@@ -121,7 +117,7 @@
         message = self.get_standard_message(to, subject, author)
         message.set_payload(content)
         encode_quopri(message)
-        self.smtp_send(to, str(message))
+        self.smtp_send(to, message.as_string())
 
     def bounce_message(self, bounced_message, to, error,
                        subject='Failed issue tracker submission'):
@@ -145,29 +141,30 @@
         elif error_messages_to == "both":
             to.append(dispatcher_email)
 
-        message = self.get_standard_message(to, subject)
+        message = self.get_standard_message(to, subject, multipart=True)
 
         # add the error text
-        part = MIMEText(error)
+        part = MIMEText('\n'.join(error))
         message.attach(part)
 
         # attach the original message to the returned message
+        body = []
+        for header in bounced_message.headers:
+            body.append(header)
         try:
             bounced_message.rewindbody()
-        except IOError, message:
-            body.write("*** couldn't include message body: %s ***"
-                       % bounced_message)
+        except IOError, errmessage:
+            body.append("*** couldn't include message body: %s ***" %
+                errmessage)
         else:
-            body.write(bounced_message.fp.read())
-        part = MIMEText(bounced_message.fp.read())
-        part['Content-Disposition'] = 'attachment'
-        for header in bounced_message.headers:
-            part.write(header)
+            body.append('\n')
+            body.append(bounced_message.fp.read())
+        part = MIMEText(''.join(body))
         message.attach(part)
 
         # send
         try:
-            self.smtp_send(to, str(message))
+            self.smtp_send(to, message.as_string())
         except MessageSendError:
             # squash mail sending errors when bouncing mail
             # TODO this *could* be better, as we could notify admin of the

Modified: tracker/roundup-src/roundup/mailgw.py
==============================================================================
--- tracker/roundup-src/roundup/mailgw.py	(original)
+++ tracker/roundup-src/roundup/mailgw.py	Fri Nov 27 12:58:04 2009
@@ -79,7 +79,7 @@
 
 import string, re, os, mimetools, cStringIO, smtplib, socket, binascii, quopri
 import time, random, sys, logging
-import traceback, MimeWriter, rfc822
+import traceback, rfc822
 
 from email.Header import decode_header
 
@@ -524,9 +524,8 @@
 
 class MailGW:
 
-    def __init__(self, instance, db, arguments=()):
+    def __init__(self, instance, arguments=()):
         self.instance = instance
-        self.db = db
         self.arguments = arguments
         self.default_class = None
         for option, value in self.arguments:
@@ -802,6 +801,21 @@
 
         Parse the message as per the module docstring.
         '''
+        # get database handle for handling one email
+        self.db = self.instance.open ('admin')
+        try:
+            return self._handle_message (message)
+        finally:
+            self.db.close()
+
+    def _handle_message(self, message):
+        ''' message - a Message instance
+
+        Parse the message as per the module docstring.
+
+        The implementation expects an opened database and a try/finally
+        that closes the database.
+        '''
         # detect loops
         if message.getheader('x-roundup-loop', ''):
             raise IgnoreLoop

Modified: tracker/roundup-src/roundup/roundupdb.py
==============================================================================
--- tracker/roundup-src/roundup/roundupdb.py	(original)
+++ tracker/roundup-src/roundup/roundupdb.py	Fri Nov 27 12:58:04 2009
@@ -16,7 +16,6 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-# $Id: roundupdb.py,v 1.139 2008-08-07 06:31:16 richard Exp $
 
 """Extending hyperdb with types specific to issue-tracking.
 """
@@ -379,7 +378,7 @@
         charset = getattr(self.db.config, 'EMAIL_CHARSET', 'utf-8')
 
         # construct the content and convert to unicode object
-        content = unicode('\n'.join(m), 'utf-8').encode(charset)
+        body = unicode('\n'.join(m), 'utf-8').encode(charset)
 
         # make sure the To line is always the same (for testing mostly)
         sendto.sort()
@@ -466,7 +465,7 @@
             # attach files
             if message_files:
                 # first up the text as a part
-                part = MIMEText(content)
+                part = MIMEText(body)
                 encode_quopri(part)
                 message.attach(part)
 
@@ -501,13 +500,13 @@
                     message.attach(part)
 
             else:
-                message.set_payload(content)
+                message.set_payload(body)
                 encode_quopri(message)
 
             if first:
-                mailer.smtp_send(sendto + bcc_sendto, str(message))
+                mailer.smtp_send(sendto + bcc_sendto, message.as_string())
             else:
-                mailer.smtp_send(sendto, str(message))
+                mailer.smtp_send(sendto, message.as_string())
             first = False
 
     def email_signature(self, nodeid, msgid):

Modified: tracker/roundup-src/roundup/scripts/roundup_mailgw.py
==============================================================================
--- tracker/roundup-src/roundup/scripts/roundup_mailgw.py	(original)
+++ tracker/roundup-src/roundup/scripts/roundup_mailgw.py	Fri Nov 27 12:58:04 2009
@@ -138,73 +138,65 @@
     import roundup.instance
     instance = roundup.instance.open(instance_home)
 
-    # get a mail handler
-    db = instance.open('admin')
+    if hasattr(instance, 'MailGW'):
+        handler = instance.MailGW(instance, optionsList)
+    else:
+        handler = mailgw.MailGW(instance, optionsList)
+
+    # if there's no more arguments, read a single message from stdin
+    if len(args) == 1:
+        return handler.do_pipe()
+
+    # otherwise, figure what sort of mail source to handle
+    if len(args) < 3:
+        return usage(argv, _('Error: not enough source specification information'))
+    source, specification = args[1:3]
+
+    # time out net connections after a minute if we can
+    if source not in ('mailbox', 'imaps'):
+        if hasattr(socket, 'setdefaulttimeout'):
+            socket.setdefaulttimeout(60)
 
-    # now wrap in try/finally so we always close the database
+    if source == 'mailbox':
+        return handler.do_mailbox(specification)
+
+    # the source will be a network server, so obtain the credentials to
+    # use in connecting to the server
     try:
-        if hasattr(instance, 'MailGW'):
-            handler = instance.MailGW(instance, db, optionsList)
+        # attempt to obtain credentials from a ~/.netrc file
+        authenticator = netrc.netrc().authenticators(specification)
+        username = authenticator[0]
+        password = authenticator[2]
+        server = specification
+        # IOError if no ~/.netrc file, TypeError if the hostname
+        # not found in the ~/.netrc file:
+    except (IOError, TypeError):
+        match = re.match(r'((?P<user>[^:]+)(:(?P<pass>.+))?@)?(?P<server>.+)',
+                         specification)
+        if match:
+            username = match.group('user')
+            password = match.group('pass')
+            server = match.group('server')
         else:
-            handler = mailgw.MailGW(instance, db, optionsList)
+            return usage(argv, _('Error: %s specification not valid') % source)
+
+    # now invoke the mailgw handler depending on the server handler requested
+    if source.startswith('pop'):
+        ssl = source.endswith('s')
+        if ssl and sys.version_info<(2,4):
+            return usage(argv, _('Error: a later version of python is required'))
+        return handler.do_pop(server, username, password, ssl)
+    elif source == 'apop':
+        return handler.do_apop(server, username, password)
+    elif source.startswith('imap'):
+        ssl = source.endswith('s')
+        mailbox = ''
+        if len(args) > 3:
+            mailbox = args[3]
+        return handler.do_imap(server, username, password, mailbox, ssl)
 
-        # if there's no more arguments, read a single message from stdin
-        if len(args) == 1:
-            return handler.do_pipe()
-
-        # otherwise, figure what sort of mail source to handle
-        if len(args) < 3:
-            return usage(argv, _('Error: not enough source specification information'))
-        source, specification = args[1:3]
-
-        # time out net connections after a minute if we can
-        if source not in ('mailbox', 'imaps'):
-            if hasattr(socket, 'setdefaulttimeout'):
-                socket.setdefaulttimeout(60)
-
-        if source == 'mailbox':
-            return handler.do_mailbox(specification)
-
-        # the source will be a network server, so obtain the credentials to
-        # use in connecting to the server
-        try:
-            # attempt to obtain credentials from a ~/.netrc file
-            authenticator = netrc.netrc().authenticators(specification)
-            username = authenticator[0]
-            password = authenticator[2]
-            server = specification
-            # IOError if no ~/.netrc file, TypeError if the hostname
-            # not found in the ~/.netrc file:
-        except (IOError, TypeError):
-            match = re.match(r'((?P<user>[^:]+)(:(?P<pass>.+))?@)?(?P<server>.+)',
-                             specification)
-            if match:
-                username = match.group('user')
-                password = match.group('pass')
-                server = match.group('server')
-            else:
-                return usage(argv, _('Error: %s specification not valid') % source)
-
-        # now invoke the mailgw handler depending on the server handler requested
-        if source.startswith('pop'):
-            ssl = source.endswith('s')
-            if ssl and sys.version_info<(2,4):
-                return usage(argv, _('Error: a later version of python is required'))
-            return handler.do_pop(server, username, password, ssl)
-        elif source == 'apop':
-            return handler.do_apop(server, username, password)
-        elif source.startswith('imap'):
-            ssl = source.endswith('s')
-            mailbox = ''
-            if len(args) > 3:
-                mailbox = args[3]
-            return handler.do_imap(server, username, password, mailbox, ssl)
-
-        return usage(argv, _('Error: The source must be either "mailbox",'
-            ' "pop", "pops", "apop", "imap" or "imaps"'))
-    finally:
-        # handler might have closed the initial db and opened a new one
-        handler.db.close()
+    return usage(argv, _('Error: The source must be either "mailbox",'
+        ' "pop", "pops", "apop", "imap" or "imaps"'))
 
 def run():
     sys.exit(main(sys.argv))

Modified: tracker/roundup-src/roundup/scripts/roundup_server.py
==============================================================================
--- tracker/roundup-src/roundup/scripts/roundup_server.py	(original)
+++ tracker/roundup-src/roundup/scripts/roundup_server.py	Fri Nov 27 12:58:04 2009
@@ -29,6 +29,8 @@
 except ImportError:
     SSL = None
 
+from time import sleep
+
 # python version check
 from roundup import configuration, version_check
 from roundup import __version__ as roundup_version
@@ -127,9 +129,18 @@
                         try:
                             line = self.__fileobj.readline(*args)
                         except SSL.WantReadError:
+                            sleep (.1)
                             line = None
                     return line
 
+                def read(self, *args):
+                    """ SSL.Connection can return WantRead """
+                    while True:
+                        try:
+                            return self.__fileobj.read(*args)
+                        except SSL.WantReadError:
+                            sleep (.1)
+
                 def __getattr__(self, attrib):
                     return getattr(self.__fileobj, attrib)
 

Modified: tracker/roundup-src/roundup/xmlrpc.py
==============================================================================
--- tracker/roundup-src/roundup/xmlrpc.py	(original)
+++ tracker/roundup-src/roundup/xmlrpc.py	Fri Nov 27 12:58:04 2009
@@ -59,6 +59,14 @@
         self.actions = actions
         self.translator = translator
 
+    def schema(self):
+        s = {}
+        for c in self.db.classes:
+            cls = self.db.classes[c]
+            props = [(n,repr(v)) for n,v in cls.properties.items()]
+            s[c] = props
+        return s
+
     def list(self, classname, propname=None):
         cl = self.db.getclass(classname)
         if not propname:
@@ -90,6 +98,7 @@
         return dict(result)
 
     def create(self, classname, *args):
+        
         if not self.db.security.hasPermission('Create', self.db.getuid(), classname):
             raise Unauthorised('Permission to create %s denied'%classname)
 
@@ -103,9 +112,15 @@
         if key and not props.has_key(key):
             raise UsageError, 'you must provide the "%s" property.'%key
 
+        for key in props:
+            if not self.db.security.hasPermission('Edit', self.db.getuid(), classname,
+                                                  property=key):
+                raise Unauthorised('Permission to set %s.%s denied'%(classname, key))
+
         # do the actual create
         try:
             result = cl.create(**props)
+            self.db.commit()
         except (TypeError, IndexError, ValueError), message:
             raise UsageError, message
         return result
@@ -121,15 +136,17 @@
                 raise Unauthorised('Permission to edit %s of %s denied'%
                                    (p, designator))
         try:
-            return cl.set(itemid, **props)
+            result = cl.set(itemid, **props)
+            self.db.commit()
         except (TypeError, IndexError, ValueError), message:
             raise UsageError, message
+        return result
 
 
     builtin_actions = {'retire': actions.Retire}
 
     def action(self, name, *args):
-        """"""
+        """Execute a named action."""
         
         if name in self.actions:
             action_type = self.actions[name]
@@ -148,7 +165,12 @@
     def __init__(self, db, actions, translator,
                  allow_none=False, encoding=None):
 
-        SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding)
+        try:
+            # python2.5 and beyond
+            SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding)
+        except TypeError:
+            # python2.4
+            SimpleXMLRPCDispatcher.__init__(self)
         self.register_instance(RoundupInstance(db, actions, translator))
                  
 

Modified: tracker/roundup-src/setup.py
==============================================================================
--- tracker/roundup-src/setup.py	(original)
+++ tracker/roundup-src/setup.py	Fri Nov 27 12:58:04 2009
@@ -101,22 +101,31 @@
           description="A simple-to-use and -install issue-tracking system"
             " with command-line, web and e-mail interfaces. Highly"
             " customisable.",
-          long_description='''
-1.4.8 fixes some regressions:
+          long_description='''This version of Roundup fixes some bugs:
 
-- bug introduced into hyperdb filter (issue 2550505)
-- bug introduced into CVS export and view (issue 2550529)
-- bugs introduced in the migration to the email package (issue 2550531)
-
-And adds a couple of other fixes:
-
-- handle bogus pagination values (issue 2550530)
-- fix TLS handling with some SMTP servers (issues 2484879 and 1912923)
+- Minor update of doc/developers.txt to point to the new resources
+  on www.roundup-tracker.org (Bernhard Reiter)
+- Small CSS improvements regaring the search box (thanks Thomas Arendsan Hein)
+  (issue 2550589)
+- Indexers behaviour made more consistent regarding length of indexed words
+  and stopwords (thanks Thomas Arendsen Hein, Bernhard Reiter)(issue 2550584)
+- fixed typos in the installation instructions (thanks Thomas Arendsen Hein)
+  (issue 2550573) 
+- New config option csv_field_size: Pythons csv module (which is used
+  for export/import) has a new field size limit starting with python2.5.
+  We now issue a warning during export if the limit is too small and use
+  the csv_field_size configuration during import to set the limit for
+  the csv module.
+- Small fix for CGI-handling of XMLRPC requests for python2.4, this
+  worked only for 2.5 and beyond due to a change in the xmlrpc interface
+  in python
+- Document filter method of xmlrpc interface
+- Fix interaction of SSL and XMLRPC, now XMLRPC works with SSL
 
 If you're upgrading from an older version of Roundup you *must* follow
 the "Software Upgrade" guidelines given in the maintenance documentation.
 
-Roundup requires python 2.3 or later for correct operation.
+Roundup requires python 2.3 or later (but not 3+) for correct operation.
 
 To give Roundup a try, just download (see below), unpack and run::
 
@@ -147,9 +156,9 @@
 The system will facilitate communication among the participants by managing
 discussions and notifying interested parties when issues are edited. One of
 the major design goals for Roundup that it be simple to get going. Roundup
-is therefore usable "out of the box" with any python 2.3+ installation. It
-doesn't even need to be "installed" to be operational, though a
-disutils-based install script is provided.
+is therefore usable "out of the box" with any python 2.3+ (but not 3+)
+installation. It doesn't even need to be "installed" to be operational,
+though an install script is provided.
 
 It comes with two issue tracker templates (a classic bug/feature tracker and
 a minimal skeleton) and five database back-ends (anydbm, sqlite, metakit,

Modified: tracker/roundup-src/share/roundup/templates/classic/html/_generic.item.html
==============================================================================
--- tracker/roundup-src/share/roundup/templates/classic/html/_generic.item.html	(original)
+++ tracker/roundup-src/share/roundup/templates/classic/html/_generic.item.html	Fri Nov 27 12:58:04 2009
@@ -44,7 +44,12 @@
 
 </form>
 
-<tal:block tal:condition="context/id" tal:replace="structure context/history" />
+<tal:block tal:condition="context/id"
+    tal:define="limit python:[10, None][request.form.has_key('show_all_history')]"
+    tal:replace="structure python:context.history(limit=limit)" />
+<p tal:condition="not:exists:request/form/show_all_history" i18n:translate="">Showing 10 items.
+<a tal:attributes="href string:${context/_classname}${context/id}?show_all_history=yes">Show all history</a>
+(warning: this could be VERY long)</p>
 
 </div>
 

Modified: tracker/roundup-src/share/roundup/templates/classic/html/page.html
==============================================================================
--- tracker/roundup-src/share/roundup/templates/classic/html/page.html	(original)
+++ tracker/roundup-src/share/roundup/templates/classic/html/page.html	Fri Nov 27 12:58:04 2009
@@ -28,9 +28,6 @@
 <tr>
  <td class="page-header-left">&nbsp;</td>
  <td class="page-header-top">
-   <div id="body-title">
-     <h2><span metal:define-slot="body_title">body title</span></h2>
-   </div>
    <div id="searchbox">
      <form method="GET" action="issue">
        <input type="hidden" name="@columns"
@@ -43,7 +40,10 @@
        <input type="submit" id="submit" name="submit" value="Search"
               i18n:attributes="value" />
      </form>
-  </div>
+   </div>
+   <div id="body-title">
+     <h2><span metal:define-slot="body_title">body title</span></h2>
+   </div>
  </td>
 </tr>
 
@@ -136,7 +136,7 @@
     <input type="hidden" name="__came_from" tal:attributes="value string:${request/base}${request/env/PATH_INFO}">
     <span tal:replace="structure request/indexargs_form" />
     <a href="user?@template=register"
-       tal:condition="python:request.user.hasPermission('Create', 'user')"
+       tal:condition="python:request.user.hasPermission('Register', 'user')"
      i18n:translate="">Register</a><br>
     <a href="user?@template=forgotten" i18n:translate="">Lost&nbsp;your&nbsp;login?</a><br>
    </p>
@@ -164,7 +164,7 @@
   </p>
   <p class="userblock">
    <b i18n:translate="">Help</b><br>
-   <a href="http://roundup.sourceforge.net/doc-1.0/"
+   <a href="http://www.roundup-tracker.org"
     i18n:translate="">Roundup docs</a>
   </p>
  </td>

Modified: tracker/roundup-src/share/roundup/templates/classic/html/style.css
==============================================================================
--- tracker/roundup-src/share/roundup/templates/classic/html/style.css	(original)
+++ tracker/roundup-src/share/roundup/templates/classic/html/style.css	Fri Nov 27 12:58:04 2009
@@ -50,14 +50,6 @@
   padding: 5px;
   border-bottom: 1px solid #444;
 }
-#searchbox {
-    float: right;
-}
-
-div#body-title {
-  float: left;
-}
-
 
 div#searchbox {
   float: right;

Modified: tracker/roundup-src/share/roundup/templates/classic/schema.py
==============================================================================
--- tracker/roundup-src/share/roundup/templates/classic/schema.py	(original)
+++ tracker/roundup-src/share/roundup/templates/classic/schema.py	Fri Nov 27 12:58:04 2009
@@ -47,6 +47,8 @@
                 roles=String(),     # comma-separated string of Role names
                 timezone=String())
 user.setkey("username")
+db.security.addPermission(name='Register', klass='user',
+                          description='User is allowed to register new user')
 
 # FileClass automatically gets this property in addition to the Class ones:
 #   content = String()    [saved to disk in <tracker home>/db/files/]
@@ -154,7 +156,7 @@
 # Assign the appropriate permissions to the anonymous user's Anonymous
 # Role. Choices here are:
 # - Allow anonymous users to register
-db.security.addPermissionToRole('Anonymous', 'Create', 'user')
+db.security.addPermissionToRole('Anonymous', 'Register', 'user')
 
 # Allow anonymous users access to view issues (and the related, linked
 # information)

Modified: tracker/roundup-src/share/roundup/templates/minimal/html/_generic.item.html
==============================================================================
--- tracker/roundup-src/share/roundup/templates/minimal/html/_generic.item.html	(original)
+++ tracker/roundup-src/share/roundup/templates/minimal/html/_generic.item.html	Fri Nov 27 12:58:04 2009
@@ -9,18 +9,18 @@
 
 <td class="content" metal:fill-slot="content">
 
-<span tal:condition="python:not (context.is_view_ok() or context.is_edit_ok()
- or request.user.hasRole('Anonymous'))"
- tal:omit-tag="python:1" i18n:translate=""
->You are not allowed to view this page.</span>
-
-<span tal:condition="python:not (context.is_view_ok() or context.is_edit_ok())
- and request.user.hasRole('Anonymous')"
- tal:omit-tag="python:1" i18n:translate=""
->Please login with your username and password.</span>
+<p tal:condition="python:not (context.is_view_ok()
+ or request.user.hasRole('Anonymous'))" i18n:translate="">
+ You are not allowed to view this page.</p>
+
+<p tal:condition="python:not context.is_view_ok()
+ and request.user.hasRole('Anonymous')" i18n:translate="">
+ Please login with your username and password.</p>
+
+<div tal:condition="context/is_view_ok">
 
 <form method="POST" onSubmit="return submit_once()"
-      enctype="multipart/form-data" tal:condition="context/is_edit_ok"
+      enctype="multipart/form-data" tal:condition="context/is_view_ok"
       tal:attributes="action context/designator">
 
 <input type="hidden" name="@template" value="item">
@@ -44,21 +44,14 @@
 
 </form>
 
-<table class="form" tal:condition="context/is_only_view_ok">
-
-<tr tal:repeat="prop python:db[context._classname].properties()">
- <tal:block tal:condition="python:prop._name not in ('id', 'creator',
-                                  'creation', 'activity')">
-  <th tal:content="prop/_name"></th>
-  <td tal:content="structure python:context[prop._name].field()"></td>
- </tal:block>
-</tr>
-</table>
-
+<tal:block tal:condition="context/id"
+    tal:define="limit python:[10, None][request.form.has_key('show_all_history')]"
+    tal:replace="structure python:context.history(limit=limit)" />
+<p tal:condition="not:exists:request/form/show_all_history" i18n:translate="">Showing 10 items.
+<a tal:attributes="href string:${context/_classname}${context/id}?show_all_history=yes">Show all history</a>
+(warning: this could be VERY long)</p>
 
-<tal:block tal:condition="python:context.id and context.is_view_ok()">
- <tal:block tal:replace="structure context/history" />
-</tal:block>
+</div>
 
 </td>
 

Modified: tracker/roundup-src/share/roundup/templates/minimal/html/page.html
==============================================================================
--- tracker/roundup-src/share/roundup/templates/minimal/html/page.html	(original)
+++ tracker/roundup-src/share/roundup/templates/minimal/html/page.html	Fri Nov 27 12:58:04 2009
@@ -135,7 +135,7 @@
     <input type="hidden" name="__came_from" tal:attributes="value string:${request/base}${request/env/PATH_INFO}">
     <span tal:replace="structure request/indexargs_form" />
     <a href="user?@template=register"
-       tal:condition="python:request.user.hasPermission('Create', 'user')"
+       tal:condition="python:request.user.hasPermission('Register', 'user')"
      i18n:translate="">Register</a><br>
     <a href="user?@template=forgotten" i18n:translate="">Lost&nbsp;your&nbsp;login?</a><br>
    </p>
@@ -151,7 +151,7 @@
   </p>
   <p class="userblock">
    <b i18n:translate="">Help</b><br>
-   <a href="http://roundup.sourceforge.net/doc-1.0/"
+   <a href="http://www.roundup-tracker.org"
     i18n:translate="">Roundup docs</a>
   </p>
  </td>

Modified: tracker/roundup-src/share/roundup/templates/minimal/html/user.item.html
==============================================================================
--- tracker/roundup-src/share/roundup/templates/minimal/html/user.item.html	(original)
+++ tracker/roundup-src/share/roundup/templates/minimal/html/user.item.html	Fri Nov 27 12:58:04 2009
@@ -1,4 +1,3 @@
-<!-- dollarId: user.item,v 1.7 2002/08/16 04:29:04 richard Exp dollar-->
 <tal:doc metal:use-macro="templates/page/macros/icing"
 define="edit_ok context/is_edit_ok"
 >
@@ -57,10 +56,6 @@
   confirm_input templates/page/macros/user_confirm_input;
   edit_ok context/is_edit_ok;
   ">
- <tr tal:define="name string:realname; label string:Name; value context/realname; edit_ok edit_ok">
-  <th metal:use-macro="th_label">Name</th>
-  <td><input name="realname" metal:use-macro="src_input"></td>
- </tr>
  <tr tal:define="name string:username; label string:Login Name; value context/username">
    <th metal:use-macro="th_label">Login Name</th>
    <td><input metal:use-macro="src_input"></td>
@@ -91,51 +86,6 @@
  </tr>
  </tal:if>
 
- <tr tal:define="name string:phone; label string:Phone; value context/phone">
-  <th metal:use-macro="th_label">Phone</th>
-  <td><input name="phone" metal:use-macro="normal_input"></td>
- </tr>
-
- <tr tal:define="name string:organisation; label string:Organisation; value context/organisation">
-  <th metal:use-macro="th_label">Organisation</th>
-  <td><input name="organisation" metal:use-macro="normal_input"></td>
- </tr>
-
- <tr tal:condition="python:edit_ok or context.timezone"
-     tal:define="name string:timezone; label string:Timezone; value context/timezone">
-  <th metal:use-macro="th_label">Timezone</th>
-  <td><input name="timezone" metal:use-macro="normal_input">
-   <tal:block tal:condition="edit_ok" i18n:translate="">(this is a numeric hour offset, the default is
-    <span tal:replace="db/config/DEFAULT_TIMEZONE" i18n:name="zone"
-    />)</tal:block>
-  </td>
- </tr>
-
- <tr tal:define="name string:address; label string:E-mail address; value context/address">
-  <th metal:use-macro="th_label">E-mail address</th>
-  <td tal:define="mailto python:context.address.field(id='address');
-	  mklink python:mailto and not edit_ok">
-      <a href="mailto:calvin at the-z.org"
-		  tal:attributes="href string:mailto:$value"
-		  tal:content="value"
-          tal:condition="python:mklink">calvin at the-z.org</a>
-      <tal:if condition=edit_ok>
-      <input metal:use-macro="src_input" value="calvin at the-z.org">
-      </tal:if>
-      &nbsp;
-  </td>
- </tr>
-
- <tr>
-  <th><label for="alternate_addresses" i18n:translate="">Alternate E-mail addresses<br>One address per line</label></th>
-  <td>
-    <textarea rows=5 cols=40 tal:replace="structure context/alternate_addresses/multiline">nobody at nowhere.org
-anybody at everywhere.net
-(alternate_addresses)
-    </textarea>
-  </td>
- </tr>
-
  <tr tal:condition="edit_ok">
   <td>
    &nbsp;

Modified: tracker/roundup-src/share/roundup/templates/minimal/schema.py
==============================================================================
--- tracker/roundup-src/share/roundup/templates/minimal/schema.py	(original)
+++ tracker/roundup-src/share/roundup/templates/minimal/schema.py	Fri Nov 27 12:58:04 2009
@@ -60,6 +60,6 @@
 # Assign the appropriate permissions to the anonymous user's
 # Anonymous Role. Choices here are:
 # - Allow anonymous users to register
-db.security.addPermissionToRole('Anonymous', 'Create', 'user')
+db.security.addPermissionToRole('Anonymous', 'Register', 'user')
 
 # vim: set et sts=4 sw=4 :

Modified: tracker/roundup-src/test/db_test_base.py
==============================================================================
--- tracker/roundup-src/test/db_test_base.py	(original)
+++ tracker/roundup-src/test/db_test_base.py	Fri Nov 27 12:58:04 2009
@@ -1613,6 +1613,18 @@
 
 # XXX add sorting tests for other types
 
+    # nuke and re-create db for restore
+    def nukeAndCreate(self):
+        # shut down this db and nuke it
+        self.db.close()
+        self.nuke_database()
+
+        # open a new, empty database
+        os.makedirs(config.DATABASE + '/files')
+        self.db = self.module.Database(config, 'admin')
+        setupSchema(self.db, 0, self.module)
+
+
     def testImportExport(self):
         # use the filtering setup to create a bunch of items
         ae, filt = self.filteringSetup()
@@ -1660,14 +1672,7 @@
                         klass.export_files('_test_export', id)
                 journals[cn] = klass.export_journals()
 
-            # shut down this db and nuke it
-            self.db.close()
-            self.nuke_database()
-
-            # open a new, empty database
-            os.makedirs(config.DATABASE + '/files')
-            self.db = self.module.Database(config, 'admin')
-            setupSchema(self.db, 0, self.module)
+            self.nukeAndCreate()
 
             # import
             for cn, items in export.items():
@@ -1730,6 +1735,58 @@
         newid = self.db.user.create(username='testing')
         assert newid > maxid
 
+    # test import/export via admin interface
+    def testAdminImportExport(self):
+        import roundup.admin
+        import csv
+        # use the filtering setup to create a bunch of items
+        ae, filt = self.filteringSetup()
+        # create large field
+        self.db.priority.create(name = 'X' * 500)
+        self.db.config.CSV_FIELD_SIZE = 400
+        self.db.commit()
+        output = []
+        # ugly hack to get stderr output and disable stdout output
+        # during regression test. Depends on roundup.admin not using
+        # anything but stdout/stderr from sys (which is currently the
+        # case)
+        def stderrwrite(s):
+            output.append(s)
+        roundup.admin.sys = MockNull ()
+        try:
+            roundup.admin.sys.stderr.write = stderrwrite
+            tool = roundup.admin.AdminTool()
+            home = '.'
+            tool.tracker_home = home
+            tool.db = self.db
+            tool.verbose = False
+            tool.do_export (['_test_export'])
+            self.assertEqual(len(output), 2)
+            self.assertEqual(output [1], '\n')
+            self.failUnless(output [0].startswith
+                ('Warning: config csv_field_size should be at least'))
+            self.failUnless(int(output[0].split()[-1]) > 500)
+
+            if hasattr(roundup.admin.csv, 'field_size_limit'):
+                self.nukeAndCreate()
+                self.db.config.CSV_FIELD_SIZE = 400
+                tool = roundup.admin.AdminTool()
+                tool.tracker_home = home
+                tool.db = self.db
+                tool.verbose = False
+                self.assertRaises(csv.Error, tool.do_import, ['_test_export'])
+
+            self.nukeAndCreate()
+            self.db.config.CSV_FIELD_SIZE = 3200
+            tool = roundup.admin.AdminTool()
+            tool.tracker_home = home
+            tool.db = self.db
+            tool.verbose = False
+            tool.do_import(['_test_export'])
+        finally:
+            roundup.admin.sys = sys
+            shutil.rmtree('_test_export')
+
     def testAddProperty(self):
         self.db.issue.create(title="spam", status='1')
         self.db.commit()

Modified: tracker/roundup-src/test/test_anypy_hashlib.py
==============================================================================
--- tracker/roundup-src/test/test_anypy_hashlib.py	(original)
+++ tracker/roundup-src/test/test_anypy_hashlib.py	Fri Nov 27 12:58:04 2009
@@ -39,7 +39,7 @@
 class TestCase_anypy_hashlib(unittest.TestCase):
     """test the hashlib compatibility layer"""
 
-    testdata = (
+    data_for_test = (
            ('',
             'da39a3ee5e6b4b0d3255bfef95601890afd80709',
             'd41d8cd98f00b204e9800998ecf8427e'),
@@ -58,12 +58,12 @@
     # the following two are always excecuted: 
     def test_sha1_expected_anypy(self):
         """...anypy.hashlib_.sha1().hexdigest() yields expected results"""
-        for src, SHA, MD5 in self.testdata:
+        for src, SHA, MD5 in self.data_for_test:
             self.assertEqual(roundup.anypy.hashlib_.sha1(src).hexdigest(), SHA)
 
     def test_md5_expected_anypy(self):
         """...anypy.hashlib_.md5().hexdigest() yields expected results"""
-        for src, SHA, MD5 in self.testdata:
+        for src, SHA, MD5 in self.data_for_test:
             self.assertEqual(roundup.anypy.hashlib_.md5(src).hexdigest(), MD5)
 
     # execution depending on availability of modules: 
@@ -73,14 +73,14 @@
             if md5.md5 is hashlib.md5:
                 return
             else:
-                for s, i1, i2 in self.testdata:
+                for s, i1, i2 in self.data_for_test:
                     self.assertEqual(md5.md5(s).digest(),
                                      hashlib.md5().digest())
 
     if md5:
         def test_md5_expected(self):
             """md5.md5().hexdigest() yields expected results"""
-            for src, SHA, MD5 in self.testdata:
+            for src, SHA, MD5 in self.data_for_test:
                 self.assertEqual(md5.md5(src).hexdigest(), MD5)
 
         def test_md5_new_expected(self):
@@ -88,7 +88,7 @@
             if md5.new is md5.md5:
                 return
             else:
-                for src, SHA, MD5 in self.testdata:
+                for src, SHA, MD5 in self.data_for_test:
                     self.assertEqual(md5.new(src).hexdigest(), MD5)
 
     if sha and hashlib:
@@ -97,14 +97,14 @@
             if sha.sha is hashlib.sha1:
                 return
             else:
-                for s in self.testdata:
+                for s in self.data_for_test:
                     self.assertEqual(sha.sha(s).digest(),
                                      hashlib.sha1().digest())
 
     if sha:
         def test_sha_expected(self):
             """sha.sha().hexdigest() yields expected results"""
-            for src, SHA, MD5 in self.testdata:
+            for src, SHA, MD5 in self.data_for_test:
                 self.assertEqual(sha.sha(src).hexdigest(), SHA)
 
         # fails for me with Python 2.3; unittest module bug?
@@ -113,18 +113,18 @@
             if sha.new is sha.sha:
                 return
             else:
-                for src, SHA, MD5 in self.testdata:
+                for src, SHA, MD5 in self.data_for_test:
                     self.assertEqual(sha.new(src).hexdigest(), SHA)
 
     if hashlib:
         def test_sha1_expected_hashlib(self):
             """hashlib.sha1().hexdigest() yields expected results"""
-            for src, SHA, MD5 in self.testdata:
+            for src, SHA, MD5 in self.data_for_test:
                 self.assertEqual(hashlib.sha1(src).hexdigest(), SHA)
 
         def test_md5_expected_hashlib(self):
             """hashlib.md5().hexdigest() yields expected results"""
-            for src, SHA, MD5 in self.testdata:
+            for src, SHA, MD5 in self.data_for_test:
                 self.assertEqual(hashlib.md5(src).hexdigest(), MD5)
 
 def test_suite():

Modified: tracker/roundup-src/test/test_indexer.py
==============================================================================
--- tracker/roundup-src/test/test_indexer.py	(original)
+++ tracker/roundup-src/test/test_indexer.py	Fri Nov 27 12:58:04 2009
@@ -82,6 +82,48 @@
         self.dex.add_text(('test', '1', 'foo'), '')
         self.assertSeqEqual(self.dex.find(['world']), [('test', '2', 'foo')])
 
+    def test_stopwords(self):
+        """Test that we can find a text with a stopword in it."""
+        stopword = "with"
+        self.assert_(self.dex.is_stopword(stopword.upper()))
+        self.dex.add_text(('test', '1', 'bar'), '%s hello world' % stopword)
+        self.dex.add_text(('test', '2', 'bar'), 'blah a %s world' % stopword)
+        self.dex.add_text(('test', '3', 'bar'), 'blah Blub river')
+        self.dex.add_text(('test', '4', 'bar'), 'blah river %s' % stopword)
+        self.assertSeqEqual(self.dex.find(['with','world']),
+                                                    [('test', '1', 'bar'),
+                                                     ('test', '2', 'bar')])
+    def test_extremewords(self):
+        """Testing too short or too long words."""
+        short = "b"
+        long = "abcdefghijklmnopqrstuvwxyz"
+        self.dex.add_text(('test', '1', 'a'), '%s hello world' % short)
+        self.dex.add_text(('test', '2', 'a'), 'blah a %s world' % short)
+        self.dex.add_text(('test', '3', 'a'), 'blah Blub river')
+        self.dex.add_text(('test', '4', 'a'), 'blah river %s %s'
+                                                        % (short, long))
+        self.assertSeqEqual(self.dex.find([short,'world', long, short]),
+                                                    [('test', '1', 'a'),
+                                                     ('test', '2', 'a')])
+        self.assertSeqEqual(self.dex.find([long]),[])
+
+        # special test because some faulty code indexed length(word)>=2
+        # but only considered length(word)>=3 to be significant
+        self.dex.add_text(('test', '5', 'a'), 'blah py %s %s'
+                                                        % (short, long))
+        self.assertSeqEqual(self.dex.find(["py"]), [('test', '5', 'a')])
+
+    def test_casesensitity(self):
+        """Test if searches are case-in-sensitive."""
+        self.dex.add_text(('test', '1', 'a'), 'aaaa bbbb')
+        self.dex.add_text(('test', '2', 'a'), 'aAaa BBBB')
+        self.assertSeqEqual(self.dex.find(['aaaa']),
+                                                    [('test', '1', 'a'),
+                                                     ('test', '2', 'a')])
+        self.assertSeqEqual(self.dex.find(['BBBB']),
+                                                    [('test', '1', 'a'),
+                                                     ('test', '2', 'a')])
+
     def tearDown(self):
         shutil.rmtree('test-index')
 

Modified: tracker/roundup-src/test/test_mailgw.py
==============================================================================
--- tracker/roundup-src/test/test_mailgw.py	(original)
+++ tracker/roundup-src/test/test_mailgw.py	Fri Nov 27 12:58:04 2009
@@ -51,6 +51,7 @@
         if not new == old:
             res = []
 
+            replace = {}
             for key in new.keys():
                 if key.startswith('from '):
                     # skip the unix from line
@@ -60,11 +61,18 @@
                     if new[key] != __version__:
                         res.append('  %s: %r != %r' % (key, __version__,
                             new[key]))
+                elif key.lower() == 'content-type' and 'boundary=' in new[key]:
+                    # handle mime messages
+                    newmime = new[key].split('=',1)[-1].strip('"')
+                    oldmime = old.get(key, '').split('=',1)[-1].strip('"')
+                    replace ['--' + newmime] = '--' + oldmime
+                    replace ['--' + newmime + '--'] = '--' + oldmime + '--'
                 elif new.get(key, '') != old.get(key, ''):
                     res.append('  %s: %r != %r' % (key, old.get(key, ''),
                         new.get(key, '')))
 
-            body_diff = self.compareStrings(new.fp.read(), old.fp.read())
+            body_diff = self.compareStrings(new.fp.read(), old.fp.read(),
+                replace=replace)
             if body_diff:
                 res.append('')
                 res.extend(body_diff)
@@ -73,13 +81,14 @@
                 res.insert(0, 'Generated message not correct (diff follows):')
                 raise AssertionError, '\n'.join(res)
 
-    def compareStrings(self, s2, s1):
+    def compareStrings(self, s2, s1, replace={}):
         '''Note the reversal of s2 and s1 - difflib.SequenceMatcher wants
            the first to be the "original" but in the calls in this file,
            the second arg is the original. Ho hum.
+           Do replacements over the replace dict -- used for mime boundary
         '''
         l1 = s1.strip().split('\n')
-        l2 = s2.strip().split('\n')
+        l2 = [replace.get(i,i) for i in s2.strip().split('\n')]
         if l1 == l2:
             return
         s = difflib.SequenceMatcher(None, l1, l2)
@@ -116,11 +125,11 @@
             address='chef at bork.bork.bork', realname='Bork, Chef', roles='User')
         self.richard_id = self.db.user.create(username='richard',
             address='richard at test.test', roles='User')
-        self.mary_id = self.db.user.create(username='mary', address='mary at test.test',
-            roles='User', realname='Contrary, Mary')
-        self.john_id = self.db.user.create(username='john', address='john at test.test',
-            alternate_addresses='jondoe at test.test\njohn.doe at test.test', roles='User',
-            realname='John Doe')
+        self.mary_id = self.db.user.create(username='mary',
+            address='mary at test.test', roles='User', realname='Contrary, Mary')
+        self.john_id = self.db.user.create(username='john',
+            address='john at test.test', roles='User', realname='John Doe',
+            alternate_addresses='jondoe at test.test\njohn.doe at test.test')
 
     def tearDown(self):
         if os.path.exists(SENDMAILDEBUG):
@@ -132,11 +141,15 @@
             if error.errno not in (errno.ENOENT, errno.ESRCH): raise
 
     def _handle_mail(self, message):
-        handler = self.instance.MailGW(self.instance, self.db)
+        # handler will open a new db handle. On single-threaded
+        # databases we'll have to close our current connection
+        self.db.commit()
+        self.db.close()
+        handler = self.instance.MailGW(self.instance)
         handler.trapExceptions = 0
         ret = handler.main(StringIO(message))
-        # handler can close the db on us and open a new one
-        self.db = handler.db
+        # handler had its own database, open new connection
+        self.db = self.instance.open('admin')
         return ret
 
     def _get_mail(self):
@@ -549,6 +562,11 @@
         self.doNewIssue()
         oldvalues = self.db.getnode('issue', '1').copy()
         oldvalues['assignedto'] = None
+        # reconstruct old behaviour: This would reuse the
+        # database-handle from the doNewIssue above which has committed
+        # as user "Chef". So we close and reopen the db as that user.
+        self.db.close()
+        self.db = self.instance.open('Chef')
         self.db.issue.set('1', assignedto=self.chef_id)
         self.db.commit()
         self.db.issue.nosymessage('1', None, oldvalues)
@@ -990,11 +1008,6 @@
         assert not os.path.exists(SENDMAILDEBUG)
 
     def testNewUserAuthor(self):
-        # first without the permission
-        # heh... just ignore the API for a second ;)
-        self.db.security.role['anonymous'].permissions=[]
-        anonid = self.db.user.lookup('anonymous')
-        self.db.user.set(anonid, roles='Anonymous')
 
         l = self.db.user.list()
         l.sort()
@@ -1007,6 +1020,12 @@
 
 This is a test submission of a new issue.
 '''
+        def hook (db, **kw):
+            ''' set up callback for db open '''
+            db.security.role['anonymous'].permissions=[]
+            anonid = db.user.lookup('anonymous')
+            db.user.set(anonid, roles='Anonymous')
+        self.instance.schema_hook = hook
         try:
             self._handle_mail(message)
         except Unauthorized, value:
@@ -1021,15 +1040,17 @@
         else:
             raise AssertionError, "Unathorized not raised when handling mail"
 
-        # Add Web Access role to anonymous, and try again to make sure
-        # we get a "please register at:" message this time.
-        p = [
-            self.db.security.getPermission('Create', 'user'),
-            self.db.security.getPermission('Web Access', None),
-        ]
-
-        self.db.security.role['anonymous'].permissions=p
 
+        def hook (db, **kw):
+            ''' set up callback for db open '''
+            # Add Web Access role to anonymous, and try again to make sure
+            # we get a "please register at:" message this time.
+            p = [
+                db.security.getPermission('Create', 'user'),
+                db.security.getPermission('Web Access', None),
+            ]
+            db.security.role['anonymous'].permissions=p
+        self.instance.schema_hook = hook
         try:
             self._handle_mail(message)
         except Unauthorized, value:
@@ -1053,12 +1074,15 @@
         m.sort()
         self.assertEqual(l, m)
 
-        # now with the permission
-        p = [
-            self.db.security.getPermission('Create', 'user'),
-            self.db.security.getPermission('Email Access', None),
-        ]
-        self.db.security.role['anonymous'].permissions=p
+        def hook (db, **kw):
+            ''' set up callback for db open '''
+            # now with the permission
+            p = [
+                db.security.getPermission('Create', 'user'),
+                db.security.getPermission('Email Access', None),
+            ]
+            db.security.role['anonymous'].permissions=p
+        self.instance.schema_hook = hook
         self._handle_mail(message)
         m = self.db.user.list()
         m.sort()
@@ -1076,17 +1100,80 @@
 
 This is a test submission of a new issue.
 '''
-        p = [
-            self.db.security.getPermission('Create', 'user'),
-            self.db.security.getPermission('Email Access', None),
-        ]
-        self.db.security.role['anonymous'].permissions=p
+        def hook (db, **kw):
+            ''' set up callback for db open '''
+            p = [
+                db.security.getPermission('Create', 'user'),
+                db.security.getPermission('Email Access', None),
+            ]
+            db.security.role['anonymous'].permissions=p
+        self.instance.schema_hook = hook
         self._handle_mail(message)
         m = set(self.db.user.list())
         new = list(m - l)[0]
         name = self.db.user.get(new, 'realname')
         self.assertEquals(name, 'H€llo')
 
+    def testUnknownUser(self):
+        l = set(self.db.user.list())
+        message = '''Content-Type: text/plain;
+  charset="iso-8859-1"
+From: Nonexisting User <nonexisting at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Message-Id: <dummy_test_message_id>
+Subject: [issue] Testing nonexisting user...
+
+This is a test submission of a new issue.
+'''
+        self.db.close()
+        handler = self.instance.MailGW(self.instance)
+        # we want a bounce message:
+        handler.trapExceptions = 1
+        ret = handler.main(StringIO(message))
+        self.compareMessages(self._get_mail(),
+'''FROM: Roundup issue tracker <roundup-admin at your.tracker.email.domain.example>
+TO: nonexisting at bork.bork.bork
+From nobody Tue Jul 14 12:04:11 2009
+Content-Type: multipart/mixed; boundary="===============0639262320=="
+MIME-Version: 1.0
+Subject: Failed issue tracker submission
+To: nonexisting at bork.bork.bork
+From: Roundup issue tracker <roundup-admin at your.tracker.email.domain.example>
+Date: Tue, 14 Jul 2009 12:04:11 +0000
+Precedence: bulk
+X-Roundup-Name: Roundup issue tracker
+X-Roundup-Loop: hello
+X-Roundup-Version: 1.4.8
+MIME-Version: 1.0
+
+--===============0639262320==
+Content-Type: text/plain; charset="us-ascii"
+MIME-Version: 1.0
+Content-Transfer-Encoding: 7bit
+
+
+
+You are not a registered user.
+
+Unknown address: nonexisting at bork.bork.bork
+
+--===============0639262320==
+Content-Type: text/plain; charset="us-ascii"
+MIME-Version: 1.0
+Content-Transfer-Encoding: 7bit
+
+Content-Type: text/plain;
+  charset="iso-8859-1"
+From: Nonexisting User <nonexisting at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Message-Id: <dummy_test_message_id>
+Subject: [issue] Testing nonexisting user...
+
+This is a test submission of a new issue.
+
+--===============0639262320==--
+''')
+
     def testEnc01(self):
         self.doNewIssue()
         self._handle_mail('''Content-Type: text/plain;

Modified: tracker/roundup-src/test/test_xmlrpc.py
==============================================================================
--- tracker/roundup-src/test/test_xmlrpc.py	(original)
+++ tracker/roundup-src/test/test_xmlrpc.py	Fri Nov 27 12:58:04 2009
@@ -98,18 +98,20 @@
     def testAuthAllowedEdit(self):
         self.db.setCurrentUser('admin')
         try:
-            self.server.set('user2', 'realname=someone')
-        except Unauthorised, err:
-            self.fail('raised %s'%err)
+            try:
+                self.server.set('user2', 'realname=someone')
+            except Unauthorised, err:
+                self.fail('raised %s'%err)
         finally:
             self.db.setCurrentUser('joe')
 
     def testAuthAllowedCreate(self):
         self.db.setCurrentUser('admin')
         try:
-            self.server.create('user', 'username=blah')
-        except Unauthorised, err:
-            self.fail('raised %s'%err)
+            try:
+                self.server.create('user', 'username=blah')
+            except Unauthorised, err:
+                self.fail('raised %s'%err)
         finally:
             self.db.setCurrentUser('joe')
 


More information about the Python-checkins mailing list