[Python-checkins] r61320 - in tracker/roundup-src: CHANGES.txt README.txt demo.py doc/Makefile doc/admin_guide.txt doc/announcement.txt doc/customizing.txt doc/design.txt doc/features.txt doc/index.txt doc/installation.txt doc/mysql.txt doc/overview.txt doc/roundup-server.1 doc/roundup-server.ini.example doc/upgrading.txt doc/user_guide.txt frontends/ZRoundup/ZRoundup.py locale/es.po locale/hu.po locale/lt.po locale/roundup.pot locale/ru.po roundup/__init__.py roundup/admin.py roundup/backends/__init__.py roundup/backends/back_anydbm.py roundup/backends/back_metakit.py roundup/backends/back_mysql.py roundup/backends/back_postgresql.py roundup/backends/back_sqlite.py roundup/backends/blobfiles.py roundup/backends/indexer_xapian.py roundup/backends/rdbms_common.py roundup/backends/sessions_dbm.py roundup/backends/sessions_rdbms.py roundup/cgi/TranslationService.py roundup/cgi/actions.py roundup/cgi/client.py roundup/cgi/form_parser.py roundup/cgi/templating.py roundup/configuration.py roundup/date.py roundup/hyperdb.py roundup/mailer.py roundup/mailgw.py roundup/roundupdb.py roundup/scripts/roundup_server.py scripts/import_sf.py scripts/roundup-reminder setup.py templates/classic/detectors/messagesummary.py templates/classic/detectors/userauditor.py templates/classic/html/help_controls.js templates/classic/html/issue.index.html templates/classic/html/issue.item.html templates/classic/html/issue.search.html templates/classic/html/query.edit.html templates/classic/html/user.item.html templates/classic/html/user_utils.js templates/classic/schema.py templates/minimal/detectors/userauditor.py templates/minimal/html/help_controls.js templates/minimal/html/user.item.html test/db_test_base.py test/test_actions.py test/test_cgi.py test/test_dates.py test/test_mailgw.py test/test_metakit.py test/test_multipart.py

martin.v.loewis python-checkins at python.org
Sun Mar 9 09:26:21 CET 2008


Author: martin.v.loewis
Date: Sun Mar  9 09:26:16 2008
New Revision: 61320

Modified:
   tracker/roundup-src/CHANGES.txt
   tracker/roundup-src/README.txt
   tracker/roundup-src/demo.py
   tracker/roundup-src/doc/Makefile
   tracker/roundup-src/doc/admin_guide.txt
   tracker/roundup-src/doc/announcement.txt
   tracker/roundup-src/doc/customizing.txt
   tracker/roundup-src/doc/design.txt
   tracker/roundup-src/doc/features.txt
   tracker/roundup-src/doc/index.txt
   tracker/roundup-src/doc/installation.txt
   tracker/roundup-src/doc/mysql.txt
   tracker/roundup-src/doc/overview.txt
   tracker/roundup-src/doc/roundup-server.1
   tracker/roundup-src/doc/roundup-server.ini.example
   tracker/roundup-src/doc/upgrading.txt
   tracker/roundup-src/doc/user_guide.txt
   tracker/roundup-src/frontends/ZRoundup/ZRoundup.py
   tracker/roundup-src/locale/es.po
   tracker/roundup-src/locale/hu.po
   tracker/roundup-src/locale/lt.po
   tracker/roundup-src/locale/roundup.pot
   tracker/roundup-src/locale/ru.po
   tracker/roundup-src/roundup/__init__.py
   tracker/roundup-src/roundup/admin.py
   tracker/roundup-src/roundup/backends/__init__.py
   tracker/roundup-src/roundup/backends/back_anydbm.py
   tracker/roundup-src/roundup/backends/back_metakit.py
   tracker/roundup-src/roundup/backends/back_mysql.py
   tracker/roundup-src/roundup/backends/back_postgresql.py
   tracker/roundup-src/roundup/backends/back_sqlite.py
   tracker/roundup-src/roundup/backends/blobfiles.py
   tracker/roundup-src/roundup/backends/indexer_xapian.py
   tracker/roundup-src/roundup/backends/rdbms_common.py
   tracker/roundup-src/roundup/backends/sessions_dbm.py
   tracker/roundup-src/roundup/backends/sessions_rdbms.py
   tracker/roundup-src/roundup/cgi/TranslationService.py
   tracker/roundup-src/roundup/cgi/actions.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/date.py
   tracker/roundup-src/roundup/hyperdb.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_server.py
   tracker/roundup-src/scripts/import_sf.py
   tracker/roundup-src/scripts/roundup-reminder
   tracker/roundup-src/setup.py
   tracker/roundup-src/templates/classic/detectors/messagesummary.py
   tracker/roundup-src/templates/classic/detectors/userauditor.py
   tracker/roundup-src/templates/classic/html/help_controls.js
   tracker/roundup-src/templates/classic/html/issue.index.html
   tracker/roundup-src/templates/classic/html/issue.item.html
   tracker/roundup-src/templates/classic/html/issue.search.html
   tracker/roundup-src/templates/classic/html/query.edit.html
   tracker/roundup-src/templates/classic/html/user.item.html
   tracker/roundup-src/templates/classic/html/user_utils.js
   tracker/roundup-src/templates/classic/schema.py
   tracker/roundup-src/templates/minimal/detectors/userauditor.py
   tracker/roundup-src/templates/minimal/html/help_controls.js
   tracker/roundup-src/templates/minimal/html/user.item.html
   tracker/roundup-src/test/db_test_base.py
   tracker/roundup-src/test/test_actions.py
   tracker/roundup-src/test/test_cgi.py
   tracker/roundup-src/test/test_dates.py
   tracker/roundup-src/test/test_mailgw.py
   tracker/roundup-src/test/test_metakit.py
   tracker/roundup-src/test/test_multipart.py
Log:
Upgrade to roundup 1.4.2.
Patch contributed by Philipp Gortan.


Modified: tracker/roundup-src/CHANGES.txt
==============================================================================
--- tracker/roundup-src/CHANGES.txt	(original)
+++ tracker/roundup-src/CHANGES.txt	Sun Mar  9 09:26:16 2008
@@ -1,6 +1,107 @@
 This file contains the changes to the Roundup system over time. The entries
 are given with the most recent entry first.
 
+2008-02-07 1.4.2
+Feature:
+- New config option in mail section: ignore_alternatives allows to
+  ignore alternatives besides the text/plain part used for the content
+  of a message in multipart/alternative attachments.
+- Admin copy of error email from mailgw includes traceback (thanks Ulrik
+  Mikaelsson)
+- Messages created through the web are now given an in-reply-to header
+  when email out to nosy (thanks Martin v. Löwis)
+- Nosy messages now include more information about issues (all link
+  properties with a "name" attribute) (thanks Martin v. Löwis)
+
+Fixed:
+- Searching date range by supplying just a date as the filter spec
+- Handle no time.tzset under Windows (sf #1825643)
+- Fix race condition in file storage transaction commit (sf #1883580)
+- Make user utils JS work with firstname/lastname again (sf #1868323)
+- Fix ZRoundup to work with Zope 2.8.5 (sf #1806125)
+- Fix race condition for key properties in rdbms backends (sf #1876683)
+- Handle Reject in mailgw final set/create (sf #1826425)
+
+
+2007-11-09 1.4.1
+Fixed:
+- Removed some metakit references
+
+
+2007-11-04 1.4.0
+Feature:
+- Roundup has a new xmlrpc frontend that gives access to a tracker using
+  XMLRPC.
+- Dates can now be in the year-range 1-9999
+- The metakit backend has been removed
+- Add simple anti-spam recipe to docs
+- Allow customisation of regular expressions used in email parsing, thanks
+  Bruno Damour
+- Italian translation by Marco Ghidinelli
+- Multilinks take any iterable
+- config option: specify port and local hostname for SMTP connections
+- Tracker index templating (i.e. when roundup_server is serving multiple
+  trackers) (sf bug 1058020)
+- config option: Limit nosy attachments based on size (Philipp Gortan)
+- roundup_server supports SSL via pyopenssl
+- templatable 404 not found messages (sf bug 1403287)
+- Unauthorized email includes a link to the registration page for
+  the tracker
+- config options: control whether author info/email is included in email
+  sent by roundup
+- support for receiving OpenPGP MIME messages (signed or encrypted)
+
+Fixed:
+- Handling of unset Link search in RDBMS backend
+- Journal export of anydbm didn't correctly export previously empty values
+- Fix handling of defaults for date fields
+- Fix <form> name in user editing to allow multilink popups to work
+- Fix form handling of editing existing hyperdb items from a new item page.
+- Added new rdbms-indexes for full-text index which will speed up
+  reindexing.
+- Turning off indexing for content properties of FileClass instance
+  (e.g., "file" and "msg") now works for SQL backends.
+- Enabled over-riding of content-type in web interface (thanks
+  John Mitchell)
+- Validate user timezones to filter bad entries (sf bug 1738470)
+- Classic template allows searching for issues with no topic set
+  (sf bug 1610787)
+- xapian_indexer uses current API for stemming (Rick Benavidez)
+  (sf bug 1771414)
+- Ensure email addresses are unique (sf bug 1611787)
+- roundup_admin tracks uncommitted changes in interactive mode
+  for all backends (sf bug 1297014)
+- add template search path for easy_install (Marek Kubica)
+- don't spam the roundup admin on client shutdowns (Ulrik Mikaelsson)
+- respect umask on filestorage backends (Ulrik Mikaelsson) (sf bug 1744328)
+- cope with spam robots posting multiple instances of the same form
+- include the author of property-only changes in generated messages
+- fuller email validation in templates (sf feature 1216291)
+- cope with bad cookies from other apps on same domain (sf bug 1691708)
+- updated Spanish translation from Ramiro Morales
+- clean up query display of "Private to you items" (sf bug 1481394)
+- use local timezone for mail date header (sf bug 1658173)
+- allow CSV export of queries on selected issues (sf bug 1783492)
+- remove blobfiles on destroy (sf bug 1654132)
+- handle postgres exceptions during session cleanup (sf bug 1703116)
+- update Xapian indexer to use current API
+- handle export and import of old trackers that have data attached to
+  journal "create" events
+- fix a couple more old instances of "type" instead of "ENGINE" for mysql
+  backend
+- make LinkHTMLProperty handle non-existing keys (sf patch 1815895)
+
+
+2007-02-15 1.3.3
+Fixed:
+- If-Modified-Since handling was broken
+- Updated documentation for customising hard-coded searches in page.html
+- Updated Windows installation docs (thanks Bo Berglund)
+- Handle rounding of seconds generating invalid date values
+- Handle 8-bit untranslateable messages from database properties
+- Fix scripts/roundup-reminder date calculation (sf bug 1649979)
+- Improved due_date and timelog customisation docs (sf bug 1625124)
+
 
 2006-12-19 1.3.2
 Fixed:
@@ -760,9 +861,9 @@
 - anonymous user can no longer edit or view itself (sf bug 828901).
 - corrected typo in installation.html (sf bug 822967).
 - clarified listTemplates docstring.
-- print a nicer error message when the address is already in use 
+- print a nicer error message when the address is already in use
   (sf bug 798659).
-- remove empty lines before sending strings off to the csv parser 
+- remove empty lines before sending strings off to the csv parser
   (sf bug 821364).
 - centralised conversion of user-input data to hyperdb values (sf bug
   802405, sf bug 817217, sf rfe 816994)
@@ -786,7 +887,7 @@
 - tidied up forms in default stylesheet
 - force textareas to use monospace fonts, lessening surprise on the user
 - moved out parts of client.py to new modules:
-  * actions.py - the xxxAction and xxxPermission functions refactored into 
+  * actions.py - the xxxAction and xxxPermission functions refactored into
     Action classes
   * exceptions.py - all exceptions
   * form_parser.py - parsePropsFromForm & extractFormList in a FormParser
@@ -944,7 +1045,7 @@
 - audit some user properties for valid values (roles, address) (sf bugs
   742968 and 739653)
 - fix HTML file detection (hence history xref linking) (sf bug 741478)
-- session database caches it's type, rather than calling whichdb each time 
+- session database caches it's type, rather than calling whichdb each time
   around.
 - changed rdbms_common to fix sql backends for new Boolean types under Py2.3
 
@@ -979,7 +1080,7 @@
   cc addresses, different from address and different nosy list property)
   (thanks John Rouillard)
 - applied patch for nicer history display (sf feature 638280)
-- cleaning old unused sessions only once per hour, not on every cgi 
+- cleaning old unused sessions only once per hour, not on every cgi
   request. It is greatly improves web interface performance, especially
   on trackers under high load
 - added mysql backend (see doc/mysql.txt for details)
@@ -1037,7 +1138,7 @@
 
 Fixed:
 - applied unicode patch. All data is stored in utf-8. Incoming messages
-  converted from any encoding to utf-8, outgoing messages are encoded 
+  converted from any encoding to utf-8, outgoing messages are encoded
   according to rfc2822 (sf bug 568873)
 - fixed layout issues with forms in sidebar
 - fixed timelog example so it handles new issues (sf bug 678908)
@@ -1120,7 +1221,7 @@
 - handle :add: better in cgi form parsing (sf bug 663235)
 - handle all-whitespace multilink values in forms (sf bug 663855)
 - fixed searching on date / interval fields (sf bug 658157)
-- fixed form elements names in search form to allow grouping and sorting 
+- fixed form elements names in search form to allow grouping and sorting
   on "creation" field
 - display of saved queries is now performed correctly
 
@@ -1310,7 +1411,7 @@
 -  daemonify roundup-server (fork, logfile, pidfile)
 -  modify cgitb to display PageTemplate errors better
 -  rename to "instance" to "tracker"
--  have roundup.cgi pick up tracker config from the environment 
+-  have roundup.cgi pick up tracker config from the environment
 -  revamped look and feel in web interface
 -  cleaned up stylesheet usage
 -  several bug fixes and documentation fixes
@@ -1344,7 +1445,7 @@
      done in the default templates.
    - the regeneration of the indexes (if necessary) is done once the schema is
      set up in the dbinit.
-   - new "reindex" command in roundup-admin used to force regeneration of the 
+   - new "reindex" command in roundup-admin used to force regeneration of the
      index
 -  added email display function - mangles email addrs so they're not so easily
    scraped from the web
@@ -1422,7 +1523,7 @@
    wants to ignore
 -  fixed the example addresses in the templates to use correct example domains
 -  cleaned out the template stylesheets, removing a bunch of junk that really
-   wasn't necessary (font specs, styles never used) and added a style for 
+   wasn't necessary (font specs, styles never used) and added a style for
    message content
 -  build htmlbase if tests are run using CVS checkout
 -  #565979 ] code error in hyperdb.Class.find
@@ -1435,7 +1536,7 @@
 -  #565992 ] if ISSUE_TRACKER_WEB doesn't have the trailing '/', add it
 -  use the rfc822 module to ensure that every (oddball) email address and
    real-name is properly quoted
--  #558867 ] ZRoundup redirect /instance requests to /instance/ 
+-  #558867 ] ZRoundup redirect /instance requests to /instance/
 -  #569415 ] {version}
 -  #569178 ] type error
    was fixed as part of the general cleanup of reactors
@@ -1499,13 +1600,13 @@
 2002-01-24 - 0.4.0
 Feature:
 -  much nicer history display (actualy real handling of property types etc)
--  journal entries for link and mutlilink properties can be switched on or 
+-  journal entries for link and mutlilink properties can be switched on or
    off
 -  properties in change note are now sorted
 -  you can now use the roundup-admin tool pack the database
 
 Fixed:
--  the mail gateway now responds with an error message when invalid values 
+-  the mail gateway now responds with an error message when invalid values
    for arguments are specified for link or mutlilink properties
 -  modified unit test to check nosy and assignedto when specified as arguments
 -  handle attachments with no name (eg tnef)
@@ -1612,7 +1713,7 @@
 -  added tests for mailgw
 
 
-2001-11-23 - 0.3.0 
+2001-11-23 - 0.3.0
 Feature:
 -  #467129 ] Lossage when username=e-mail-address
 -  #473123 ] Change message generation for author
@@ -1920,7 +2021,7 @@
 -  Added the "classic" template - a direct implementation of the Roundup
    spec. Well, as close as we're going to get, anyway.
 -  Added an issue priority of support to "extended"
--  Added command-line arg handling to roundup-server so it's more useful 
+-  Added command-line arg handling to roundup-server so it's more useful
    out-of-the-box.
 -  Added distutils-style installation of "lib" files.
 -  Added some unit tests.

Modified: tracker/roundup-src/README.txt
==============================================================================
--- tracker/roundup-src/README.txt	(original)
+++ tracker/roundup-src/README.txt	Sun Mar  9 09:26:16 2008
@@ -31,11 +31,14 @@
 Upgrading
 =========
 For upgrading instructions, please see upgrading.txt in the "doc" directory.
-
+ 
 
 Usage and Other Information
 ===========================
 See the index.txt file in the "doc" directory.
+The *.txt files in the "doc" directory are written in reStructedText. If
+you have rst2html installed (part of the docutils suite) you can convert
+these to HTML by running "make html" in the "doc" directory.
 
 
 License

Modified: tracker/roundup-src/demo.py
==============================================================================
--- tracker/roundup-src/demo.py	(original)
+++ tracker/roundup-src/demo.py	Sun Mar  9 09:26:16 2008
@@ -2,7 +2,7 @@
 #
 # Copyright (c) 2003 Richard Jones (richard at mechanicalcat.net)
 #
-# $Id: demo.py,v 1.25 2006/08/07 07:15:05 richard Exp $
+# $Id: demo.py,v 1.26 2007/08/28 22:37:45 jpend Exp $
 
 import errno
 import os
@@ -103,6 +103,13 @@
 2. Hit Control-C to stop the server.
 3. Re-start the server by running "roundup-demo" again.
 4. Re-initialise the server by running "roundup-demo nuke".
+
+Demo tracker is set up to be accessed by localhost browser.  If you
+run demo on a server host, please stop the demo, open file
+"demo/config.ini" with your editor, change the host name in the "web"
+option in section "[tracker]", save the file, then re-run the demo
+program.
+
 ''' % url
 
     # disable command line processing in roundup_server

Modified: tracker/roundup-src/doc/Makefile
==============================================================================
--- tracker/roundup-src/doc/Makefile	(original)
+++ tracker/roundup-src/doc/Makefile	Sun Mar  9 09:26:16 2008
@@ -5,12 +5,14 @@
 SOURCE = announcement.txt customizing.txt developers.txt FAQ.txt features.txt \
     glossary.txt implementation.txt index.txt design.txt mysql.txt \
     installation.txt upgrading.txt user_guide.txt admin_guide.txt \
-	postgresql.txt tracker_templates.txt
+	postgresql.txt tracker_templates.txt xmlrpc.txt
 
 COMPILED := $(SOURCE:.txt=.html)
 WEBHT := $(SOURCE:.txt=.ht)
 
-all: ${COMPILED} ${WEBHT}
+all: html ht
+html: ${COMPILED}
+ht: ${WEBHT}
 
 website: ${WEBHT}
 	cp *.ht ${WEBDIR}

Modified: tracker/roundup-src/doc/admin_guide.txt
==============================================================================
--- tracker/roundup-src/doc/admin_guide.txt	(original)
+++ tracker/roundup-src/doc/admin_guide.txt	Sun Mar  9 09:26:16 2008
@@ -2,7 +2,7 @@
 Administration Guide
 ====================
 
-:Version: $Revision: 1.23 $
+:Version: $Revision: 1.27 $
 
 .. contents::
 
@@ -82,6 +82,9 @@
     ;log_ip = yes
     ;pidfile =
     ;logfile =
+    ;template =
+    ;ssl = no
+    ;pem =
 
     [trackers]
     ; Add one of these per tracker being served
@@ -109,6 +112,18 @@
   written to this file. It must be specified if **pidfile** is specified.
   If per-tracker logging is specified, then very little will be written to
   this file.
+**template**
+  Specifies a template used for displaying the tracker index when
+  multiple trackers are being used. The variable "trackers" is available
+  to the template and is a dict of all configured trackers.
+**ssl**
+  Enables the use of SSL to secure the connection to the roundup-server.
+  If you enable this, ensure that your tracker's config.ini specifies
+  an *https* URL.
+**pem**
+  If specified, the SSL PEM file containing the private key and certificate.
+  If not specified, roundup will generate a temporary, self-signed certificate
+  for use.
 **trackers** section
   Each line denotes a mapping from a URL component to a tracker home.
   Make sure the name part doesn't include any url-unsafe characters like
@@ -168,8 +183,15 @@
 Tracker Backup
 --------------
 
-Stop the web and email frontends and to copy the contents of the tracker home
-directory to some other place using standard backup tools.
+The roundup-admin import and export commands are **not** recommended for
+performing backup.
+
+Optionally stop the web and email frontends and to copy the contents of the
+tracker home directory to some other place using standard backup tools.
+This means using
+*pg_dump* to take a snapshot of your Postgres backend database, for example.
+A simple copy of the tracker home (and files storage area if you've configured
+it to be elsewhere) will then complete the backup.
 
 
 Software Upgrade
@@ -187,6 +209,12 @@
 4. Stop the tracker web and email frontends.
 5. Follow the steps in the `upgrading documentation`_ for the new version of
    the software in the copied.
+
+   Usually you will be asked to run `roundup_admin migrate` on your tracker
+   before you allow users to start accessing the tracker.
+
+   It's safe to run this even if it's not required, so just get into the
+   habit.
 6. You may test each of the admin tool, web interface and mail gateway using
    the new version of the software. To do this, invoke the scripts directly
    in the source directory with::

Modified: tracker/roundup-src/doc/announcement.txt
==============================================================================
--- tracker/roundup-src/doc/announcement.txt	(original)
+++ tracker/roundup-src/doc/announcement.txt	Sun Mar  9 09:26:16 2008
@@ -1,24 +1,24 @@
-I'm proud to release version 1.3.2 of Roundup.
+I'm proud to release version 1.4.2 of Roundup.
 
-Fixed in 1.3.2:
-
-- relax rules for required fields in form_parser.py (sf bug 1599740)
-- documentation cleanup from Luke Ross (sf patch 1594860)
-- updated Spanish translation from Ramiro Morales (sf patch 1594718)
-- handle 8-bit untranslateable messages in tracker templates
-- handling of required for boolean False and numeric 0 (sf bug 1608200)
-- removed bogus args attr of ConfigurationError (sf bug 1608056)
-- implemented start_response in roundup.cgi (sf bug 1604304)
-- clarified windows service documentation (sf patch 1597713)
-- HTMLClass fixed to work with new item permissions check (sf bug 1602983)
-- support POP over SSL (sf patch 1597703)
-- clean up input field generation and quoting of values (sf bug 1615616)
-- allow use of roundup-server pidfile without forking (sf bug 1614753)
-- allow translation of status/priority menu options (sf bug 1613976)
-
-New Features in 1.3.0:
-
-- WSGI support via roundup.cgi.wsgi_handler
+New Features in 1.4.2:
+- New config option in mail section: ignore_alternatives allows to
+  ignore alternatives besides the text/plain part used for the content
+  of a message in multipart/alternative attachments.
+- Admin copy of error email from mailgw includes traceback (thanks Ulrik
+  Mikaelsson)
+- Messages created through the web are now given an in-reply-to header
+  when email out to nosy (thanks Martin v. Löwis)
+- Nosy messages now include more information about issues (all link
+  properties with a "name" attribute) (thanks Martin v. Löwis)
+
+And things fixed:
+- Searching date range by supplying just a date as the filter spec
+- Handle no time.tzset under Windows (sf #1825643)
+- Fix race condition in file storage transaction commit (sf #1883580)
+- Make user utils JS work with firstname/lastname again (sf #1868323)
+- Fix ZRoundup to work with Zope 2.8.5 (sf #1806125)
+- Fix race condition for key properties in rdbms backends (sf #1876683)
+- Handle Reject in mailgw final set/create (sf #1826425)
 
 If you're upgrading from an older version of Roundup you *must* follow
 the "Software Upgrade" guidelines given in the maintenance documentation.
@@ -62,6 +62,6 @@
 disutils-based 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,
-mysql and postgresql).
+a minimal skeleton) and four database back-ends (anydbm, sqlite, mysql
+and postgresql).
 

Modified: tracker/roundup-src/doc/customizing.txt
==============================================================================
--- tracker/roundup-src/doc/customizing.txt	(original)
+++ tracker/roundup-src/doc/customizing.txt	Sun Mar  9 09:26:16 2008
@@ -2,7 +2,7 @@
 Customising Roundup
 ===================
 
-:Version: $Revision: 1.215 $
+:Version: $Revision: 1.222 $
 
 .. This document borrows from the ZopeBook section on ZPT. The original is at:
    http://www.zope.org/Documentation/Books/ZopeBook/current/ZPT.stx
@@ -95,6 +95,14 @@
   Path to the HTML templates directory. The path may be either absolute
   or relative to the directory containig this config file.
 
+ static_files -- default *blank*
+  Path to directory holding additional static files available via Web
+  UI.  This directory may contain sitewide images, CSS stylesheets etc.
+  and is searched for these files prior to the TEMPLATES directory
+  specified above.  If this option is not set, all static files are
+  taken from the TEMPLATES directory The path may be either absolute or
+  relative to the directory containig this config file.
+
  admin_email -- ``roundup-admin``
   Email address that roundup will complain to if it runs into trouble. If
   the email address doesn't contain an ``@`` part, the MAIL_DOMAIN defined
@@ -150,6 +158,9 @@
   your tracker. See the indexer source for the default list of
   stop-words (e.g. ``A,AND,ARE,AS,AT,BE,BUT,BY, ...``).
 
+ umask -- ``02``
+  Defines the file creation mode mask.
+
 Section **tracker**
  name -- ``Roundup issue tracker``
   A descriptive name for your roundup instance.
@@ -164,6 +175,11 @@
  email -- ``issue_tracker``
   Email address that mail to roundup should go to.
 
+ language -- default *blank*
+  Default locale name for this tracker. If this option is not set, the
+  language is determined by the environment variable LANGUAGE, LC_ALL,
+  LC_MESSAGES, or LANG, in that order of preference.
+
 Section **web**
  http_auth -- ``yes``
   Whether to use HTTP Basic Authentication, if present.
@@ -204,6 +220,13 @@
  password -- ``roundup``
   Database user password.
 
+ read_default_file -- ``~/.my.cnf``
+  Name of the MySQL defaults file. Only used in MySQL connections.
+
+ read_default_group -- ``roundup``
+  Name of the group to use in the MySQL defaults file. Only used in
+  MySQL connections.
+
 Section **logging**
  config -- default *blank*
   Path to configuration file for standard Python logging module. If this
@@ -240,6 +263,15 @@
   SMTP login password.
   Set this if your mail host requires authenticated access.
 
+ port -- default *25*
+  SMTP port on mail host.
+  Set this if your mail host runs on a different port.
+
+ local_hostname -- default *blank*
+  The fully qualified domain name (FQDN) to use during SMTP sessions. If left
+  blank, the underlying SMTP library will attempt to detect your FQDN. If your
+  mail host requires something specific, specify the FQDN to use.
+
  tls -- ``no``
   If your SMTP mail host provides or requires TLS (Transport Layer Security)
   then you may set this option to 'yes'.
@@ -268,6 +300,16 @@
   precedence. The path may be either absolute or relative to the directory
   containig this config file.
 
+ add_authorinfo -- ``yes``
+  Add a line with author information at top of all messages send by
+  roundup.
+
+ add_authoremail -- ``yes``
+  Add the mail address of the author to the author information at the
+  top of all messages.  If this is false but add_authorinfo is true,
+  only the name of the actor is added which protects the mail address
+  of the actor from being exposed at mail archives, etc.
+
 Section **mailgw**
  Roundup Mail Gateway options
 
@@ -285,6 +327,10 @@
   Default class to use in the mailgw if one isn't supplied in email subjects.
   To disable, leave the value blank.
 
+ language -- default *blank*
+  Default locale name for the tracker mail gateway.  If this option is
+  not set, mail gateway will use the language of the tracker instance.
+
  subject_prefix_parsing -- ``strict``
   Controls the parsing of the [prefix] on subject lines in incoming emails.
   ``strict`` will return an error to the sender if the [prefix] is not
@@ -310,6 +356,42 @@
   an issue for the interval after the issue's creation or last activity.
   The interval is a standard Roundup interval.
 
+ refwd_re -- ``(\s*\W?\s*(fw|fwd|re|aw|sv|ang)\W)+``
+  Regular expression matching a single reply or forward prefix
+  prepended by the mailer. This is explicitly stripped from the
+  subject during parsing.  Value is Python Regular Expression
+  (UTF8-encoded).
+
+ origmsg_re -- `` ^[>|\s]*-----\s?Original Message\s?-----$``
+  Regular expression matching start of an original message if quoted
+  the in body.  Value is Python Regular Expression (UTF8-encoded).
+
+ sign_re -- ``^[>|\s]*-- ?$``
+  Regular expression matching the start of a signature in the message
+  body.  Value is Python Regular Expression (UTF8-encoded).
+
+ eol_re -- ``[\r\n]+``
+  Regular expression matching end of line.  Value is Python Regular
+  Expression (UTF8-encoded).
+
+ blankline_re -- ``[\r\n]+\s*[\r\n]+``
+  Regular expression matching a blank line.  Value is Python Regular
+  Expression (UTF8-encoded).
+
+Section **pgp**
+ OpenPGP mail processing options
+
+ enable -- ``no``
+  Enable PGP processing. Requires pyme.
+
+ roles -- default *blank*
+  If specified, a comma-separated list of roles to perform PGP
+  processing on. If not specified, it happens for all users.
+
+ homedir -- default *blank*
+  Location of PGP directory. Defaults to $HOME/.gnupg if not
+  specified.
+
 Section **nosy**
  Nosy messages sending
 
@@ -340,6 +422,12 @@
   a separate email is sent to each recipient. If ``single`` then a single
   email is sent with each recipient as a CC address.
 
+ max_attachment_size -- ``2147483647``
+  Attachments larger than the given number of bytes won't be attached
+  to nosy mails. They will be replaced by a link to the tracker's
+  download page for the file.
+
+
 You may generate a new default config file using the ``roundup-admin
 genconfig`` command.
 
@@ -436,7 +524,7 @@
 
     file = FileClass(db, "file", name=String())
 
-    issue = IssueClass(db, "issue", topic=Multilink("keyword"),
+    issue = IssueClass(db, "issue", keyword=Multilink("keyword"),
         status=Link("status"), assignedto=Link("user"),
         priority=Link("priority"))
     issue.setkey('title')
@@ -2472,10 +2560,10 @@
 been added for clarity)::
 
     /issue?status=unread,in-progress,resolved&
-        topic=security,ui&
+        keyword=security,ui&
         @group=priority,-status&
         @sort=-activity&
-        @filters=status,topic&
+        @filters=status,keyword&
         @columns=title,status,fixer
 
 The index view is determined by two parts of the specifier: the layout
@@ -2494,11 +2582,11 @@
 
 The example specifies an index of "issue" items. Only items with a
 "status" of either "unread" or "in-progress" or "resolved" are
-displayed, and only items with "topic" values including both "security"
+displayed, and only items with "keyword" values including both "security"
 and "ui" are displayed. The items are grouped by priority arranged in
 ascending order and in descending order by status; and within
 groups, sorted by activity, arranged in descending order. The filter
-section shows filters for the "status" and "topic" properties, and the
+section shows filters for the "status" and "keyword" properties, and the
 table includes columns for the "title", "status", and "fixer"
 properties.
 
@@ -2892,28 +2980,42 @@
 tracker access (note that roundup-server would need to be restarted as it
 caches the schema).
 
-1. modify the ``schema.py``::
+1. Modify the ``schema.py``::
 
     issue = IssueClass(db, "issue", 
-                    assignedto=Link("user"), topic=Multilink("keyword"),
+                    assignedto=Link("user"), keyword=Multilink("keyword"),
                     priority=Link("priority"), status=Link("status"),
                     due_date=Date())
 
-2. add an edit field to the ``issue.item.html`` template::
+2. Add an edit field to the ``issue.item.html`` template::
 
     <tr> 
      <th>Due Date</th> 
      <td tal:content="structure context/due_date/field" /> 
-    </tr> 
+    </tr>
+    
+   If you want to show only the date part of due_date then do this instead::
+   
+    <tr> 
+     <th>Due Date</th> 
+     <td tal:content="structure python:context.due_date.field(format='%Y-%m-%d')" /> 
+    </tr>
 
-3. add the property to the ``issue.index.html`` page::
+3. Add the property to the ``issue.index.html`` page::
 
     (in the heading row)
       <th tal:condition="request/show/due_date">Due Date</th>
     (in the data row)
-      <td tal:condition="request/show/due_date" tal:content="i/due_date" />
+      <td tal:condition="request/show/due_date" 
+          tal:content="i/due_date" />
+          
+   If you want format control of the display of the due date you can
+   enter the following in the data row to show only the actual due date::
+    
+      <td tal:condition="request/show/due_date" 
+          tal:content="python:i.due_date.pretty('%Y-%m-%d')">&nbsp;</td>
 
-4. add the property to the ``issue.search.html`` page::
+4. Add the property to the ``issue.search.html`` page::
 
      <tr tal:define="name string:due_date">
        <th i18n:translate="">Due Date:</th>
@@ -2923,11 +3025,12 @@
        <td metal:use-macro="group_input"></td>
      </tr>
 
-5. if you wish for the due date to appear in the standard views listed
-    in the sidebar of the web interface then you'll need to add "due_date"
-    to the list of @columns in the links in the sidebar section of
-    ``page.html``.
-
+5. If you wish for the due date to appear in the standard views listed
+   in the sidebar of the web interface then you'll need to add "due_date"
+   to the columns and columns_showall lists in your ``page.html``::
+    
+    columns string:id,activity,due_date,title,creator,status;
+    columns_showall string:id,activity,due_date,title,creator,assignedto,status;
 
 Adding a new constrained field to the classic schema
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -3374,7 +3477,7 @@
    ``schema.py``)::
 
     issue = IssueClass(db, "issue", 
-                    assignedto=Link("user"), topic=Multilink("keyword"),
+                    assignedto=Link("user"), keyword=Multilink("keyword"),
                     priority=Link("priority"), status=Link("status"),
                     times=Multilink("timelog"))
 
@@ -3392,7 +3495,7 @@
     <tr> 
      <th>Time Log</th> 
      <td colspan=3><input type="text" name="timelog-1 at period" /> 
-      <br />(enter as '3y 1m 4d 2:40:02' or parts thereof) 
+      (enter as '3y 1m 4d 2:40:02' or parts thereof) 
      </td> 
     </tr> 
          
@@ -3405,6 +3508,17 @@
    On submission, the "-1" timelog item will be created and assigned a
    real item id. The "times" property of the issue will have the new id
    added to it.
+   
+   The full entry will now look like this::
+   
+    <tr> 
+     <th>Time Log</th> 
+     <td colspan=3><input type="text" name="timelog-1 at period" /> 
+      (enter as '3y 1m 4d 2:40:02' or parts thereof)
+      <input type="hidden" name="@link at times" value="timelog-1" /> 
+     </td> 
+    </tr> 
+   
 
 4. We want to display a total of the timelog times that have been
    accumulated for an issue. To do this, we'll need to actually write
@@ -3451,7 +3565,7 @@
    displayed in the template as text like "+ 1y 2:40" (1 year, 2 hours
    and 40 minutes).
 
-8. If you're using a persistent web server - ``roundup-server`` or
+6. If you're using a persistent web server - ``roundup-server`` or
    ``mod_python`` for example - then you'll need to restart that to pick up
    the code changes. When that's done, you'll be able to use the new
    time logging interface.
@@ -3459,7 +3573,7 @@
 An extension of this modification attaches the timelog entries to any
 change message entered at the time of the timelog entry:
 
-1. Add a link to the timelog to the msg class:
+A. Add a link to the timelog to the msg class in ``schema.py``:
 
     msg = FileClass(db, "msg",
                     author=Link("user", do_journal='no'),
@@ -3468,19 +3582,51 @@
                     summary=String(),
                     files=Multilink("file"),
                     messageid=String(),
-                    inreplyto=String()
+                    inreplyto=String(),
                     times=Multilink("timelog"))
 
-2. Add a new hidden field that links that new timelog item (new
+B. Add a new hidden field that links that new timelog item (new
    because it's marked as having id "-1") to the new message.
-   It looks like this::
- 
-      <input type="hidden" name="msg-1 at link@times" value="timelog-1" />
+   The link is placed in ``issue.item.html`` in the same section that
+   handles the timelog entry.
+   
+   It looks like this after this addition::
+
+    <tr> 
+     <th>Time Log</th> 
+     <td colspan=3><input type="text" name="timelog-1 at period" /> 
+      (enter as '3y 1m 4d 2:40:02' or parts thereof)
+      <input type="hidden" name="@link at times" value="timelog-1" />
+      <input type="hidden" name="msg-1 at link@times" value="timelog-1" /> 
+     </td> 
+    </tr> 
  
    The "times" property of the message will have the new id added to it.
 
-3. Add the timelog listing from step 5. to the ``msg.item.html`` template
-   so that the timelog entry appears on the message view page.
+C. Add the timelog listing from step 5. to the ``msg.item.html`` template
+   so that the timelog entry appears on the message view page. Note that
+   the call to totalTimeSpent is not used here since there will only be one
+   single timelog entry for each message.
+   
+   I placed it after the Date entry like this::
+   
+    <tr>
+     <th i18n:translate="">Date:</th>
+     <td tal:content="context/date"></td>
+    </tr>
+    </table>
+    
+    <table class="otherinfo" tal:condition="context/times">
+     <tr><th colspan="3" class="header">Time Log</th></tr>
+     <tr><th>Date</th><th>Period</th><th>Logged By</th></tr>
+     <tr tal:repeat="time context/times">
+      <td tal:content="time/creation"></td>
+      <td tal:content="time/period"></td>
+      <td tal:content="time/creator"></td>
+     </tr>
+    </table>
+    
+    <table class="messages">
 
 
 Tracking different types of issues
@@ -3504,7 +3650,7 @@
 
     # store issues related to those systems
     support = IssueClass(db, "support", 
-                    assignedto=Link("user"), topic=Multilink("keyword"),
+                    assignedto=Link("user"), keyword=Multilink("keyword"),
                     status=Link("status"), deadline=Date(),
                     affects=Multilink("system"))
 
@@ -3776,6 +3922,37 @@
 Changes to Tracker Behaviour
 ----------------------------
 
+Preventing SPAM
+~~~~~~~~~~~~~~~
+
+The following detector code may be installed in your tracker's
+``detectors`` directory. It will block any messages being created that
+have HTML attachments (a very common vector for spam and phishing)
+and any messages that have more than 2 HTTP URLs in them. Just copy
+the following into ``detectors/anti_spam.py`` in your tracker::
+
+    from roundup.exceptions import Reject
+
+    def reject_html(db, cl, nodeid, newvalues):
+        if newvalues['type'] == 'text/html':
+        raise Reject, 'not allowed'
+
+    def reject_manylinks(db, cl, nodeid, newvalues):
+        content = newvalues['content']
+        if content.count('http://') > 2:
+        raise Reject, 'not allowed'
+
+    def init(db):
+        db.file.audit('create', reject_html)
+        db.msg.audit('create', reject_manylinks)
+
+You may also wish to block image attachments if your tracker does not
+need that ability::
+
+    if newvalues['type'].startswith('image/'):
+        raise Reject, 'not allowed'
+
+
 Stop "nosy" messages going to people on vacation
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -3957,14 +4134,14 @@
    this class in your tracker's ``schema.py`` file. Change this::
 
     issue = IssueClass(db, "issue", 
-                    assignedto=Link("user"), topic=Multilink("keyword"),
+                    assignedto=Link("user"), keyword=Multilink("keyword"),
                     priority=Link("priority"), status=Link("status"))
 
    to this, adding the blockers entry::
 
     issue = IssueClass(db, "issue", 
                     blockers=Multilink("issue"),
-                    assignedto=Link("user"), topic=Multilink("keyword"),
+                    assignedto=Link("user"), keyword=Multilink("keyword"),
                     priority=Link("priority"), status=Link("status"))
 
 2. Add the new ``blockers`` property to the ``issue.item.html`` edit
@@ -3974,12 +4151,14 @@
     <td>
      <span tal:replace="structure python:context.blockers.field(showid=1,
                                   size=20)" />
-     <span tal:replace="structure python:db.issue.classhelp('id,title')" />
+     <span tal:replace="structure python:db.issue.classhelp('id,title',
+                                  property='blockers')" />
      <span tal:condition="context/blockers"
            tal:repeat="blk context/blockers">
       <br>View: <a tal:attributes="href string:issue${blk/id}"
                    tal:content="blk/id"></a>
      </span>
+    </td>
 
    You'll need to fiddle with your item page layout to find an
    appropriate place to put it - I'll leave that fun part up to you.
@@ -4067,16 +4246,33 @@
    example, the existing "Show All" link in the "page" template (in the
    tracker's "html" directory) looks like this::
 
-     <a href="issue?@sort=-activity&@group=priority&@filter=status&
-        @columns=id,activity,title,creator,assignedto,status&
-        status=-1,1,2,3,4,5,6,7">Show All</a><br>
+    <a href="#"
+       tal:attributes="href python:request.indexargs_url('issue', {
+      '@sort': '-activity',
+      '@group': 'priority',
+      '@filter': 'status',
+      '@columns': columns_showall,
+      '@search_text': '',
+      'status': status_notresolved,
+      '@dispname': i18n.gettext('Show All'),
+     })"
+       i18n:translate="">Show All</a><br>
 
    modify it to add the "blockers" info to the URL (note, both the
    "@filter" *and* "blockers" values must be specified)::
 
-     <a href="issue?@sort=-activity&@group=priority&@filter=status,blockers&
-        blockers=-1&@columns=id,activity,title,creator,assignedto,status&
-        status=-1,1,2,3,4,5,6,7">Show All</a><br>
+    <a href="#"
+       tal:attributes="href python:request.indexargs_url('issue', {
+      '@sort': '-activity',
+      '@group': 'priority',
+      '@filter': 'status,blockers',
+      '@columns': columns_showall,
+      '@search_text': '',
+      'status': status_notresolved,
+      'blockers': '-1',
+      '@dispname': i18n.gettext('Show All'),
+     })"
+       i18n:translate="">Show All</a><br>
 
    The above examples are line-wrapped on the trailing & and should
    be unwrapped.
@@ -4087,33 +4283,32 @@
 history at the bottom of the issue page - look for a "link" event to
 another issue's "blockers" property.
 
-Add users to the nosy list based on the topic
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Add users to the nosy list based on the keyword
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 Let's say we need the ability to automatically add users to the nosy
 list based
-on the occurance of a topic. Every user should be allowed to edit their
-own list of topics for which they want to be added to the nosy list.
+on the occurance of a keyword. Every user should be allowed to edit their
+own list of keywords for which they want to be added to the nosy list.
 
 Below, we'll show that this change can be done with minimal
 understanding of the Roundup system, using only copy and paste.
 
 This requires three changes to the tracker: a change in the database to
-allow per-user recording of the lists of topics for which he wants to
+allow per-user recording of the lists of keywords for which he wants to
 be put on the nosy list, a change in the user view allowing them to edit
-this list of topics, and addition of an auditor which updates the nosy
-list when a topic is set.
+this list of keywords, and addition of an auditor which updates the nosy
+list when a keyword is set.
 
-Adding the nosy topic list
-::::::::::::::::::::::::::
+Adding the nosy keyword list
+::::::::::::::::::::::::::::
 
-The change to make in the database, is that for any user there should be
-a list of topics for which he wants to be put on the nosy list. Adding
-a ``Multilink`` of ``keyword`` seems to fullfill this (note that within
-the code, topics are called ``keywords``.) As such, all that has to be
-done is to add a new field to the definition of ``user`` within the
-file ``schema.py``.  We will call this new field ``nosy_keywords``, and
-the updated definition of user will be::
+The change to make in the database, is that for any user there should be a list
+of keywords for which he wants to be put on the nosy list. Adding a
+``Multilink`` of ``keyword`` seems to fullfill this. As such, all that has to
+be done is to add a new field to the definition of ``user`` within the file
+``schema.py``.  We will call this new field ``nosy_keywords``, and the updated
+definition of user will be::
 
     user = Class(db, "user", 
                     username=String(),   password=Password(),
@@ -4124,22 +4319,22 @@
                     timezone=String(),
                     nosy_keywords=Multilink('keyword'))
  
-Changing the user view to allow changing the nosy topic list
-::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+Changing the user view to allow changing the nosy keyword list
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 
-We want any user to be able to change the list of topics for which
+We want any user to be able to change the list of keywords for which
 he will by default be added to the nosy list. We choose to add this
 to the user view, as is generated by the file ``html/user.item.html``.
 We can easily 
-see that the topic field in the issue view has very similar editing
-requirements as our nosy topics, both being lists of topics. As
-such, we look for Topics in ``issue.item.html``, and extract the
+see that the keyword field in the issue view has very similar editing
+requirements as our nosy keywords, both being lists of keywords. As
+such, we look for Keywords in ``issue.item.html``, and extract the
 associated parts from there. We add this to ``user.item.html`` at the 
 bottom of the list of viewed items (i.e. just below the 'Alternate
 E-mail addresses' in the classic template)::
 
  <tr>
-  <th>Nosy Topics</th>
+  <th>Nosy Keywords</th>
   <td>
   <span tal:replace="structure context/nosy_keywords/field" />
   <span tal:replace="structure python:db.keyword.classhelp(property='nosy_keywords')" />
@@ -4152,7 +4347,7 @@
 
 The more difficult part is the logic to add
 the users to the nosy list when required. 
-We choose to perform this action whenever the topics on an
+We choose to perform this action whenever the keywords on an
 item are set (this includes the creation of items).
 Here we choose to start out with a copy of the 
 ``detectors/nosyreaction.py`` detector, which we copy to the file
@@ -4173,8 +4368,8 @@
 code, which handled adding the assignedto user(s) to the nosy list in
 ``updatenosy``, should be replaced by a block of code to add the
 interested users to the nosy list. We choose here to loop over all
-new topics, than looping over all users,
-and assign the user to the nosy list when the topic occurs in the user's
+new keywords, than looping over all users,
+and assign the user to the nosy list when the keyword occurs in the user's
 ``nosy_keywords``. The next part in ``updatenosy`` -- adding the author
 and/or recipients of a message to the nosy list -- is obviously not
 relevant here and is thus deleted from the new auditor. The last
@@ -4182,7 +4377,7 @@
 This results in the following function::
 
     def update_kw_nosy(db, cl, nodeid, newvalues):
-        '''Update the nosy list for changes to the topics
+        '''Update the nosy list for changes to the keywords
         '''
         # nodeid will be None if this is a new node
         current = {}
@@ -4207,17 +4402,17 @@
                 if not current.has_key(value):
                     current[value] = 1
 
-        # add users with topic in nosy_keywords to the nosy list
-        if newvalues.has_key('topic') and newvalues['topic'] is not None:
-            topic_ids = newvalues['topic']
-            for topic in topic_ids:
+        # add users with keyword in nosy_keywords to the nosy list
+        if newvalues.has_key('keyword') and newvalues['keyword'] is not None:
+            keyword_ids = newvalues['keyword']
+            for keyword in keyword_ids:
                 # loop over all users,
-                # and assign user to nosy when topic in nosy_keywords
+                # and assign user to nosy when keyword in nosy_keywords
                 for user_id in db.user.list():
                     nosy_kw = db.user.get(user_id, "nosy_keywords")
                     found = 0
                     for kw in nosy_kw:
-                        if kw == topic:
+                        if kw == keyword:
                             found = 1
                     if found:
                         current[user_id] = 1
@@ -4237,10 +4432,10 @@
 Multiple additions
     When a user, after automatic selection, is manually removed
     from the nosy list, he is added to the nosy list again when the
-    topic list of the issue is updated. A better design might be
-    to only check which topics are new compared to the old list
-    of topics, and only add users when they have indicated
-    interest on a new topic.
+    keyword list of the issue is updated. A better design might be
+    to only check which keywords are new compared to the old list
+    of keywords, and only add users when they have indicated
+    interest on a new keyword.
 
     The code could also be changed to only trigger on the ``create()``
     event, rather than also on the ``set()`` event, thus only setting
@@ -4250,8 +4445,8 @@
     In the auditor, there is a loop over all users. For a site with
     only few users this will pose no serious problem; however, with
     many users this will be a serious performance bottleneck.
-    A way out would be to link from the topics to the users who
-    selected these topics as nosy topics. This will eliminate the
+    A way out would be to link from the keywords to the users who
+    selected these keywords as nosy keywords. This will eliminate the
     loop over all users.
 
 Changes to Security and Permissions

Modified: tracker/roundup-src/doc/design.txt
==============================================================================
--- tracker/roundup-src/doc/design.txt	(original)
+++ tracker/roundup-src/doc/design.txt	Sun Mar  9 09:26:16 2008
@@ -819,7 +819,7 @@
     Class(db, "keyword", name=hyperdb.String())
 
     Class(db, "issue", fixer=hyperdb.Multilink("user"),
-                       topic=hyperdb.Multilink("keyword"),
+                       keyword=hyperdb.Multilink("keyword"),
                        priority=hyperdb.Link("priority"),
                        status=hyperdb.Link("status"))
 
@@ -1250,10 +1250,10 @@
 clarity)::
 
     /issue?status=unread,in-progress,resolved&
-        topic=security,ui&
+        keyword=security,ui&
         :group=priority,-status&
         :sort=-activity&
-        :filters=status,topic&
+        :filters=status,keyword&
         :columns=title,status,fixer
 
 
@@ -1274,11 +1274,11 @@
 
 The example specifies an index of "issue" items. Only issues with a
 "status" of either "unread" or "in-progres" or "resolved" are displayed,
-and only issues with "topic" values including both "security" and "ui"
+and only issues with "keyword" values including both "security" and "ui"
 are displayed.  The items are grouped by priority arranged in ascending
 order and in descending order by status; and within groups, sorted by
 activity, arranged in descending order. The filter section shows
-filters for the "status" and "topic" properties, and the table includes
+filters for the "status" and "keyword" properties, and the table includes
 columns for the "title", "status", and "fixer" properties.
 
 Associated with each issue class is a default layout specifier.  The

Modified: tracker/roundup-src/doc/features.txt
==============================================================================
--- tracker/roundup-src/doc/features.txt	(original)
+++ tracker/roundup-src/doc/features.txt	Sun Mar  9 09:26:16 2008
@@ -15,7 +15,7 @@
  - requires *no* additional support software - python (2.3+) is
    enough to get you going
  - easy to set up higher-performance storage backends like sqlite_,
-   metakit_, mysql_ and postgresql_
+   mysql_ and postgresql_
 
 *simple to use*
  - accessible through the web, email, command-line or Python programs
@@ -40,11 +40,11 @@
    customisations
 
 *fast, scalable*
- - with the sqlite, metakit, mysql and postgresql backends, roundup is
+ - with the sqlite, mysql and postgresql backends, roundup is
    also fast and scalable, easily handling thousands of issues and users
    with decent response times
  - database indexes are automatically added for those backends that
-   support them (sqlite, metakit, mysql and postgresql)
+   support them (sqlite, mysql and postgresql)
  - indexed text searching giving fast responses to searches across all
    messages and indexed string properties
  - support for the Xapian full-text indexing engine for large trackers
@@ -102,8 +102,12 @@
  - a variety of sample shell scripts are provided (weekly reports, issue
    generation, ...)
 
+*xmlrpc interface*
+ - simple remote tracker interface with basic HTTP authentication
+ - provides same access to tracker as roundup-admin, but based on
+   XMLRPC calls
+
 .. _sqlite: http://www.hwaci.com/sw/sqlite/
-.. _metakit: http://www.equi4.com/metakit/
 .. _mysql: http://sourceforge.net/projects/mysql-python
 .. _postgresql: http://initd.org/software/initd/psycopg
 

Modified: tracker/roundup-src/doc/index.txt
==============================================================================
--- tracker/roundup-src/doc/index.txt	(original)
+++ tracker/roundup-src/doc/index.txt	Sun Mar  9 09:26:16 2008
@@ -81,6 +81,7 @@
 Wil Cooley,
 Joe Cooper,
 Kelley Dagley,
+Bruno Damour,
 Toby Dickenson,
 Paul F. Dubois,
 Eric Earnst,
@@ -95,6 +96,7 @@
 Frank Gibbons,
 Johannes Gijsbers,
 Gus Gollings,
+Philipp Gortan,
 Dan Grassi,
 Robin Green,
 Jason Grout,
@@ -122,11 +124,14 @@
 Andrey Lebedev,
 Henrik Levkowetz,
 David Linke,
+Martin v. Löwis,
 Fredrik Lundh,
 Will Maier,
 Georges Martin,
 Gordon McMillan,
 John F Meinel Jr,
+Ulrik Mikaelsson,
+John Mitchell,
 Ramiro Morales,
 Toni Mueller,
 Stefan Niederhauser,

Modified: tracker/roundup-src/doc/installation.txt
==============================================================================
--- tracker/roundup-src/doc/installation.txt	(original)
+++ tracker/roundup-src/doc/installation.txt	Sun Mar  9 09:26:16 2008
@@ -2,7 +2,7 @@
 Installing Roundup
 ==================
 
-:Version: $Revision: 1.121 $
+:Version: $Revision: 1.130 $
 
 .. contents::
    :depth: 2
@@ -75,9 +75,23 @@
   you to install a snapshot. Snapshot "0.9.2_svn6532" has been tried
   successfully.
 
+pyopenssl
+  If pyopenssl_ is installed the roundup-server can be configured
+  to serve trackers over SSL. If you are going to serve roundup via
+  proxy through a server with SSL support (e.g. apache) then this is
+  unnecessary.
+
+pyme
+  If pyme_ is installed you can configure the mail gateway to perform
+  verification or decryption of incoming OpenPGP MIME messages. When
+  configured, you can require email to be cryptographically signed
+  before roundup will allow it to make modifications to issues.
+
 .. _Xapian: http://www.xapian.org/
 .. _pytz: http://www.python.org/pypi/pytz
 .. _Olson tz database: http://www.twinsun.com/tz/tz-link.htm
+.. _pyopenssl: http://pyopenssl.sourceforge.net
+.. _pyme: http://pyme.sourceforge.net
 
 
 Getting Roundup
@@ -146,7 +160,7 @@
 
 If you would like to place the Roundup scripts in a directory other
 than ``/usr/bin``, then specify the preferred location with
-``--install-script``. For example, to install them in
+``--install-scripts``. For example, to install them in
 ``/opt/roundup/bin``::
 
     python setup.py install --install-scripts=/opt/roundup/bin
@@ -272,25 +286,21 @@
 ========== =========== ===== ==============================
 anydbm     Slowest     Few   Always available
 sqlite     Fastest(*)  Few   May need install (PySQLite_)
-metakit    Fastest(*)  Few   Needs install (metakit_)
 postgresql Fast        Many  Needs install/admin (psycopg_)
 mysql      Fast        Many  Needs install/admin (MySQLdb_)
 ========== =========== ===== ==============================
 
 **sqlite**
-  These use the embedded database engines PySQLite_ and metakit_ to provide
-  very fast backends. They are not suitable for trackers which will have
-  many simultaneous users, but require much less installation and
-  maintenance effort than more scalable postgresql and mysql backends.
+  This uses the embedded database engine PySQLite_ to provide a very fast
+  backend. This is not suitable for trackers which will have many
+  simultaneous users, but requires much less installation and maintenance
+  effort than more scalable postgresql and mysql backends.
 
   SQLite is supported via PySQLite versions 1.1.7, 2.1.0 and sqlite3 (the last
   being bundled with Python 2.5+)
 
   Installed SQLite should be the latest version available (3.3.8 is known
   to work, 3.1.3 is known to have problems).
-**metakit**
-  Similar performance to sqlite. If you are choosing between these two,
-  please select sqlite.
 **postgresql**
   Backend for popular RDBMS PostgreSQL. You must read doc/postgresql.txt for
   additional installation steps and requirements. You must also configure
@@ -343,7 +353,7 @@
 
     adsutil.vbs set w3svc/AllowPathInfoForScriptMappings TRUE
 
-The ``adsutil.vbs`` file can be found in either ``c:\inetpub\adminscripts`` 
+The ``adsutil.vbs`` file can be found in either ``c:\inetpub\adminscripts``
 or ``c:\winnt\system32\inetsrv\adminsamples\`` or
 ``c:\winnt\system32\inetsrv\adminscripts\`` depending on your installation.
 
@@ -373,7 +383,7 @@
 ``.cgi`` extension of the cgi script. Place the ``roundup.cgi`` script
 wherever you want it to be, rename it to just ``roundup``, and add a
 couple lines to your Apache configuration::
- 
+
  <Location /path/to/roundup>
    SetHandler cgi-script
  </Location>
@@ -467,7 +477,8 @@
 In the following example we have two trackers set up in
 ``/var/db/roundup/support`` and ``/var/db/roundup/devel`` and accessed
 as ``https://my.host/roundup/support/`` and ``https://my.host/roundup/devel/``
-respectively.  Having them share same parent directory allows us to
+respectively (provided Apache has been set up for SSL of course).
+Having them share same parent directory allows us to
 reduce the number of configuration directives.  Support tracker has
 russian user interface.  The other tracker (devel) has english user
 interface (default).
@@ -489,7 +500,7 @@
     # everything else is handled by roundup web UI
     AliasMatch /roundup/([^/]+)/(?!@@file/)(.*) /var/db/roundup/$1/dummy.py/$2
     # roundup requires a slash after tracker name - add it if missing
-    RedirectMatch permanent /roundup/([^/]+)$ /roundup/$1/
+    RedirectMatch permanent ^/roundup/([^/]+)$ /roundup/$1/
     # common settings for all roundup trackers
     <Directory /var/db/roundup/*>
       Order allow,deny
@@ -511,6 +522,29 @@
       PythonOption TrackerHome /var/db/roundup/devel
     </Directory>
 
+Notice that the ``/var/db/roundup`` path shown above refers to the directory
+in which the tracker homes are stored. The actual value will thus depend on
+your system.
+
+On Windows the corresponding lines will look similar to these::
+
+    AliasMatch /roundup/(.+)/@@file/(.*) C:/DATA/roundup/$1/html/$2
+    AliasMatch /roundup/([^/]+)/(?!@@file/)(.*) C:/DATA/roundup/$1/dummy.py/$2
+    <Directory C:/DATA/roundup/*>
+    <Directory C:/DATA/roundup/support>
+    <Directory C:/DATA/roundup/devel>
+
+In this example the directory hosting all of the tracker homes is
+``C:\DATA\roundup``. (Notice that you must use forward slashes in paths
+inside the httpd.conf file!)
+
+The URL for accessing these trackers then become:
+`http://<roundupserver>/roundup/support/`` and
+``http://<roundupserver>/roundup/devel/``
+
+Note that in order to use https connections you must set up Apache for secure
+serving with SSL.
+
 WSGI Handler
 ~~~~~~~~~~~~
 
@@ -543,7 +577,7 @@
 Roundup tracker.  You should pick ONE of the following, all
 of which will continue my example setup from above:
 
-As a mail alias pipe process 
+As a mail alias pipe process
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 Set up a mail alias called "issue_tracker" as (include the quote marks):
@@ -556,7 +590,7 @@
 and change the command to::
 
     |roundup-mailgw /opt/roundup/trackers/support
- 
+
 To test the mail gateway on unix systems, try::
 
     echo test |mail -s '[issue] test' support at YOUR_DOMAIN_HERE
@@ -598,7 +632,7 @@
   require_files = /usr/bin/roundup-mailgw:ROUNDUP_HOME/$local_part/schema.py
 
 The following configuration has been tested on Debian Sarge with
-Exim4. 
+Exim4.
 
 .. note::
   Note that the Debian Exim4 packages don't allow pipes in alias files
@@ -764,6 +798,15 @@
 http://cjkpython.berlios.de/
 
 
+Public Tracker Considerations
+-----------------------------
+
+If you run a public tracker, you will eventually have to think about
+dealing with spam entered through both the web and mail interfaces.
+
+The `customisation documentation`_ has a simple detector that will block
+a lot of spam attempts. Look for the example "Preventing SPAM".
+
 
 Maintenance
 ===========
@@ -836,26 +879,87 @@
 Windows Server
 --------------
 
-To have the Roundup web server start up when your machine boots up, set the
-following up in Scheduled Tasks (note, the following is for a cygwin setup):
+To have the Roundup web server start up when your machine boots up, there
+are two different methods, the scheduler and installing the service.
+
 
-Run
- ``c:\cygwin\bin\bash.exe -c "roundup-server TheProject=/opt/roundup/trackers/support"``
-Start In
- ``C:\cygwin\opt\roundup\bin``
-Schedule
- At System Startup
+1. Using the Windows scheduler
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Set up the following in Scheduled Tasks (note, the following is for a
+cygwin setup):
+
+**Run**
+
+    ``c:\cygwin\bin\bash.exe -c "roundup-server TheProject=/opt/roundup/trackers/support"``
+
+**Start In**
+
+    ``C:\cygwin\opt\roundup\bin``
+
+**Schedule**
+
+    At System Startup
 
 To have the Roundup mail gateway run periodically to poll a POP email address,
-set the following up in Scheduled Tasks:
+set up the following in Scheduled Tasks:
+
+**Run**
+
+    ``c:\cygwin\bin\bash.exe -c "roundup-mailgw /opt/roundup/trackers/support pop roundup:roundup at mail-server"``
+
+**Start In**
+
+    ``C:\cygwin\opt\roundup\bin``
+
+**Schedule**
+
+    Every 10 minutes from 5:00AM for 24 hours every day
+
+    Stop the task if it runs for 8 minutes
+
+
+2. Installing the roundup server as a Windows service
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This is more Windows oriented and will make the Roundup server run as
+soon as the PC starts up without any need for a login or such. It will
+also be available in the normal Windows Administrative Tools.
+
+For this you need first to create a service ini file containing the
+relevant settings.
+
+1. It is created if you execute the following command from within the
+   scripts directory (notice the use of backslashes)::
+
+     roundup-server -S -C <trackersdir>\server.ini -n <servername> -p 8080 -l <trackersdir>\trackerlog.log software=<trackersdir>\Software
+
+   where the item ``<trackersdir>`` is replaced with the physical directory
+   that hosts all of your trackers. The ``<servername>`` item is the name
+   of your roundup server PC, such as w2003srv or similar.
+
+2. Next open the now created file ``C:\DATA\roundup\server.ini`` file
+   (if your ``<trackersdir>`` is ``C:\DATA\roundup``).
+   Check the entries for correctness, especially this one::
+
+    [trackers]
+    software = C:\DATA\Roundup\Software
+
+   (this is an example where the tracker is named software and its home is
+   ``C:\DATA\Roundup\Software``)
+
+3. Next give the commands that actually installs and starts the service::
+
+    roundup-server -C C:\DATA\Roundup\server.ini -c install
+    roundup-server -c start
+
+4. Finally open the AdministrativeTools/Services applet and locate the
+   Roundup service entry. Open its properties and change it to start
+   automatically instead of manually.
 
-Run
- ``c:\cygwin\bin\bash.exe -c "roundup-mailgw /opt/roundup/trackers/support pop roundup:roundup at mail-server"``
-Start In
- ``C:\cygwin\opt\roundup\bin``
-Schedule
- Every 10 minutes from 5:00AM for 24 hours every day
- Stop the task if it runs for 8 minutes
+If you are using Apache as the webserver you might want to use it with
+mod_python instead to serve out Roundup. In that case see the mod_python
+instructions above for details.
 
 
 Sendmail smrsh
@@ -929,7 +1033,6 @@
 .. _External hyperlink targets:
 
 .. _apache: http://httpd.apache.org/
-.. _metakit: http://www.equi4.com/metakit/
 .. _mod_python: http://www.modpython.org/
 .. _MySQLdb: http://sourceforge.net/projects/mysql-python
 .. _Psycopg: http://initd.org/software/initd/psycopg

Modified: tracker/roundup-src/doc/mysql.txt
==============================================================================
--- tracker/roundup-src/doc/mysql.txt	(original)
+++ tracker/roundup-src/doc/mysql.txt	Sun Mar  9 09:26:16 2008
@@ -2,7 +2,7 @@
 MySQL Backend
 =============
 
-:version: $Revision: 1.12 $
+:version: $Revision: 1.13 $
 
 This notes detail the MySQL backend for the Roundup issue tracker.
 
@@ -13,19 +13,13 @@
 To use MySQL as the backend for storing roundup data, you also need 
 to install:
 
-1. MySQL RDBMS 4.0.16 or higher - http://www.mysql.com. Your MySQL
+1. MySQL RDBMS 4.0.18 or higher - http://www.mysql.com. Your MySQL
    installation MUST support InnoDB tables (or Berkeley DB (BDB) tables
-   if you have no other choice). If you're running < 4.0.16 (but not <4.0)
+   if you have no other choice). If you're running < 4.0.18 (but not <4.0)
    then you'll need to use BDB to pass all unit tests. Edit the
    ``roundup/backends/back_mysql.py`` file to enable DBD instead of InnoDB.
 2. Python MySQL interface - http://sourceforge.net/projects/mysql-python
 
-.. note::
-   The InnoDB implementation has a bug__ that Roundup tickles. See
-
-__ http://bugs.mysql.com/bug.php?id=1810
-
-
 Running the MySQL tests
 =======================
 

Modified: tracker/roundup-src/doc/overview.txt
==============================================================================
--- tracker/roundup-src/doc/overview.txt	(original)
+++ tracker/roundup-src/doc/overview.txt	Sun Mar  9 09:26:16 2008
@@ -147,7 +147,7 @@
 only sometimes fall into one category; often,
 a piece of information may be related to several concepts.
 
-For example, forcing each item into a single topic
+For example, forcing each item into a single keyword
 category is not just suboptimal but counterproductive:
 seekers of that
 item may expect to find it in a different category
@@ -245,7 +245,7 @@
 The *multilink* type is for a list of links to any
 number of other items in the in the database.  A *multilink*
 property, for example, can be used to refer to related items
-or topic categories relevant to an item.
+or keyword categories relevant to an item.
 
 For Roundup, all items have four properties that are not customizable:
 
@@ -314,13 +314,13 @@
     #   superseder = Multilink("issue")
     #   (it also gets the Class properties creation, activity and creator)
     issue = IssueClass(db, "issue", 
-                    assignedto=Link("user"), topic=Multilink("keyword"),
+                    assignedto=Link("user"), keyword=Multilink("keyword"),
                     priority=Link("priority"), status=Link("status"))
 
 The **assignedto** property assigns
 responsibility for an item to a person or a list of people.
-The **topic** property places the
-item in an arbitrary number of relevant topic sets (see
+The **keyword** property places the
+item in an arbitrary number of relevant keyword sets (see
 the section on `Browsing and Searching`_).
 
 The **prority** and **status** values are initially:
@@ -449,11 +449,11 @@
 messages they might have missed.
 
 We can take this a step further and
-permit users to monitor particular topics or classifications of items
+permit users to monitor particular keywords or classifications of items
 by allowing other kinds of items to also have their own nosy lists.
 For example, a manager could be on the
 nosy list of the priority value item for "critical", or a
-developer could be on the nosy list of the topic value item for "security".
+developer could be on the nosy list of the keyword value item for "security".
 The recipients are then determined by the union of the nosy lists on the
 item and all the items it links to.
 
@@ -552,7 +552,7 @@
   (the filter selects the *intersection* of the sets of items
   associated with the active options)
 
-For a *multilink* property like **topic**,
+For a *multilink* property like **keyword**,
 one possibility is to show, as hyperlinks, the keywords whose
 sets have non-empty intersections with the currently displayed set of
 items.  Sorting the keywords by popularity seems

Modified: tracker/roundup-src/doc/roundup-server.1
==============================================================================
--- tracker/roundup-src/doc/roundup-server.1	(original)
+++ tracker/roundup-src/doc/roundup-server.1	Sun Mar  9 09:26:16 2008
@@ -21,18 +21,36 @@
 Sets a filename to log to (instead of stdout). This is required if the -d
 option is used.
 .TP
+\fB-i\fP \fIfile\fP
+Sets a filename to use as a template for generating the tracker index page.
+The variable "trackers" is available to the template and is a dict of all
+configured trackers.
+.TP
+\fB-s\fP
+Enables to use of SSL.
+.TP
+\fB-e\fP \fIfile\fP
+Sets a filename containing the PEM file to use for SSL. If left blank, a
+temporary self-signed certificate will be used.
+.TP
 \fB-h\fP
 print help
 .TP
 \fBname=\fP\fItracker home\fP
-Sets the tracker home(s) to use. The name is how the tracker is
-identified in the URL (it's the first part of the URL path). The
-tracker home is the directory that was identified when you did
-"roundup-admin init". You may specify any number of these name=home
-pairs on the command-line. For convenience, you may edit the
-TRACKER_HOMES variable in the roundup-server file instead.
-Make sure the name part doesn't include any url-unsafe characters like
-spaces, as these confuse the cookie handling in browsers like IE.
+Sets the tracker home(s) to use. The \fBname\fP variable is how the tracker is
+identified in the URL (it's the first part of the URL path). The \fItracker
+home\fP variable is the directory that was identified when you did
+"roundup-admin init". You may specify any number of these name=home pairs on
+the command-line. For convenience, you may edit the TRACKER_HOMES variable in
+the roundup-server file instead.  Make sure the name part doesn't include any
+url-unsafe characters like spaces, as these confuse the cookie handling in
+browsers like IE.
+.SH EXAMPLES
+.TP
+.B roundup-server -p 9000 bugs=/var/tracker reqs=/home/roundup/group1
+Start the server on port \fB9000\fP serving two trackers; one under
+\fB/bugs\fP and one under \fB/reqs\fP.
+
 .SH CONFIGURATION FILE
 See the "admin_guide" in the Roundup "doc" directory.
 .SH AUTHOR

Modified: tracker/roundup-src/doc/roundup-server.ini.example
==============================================================================
--- tracker/roundup-src/doc/roundup-server.ini.example	(original)
+++ tracker/roundup-src/doc/roundup-server.ini.example	Sun Mar  9 09:26:16 2008
@@ -1,6 +1,6 @@
 ; This is a sample configuration file for roundup-server. See the
 ; admin_guide for information about its contents.
-[server]
+[main]
 port = 8080
 ;hostname = 
 ;user = 
@@ -8,9 +8,12 @@
 ;log_ip = yes
 ;pidfile = 
 ;logfile = 
+;template =
+;ssl = no
+;pem =
 
 
 ; Add one of these per tracker being served
-[tracker_url_component]
+[trackers]
 home = /path/to/tracker
 

Modified: tracker/roundup-src/doc/upgrading.txt
==============================================================================
--- tracker/roundup-src/doc/upgrading.txt	(original)
+++ tracker/roundup-src/doc/upgrading.txt	Sun Mar  9 09:26:16 2008
@@ -13,6 +13,51 @@
 
 .. contents::
 
+Migrating from 1.4.x to 1.4.2
+=============================
+
+You should run the "roundup-admin migrate" command for your tracker once
+you've installed the latest codebase. 
+
+Do this before you use the web, command-line or mail interface and before
+any users access the tracker.
+
+This command will respond with either "Tracker updated" (if you've not
+previously run it on an RDBMS backend) or "No migration action required"
+(if you have run it, or have used another interface to the tracker,
+or are using anydbm).
+
+It's safe to run this even if it's not required, so just get into the
+habit.
+
+
+Migrating from 1.3.3 to 1.4.0
+=============================
+
+Value of the "refwd_re" tracker configuration option (section "mailgw")
+is treated as UTF-8 string.  In previous versions, it was ISO8859-1.
+
+If you have running trackers based on the classic template, please
+update the messagesummary detector as follows::
+
+    --- detectors/messagesummary.py 17 Apr 2003 03:26:38 -0000      1.1
+    +++ detectors/messagesummary.py 3 Apr 2007 06:47:21 -0000       1.2
+    @@ -8,7 +8,7 @@
+     if newvalues.has_key('summary') or not newvalues.has_key('content'):
+         return
+
+    -    summary, content = parseContent(newvalues['content'], 1, 1)
+    +    summary, content = parseContent(newvalues['content'], config=db.config)
+     newvalues['summary'] = summary
+
+In the latest version we have added some database indexes to the
+SQL-backends (mysql, postgresql, sqlite) for speeding up building the
+roundup-index for full-text search. We recommend that you create the
+following database indexes on the database by hand::
+
+ CREATE INDEX words_by_id ON __words (_textid)
+ CREATE UNIQUE INDEX __textids_by_props ON __textids (_class, _itemid, _prop)
+
 Migrating from 1.2.x to 1.3.0
 =============================
 

Modified: tracker/roundup-src/doc/user_guide.txt
==============================================================================
--- tracker/roundup-src/doc/user_guide.txt	(original)
+++ tracker/roundup-src/doc/user_guide.txt	Sun Mar  9 09:26:16 2008
@@ -2,7 +2,7 @@
 User Guide
 ==========
 
-:Version: $Revision: 1.36 $
+:Version: $Revision: 1.37 $
 
 .. contents::
 
@@ -112,7 +112,7 @@
 Constrained (link and multilink) properties
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-Fields like "Assigned To" and "Topics" hold references to items in other
+Fields like "Assigned To" and "Keywords" hold references to items in other
 classes ("user" and "keyword" in those two cases.)
 
 Sometimes, the selection is done through a menu, like in the "Assigned
@@ -130,13 +130,13 @@
   match issues that are not assigned to a user.
 ``assignedto=2,3,40``
   match issues that are assigned to users 2, 3 or 40.
-``topic=user interface``
-  match issues with the keyword "user interface" in their topic list
-``topic=web interface,e-mail interface``
+``keyword=user interface``
+  match issues with the keyword "user interface" in their keyword list
+``keyword=web interface,e-mail interface``
   match issues with the keyword "web interface" or "e-mail interface" in
-  their topic list
-``topic=-1``
-  match issues with no topics set
+  their keyword list
+``keyword=-1``
+  match issues with no keywords set
 
 
 Date properties
@@ -350,10 +350,10 @@
 (whitespace has been added for clarity)::
 
     /issue?status=unread,in-progress,resolved&
-        topic=security,ui&
+        keyword=security,ui&
         @group=priority,-status&
         @sort=-activity&
-        @filters=status,topic&
+        @filters=status,keyword&
         @columns=title,status,fixer
 
 

Modified: tracker/roundup-src/frontends/ZRoundup/ZRoundup.py
==============================================================================
--- tracker/roundup-src/frontends/ZRoundup/ZRoundup.py	(original)
+++ tracker/roundup-src/frontends/ZRoundup/ZRoundup.py	Sun Mar  9 09:26:16 2008
@@ -14,7 +14,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: ZRoundup.py,v 1.22 2006/01/25 03:43:04 richard Exp $
+# $Id: ZRoundup.py,v 1.23 2008/02/07 01:03:39 richard Exp $
 #
 ''' ZRoundup module - exposes the roundup web interface to Zope
 
@@ -67,6 +67,11 @@
     def end_headers(self):
         # not needed - the RESPONSE object handles this internally on write()
         pass
+    def start_response(self, headers, response):
+        self.send_response(response)
+        for key, value in headers:
+            self.send_header(key, value)
+        self.end_headers()
 
 class FormItem:
     '''Make a Zope form item look like a cgi.py one
@@ -89,6 +94,8 @@
         else:
             entry = FormItem(entry)
         return entry
+    def __iter__(self):
+        return iter(self.__form)
     def getvalue(self, key, default=None):
         if self.__form.has_key(key):
             return self.__form[key]

Modified: tracker/roundup-src/locale/es.po
==============================================================================
--- tracker/roundup-src/locale/es.po	(original)
+++ tracker/roundup-src/locale/es.po	Sun Mar  9 09:26:16 2008
@@ -5,10 +5,10 @@
 #
 msgid ""
 msgstr ""
-"Project-Id-Version: Roundup 1.3.1\n"
+"Project-Id-Version: Roundup 1.3.3\n"
 "Report-Msgid-Bugs-To: roundup-devel at lists.sourceforge.net\n"
-"POT-Creation-Date: 2006-04-27 09:02+0300\n"
-"PO-Revision-Date: 2006-11-11 01:32:00-0300\n"
+"POT-Creation-Date: 2007-09-16 09:48+0300\n"
+"PO-Revision-Date: 2007-09-18 01:22-0300\n"
 "Last-Translator: Ramiro Morales <rm0 at gmx.net>\n"
 "Language-Team: Spanish Translators <roundup-devel at lists.sourceforge.net>\n"
 "MIME-Version: 1.0\n"
@@ -17,19 +17,19 @@
 "Plural-Forms: nplurals=2; plural=(n != 1);\n"
 
 # ../roundup/admin.py:85 :955 :1004 :1026
-#: ../roundup/admin.py:85 ../roundup/admin.py:981 ../roundup/admin.py:1030
-#: ../roundup/admin.py:1052
+#: ../roundup/admin.py:86 ../roundup/admin.py:989 ../roundup/admin.py:1040
+#: ../roundup/admin.py:1063 ../roundup/admin.py:86:989 :1040:1063
 #, python-format
 msgid "no such class \"%(classname)s\""
 msgstr "la clase \"%(classname)s\" no existe"
 
 # ../roundup/admin.py:95 :99
-#: ../roundup/admin.py:95 ../roundup/admin.py:99
+#: ../roundup/admin.py:96 ../roundup/admin.py:100 ../roundup/admin.py:96:100
 #, python-format
 msgid "argument \"%(arg)s\" not propname=value"
 msgstr "el argumento \"%(arg)s\" no es de la forma nombrepropiedad=valor"
 
-#: ../roundup/admin.py:112
+#: ../roundup/admin.py:113
 #, python-format
 msgid ""
 "Problem: %(message)s\n"
@@ -38,7 +38,7 @@
 "Problema: %(message)s\n"
 "\n"
 
-#: ../roundup/admin.py:113
+#: ../roundup/admin.py:114
 #, python-format
 msgid ""
 "%(message)sUsage: roundup-admin [options] [<command> <arguments>]\n"
@@ -93,11 +93,11 @@
 " roundup-admin help <comando>             -- ayuda específica a un comando\n"
 " roundup-admin help all                   -- toda la ayuda disponible\n"
 
-#: ../roundup/admin.py:140
+#: ../roundup/admin.py:141
 msgid "Commands:"
 msgstr "Comandos:"
 
-#: ../roundup/admin.py:147
+#: ../roundup/admin.py:148
 msgid ""
 "Commands may be abbreviated as long as the abbreviation\n"
 "matches only one command, e.g. l == li == lis == list."
@@ -105,7 +105,7 @@
 "Los comandos pueden ser abreviados siempre y cuando la abreviación\n"
 "coincida con sólo un comando, ej. l == li == lis == list."
 
-#: ../roundup/admin.py:177
+#: ../roundup/admin.py:178
 msgid ""
 "\n"
 "All commands (except help) require a tracker specifier. This is just\n"
@@ -175,7 +175,7 @@
 "Todos los comandos (excepto ayuda) requieren un especificador de tracker.\n"
 "Este es simplemente la ruta al tracker roundup con el que se está "
 "trabajando.\n"
-"Un tracker roundup es donde roundup mantiene la base de datos y el archivo "
+"Un tracker roundup es donde roundup mantiene la base de datos y el fichero "
 "de\n"
 "configuración que define un issue tracker. Puede pensarse en el mismo como "
 "el\n"
@@ -250,12 +250,12 @@
 "\n"
 "Ayuda sobre comandos:\n"
 
-#: ../roundup/admin.py:240
+#: ../roundup/admin.py:241
 #, python-format
 msgid "%s:"
-msgstr ""
+msgstr "%s:"
 
-#: ../roundup/admin.py:245
+#: ../roundup/admin.py:246
 msgid ""
 "Usage: help topic\n"
 "        Give help about topic.\n"
@@ -275,33 +275,33 @@
 "        all       -- toda la ayuda disponible\n"
 "        "
 
-#: ../roundup/admin.py:268
+#: ../roundup/admin.py:269
 #, python-format
 msgid "Sorry, no help for \"%(topic)s\""
 msgstr "Lo siento, no hay ayuda para \"%(topic)s\""
 
 # ../roundup/admin.py:338 :387
-#: ../roundup/admin.py:340 ../roundup/admin.py:396
+#: ../roundup/admin.py:346 ../roundup/admin.py:402 ../roundup/admin.py:346:402
 msgid "Templates:"
 msgstr "Plantillas:"
 
 # ../roundup/admin.py:341 :398
-#: ../roundup/admin.py:343 ../roundup/admin.py:407
+#: ../roundup/admin.py:349 ../roundup/admin.py:413 ../roundup/admin.py:349:413
 msgid "Back ends:"
 msgstr "Motor de almacenamiento"
 
-#: ../roundup/admin.py:346
+#: ../roundup/admin.py:352
 msgid ""
-"Usage: install [template [backend [admin password [key=val[,key=val]]]]]\n"
+"Usage: install [template [backend [key=val[,key=val]]]]\n"
 "        Install a new Roundup tracker.\n"
 "\n"
 "        The command will prompt for the tracker home directory\n"
 "        (if not supplied through TRACKER_HOME or the -i option).\n"
-"        The template, backend and admin password may be specified\n"
-"        on the command-line as arguments, in that order.\n"
+"        The template and backend may be specified on the command-line\n"
+"        as arguments, in that order.\n"
 "\n"
-"        The last command line argument allows to pass initial values\n"
-"        for config options.  For example, passing\n"
+"        Command line arguments following the backend allows you to\n"
+"        pass initial values for config options.  For example, passing\n"
 "        \"web_http_auth=no,rdbms_user=dinsdale\" will override defaults\n"
 "        for options http_auth in section [web] and user in section [rdbms].\n"
 "        Please be careful to not use spaces in this argument! (Enclose\n"
@@ -315,14 +315,13 @@
 "        See also initopts help.\n"
 "        "
 msgstr ""
-"Uso:   install [plantilla [backend [ctraseña adm [clave=val[,clave=val]]]]]\n"
+"Uso:   install [plantilla [backend [clave=val[,clave=val]]]]\n"
 "        Instala un nuevo tracker Roundup.\n"
 "\n"
 "        El comando preguntará el directorio base del tracker\n"
 "        (si el mismo no se provee vía TRACKER_HOME o la opción -i).\n"
-"        La plantilla, el backend y la contraseña de admin pueden\n"
-"        especificarse en la línea de comandos como argumentos, en ese "
-"orden.\n"
+"        La plantilla, el backend pueden especificarse en la línea\n"
+"        de comandos como argumentos, en ese orden.\n"
 "\n"
 "        El último argumento de la línea de comandos permite especificar "
 "valores\n"
@@ -349,23 +348,24 @@
 
 # ../roundup/admin.py:360 :442 :503 :582 :632 :688 :709 :737 :808 :875 :946
 # :994 :1016 :1043 :1106 :1173
-#: ../roundup/admin.py:369 ../roundup/admin.py:466 ../roundup/admin.py:527
-#: ../roundup/admin.py:606 ../roundup/admin.py:656 ../roundup/admin.py:714
-#: ../roundup/admin.py:735 ../roundup/admin.py:763 ../roundup/admin.py:834
-#: ../roundup/admin.py:901 ../roundup/admin.py:972 ../roundup/admin.py:1020
-#: ../roundup/admin.py:1042 ../roundup/admin.py:1069 ../roundup/admin.py:1136
-#: ../roundup/admin.py:1207
+#: ../roundup/admin.py:375 ../roundup/admin.py:472 ../roundup/admin.py:533
+#: ../roundup/admin.py:612 ../roundup/admin.py:663 ../roundup/admin.py:721
+#: ../roundup/admin.py:742 ../roundup/admin.py:770 ../roundup/admin.py:842
+#: ../roundup/admin.py:909 ../roundup/admin.py:980 ../roundup/admin.py:1030
+#: ../roundup/admin.py:1053 ../roundup/admin.py:1084 ../roundup/admin.py:1180
+#: ../roundup/admin.py:1253 ../roundup/admin.py:375:472 :1030:1053 :1084:1180
+#: :1253 :533:612 :663:721 :742:770 :842:909:980
 msgid "Not enough arguments supplied"
 msgstr "No se proveyó una cantidad suficiente de argumentos"
 
-#: ../roundup/admin.py:375
+#: ../roundup/admin.py:381
 #, python-format
 msgid "Instance home parent directory \"%(parent)s\" does not exist"
 msgstr ""
 "El directorio padre \"%(parent)s\" del directorio base de la instancia no "
 "existe"
 
-#: ../roundup/admin.py:383
+#: ../roundup/admin.py:389
 #, python-format
 msgid ""
 "WARNING: There appears to be a tracker in \"%(tracker_home)s\"!\n"
@@ -376,20 +376,20 @@
 "Si Ud. lo reinstala, perderá toda la información relacionada al mismo!\n"
 "Elimino la misma? Y/N: "
 
-#: ../roundup/admin.py:398
+#: ../roundup/admin.py:404
 msgid "Select template [classic]: "
 msgstr "Seleccione la plantilla [classic]: "
 
-#: ../roundup/admin.py:409
+#: ../roundup/admin.py:415
 msgid "Select backend [anydbm]: "
 msgstr "Selecccione el motor de almacenamiento [anydbm]: "
 
-#: ../roundup/admin.py:419
+#: ../roundup/admin.py:425
 #, python-format
 msgid "Error in configuration settings: \"%s\""
 msgstr "Error en opciones de configuración: \"%s\""
 
-#: ../roundup/admin.py:428
+#: ../roundup/admin.py:434
 #, python-format
 msgid ""
 "\n"
@@ -399,14 +399,14 @@
 msgstr ""
 "\n"
 "---------------------------------------------------------------------------\n"
-" Ud. debe ahora editar el archivo de configuración del tracker:\n"
+" Ud. debe ahora editar el fichero de configuración del tracker:\n"
 "   %(config_file)s"
 
-#: ../roundup/admin.py:438
+#: ../roundup/admin.py:444
 msgid " ... at a minimum, you must set following options:"
 msgstr " ... como mínimo, debe configurar las siguientes opciones:"
 
-#: ../roundup/admin.py:443
+#: ../roundup/admin.py:449
 #, python-format
 msgid ""
 "\n"
@@ -424,9 +424,9 @@
 msgstr ""
 "\n"
 " Si desea modificar el esquema de la base de datos,\n"
-" debe tambien editar el archivo de esquema:\n"
+" debe tambien editar el fichero de esquema:\n"
 "   %(database_config_file)s\n"
-" Puede también cambiar el archivo de inicialización de la base de datos:\n"
+" Puede también cambiar el fichero de inicialización de la base de datos:\n"
 "   %(database_init_file)s\n"
 " ... vea la documentación sobre personalización si desea más información.\n"
 "\n"
@@ -434,21 +434,21 @@
 " completado los pasos arriba descriptos.\n"
 "---------------------------------------------------------------------------\n"
 
-#: ../roundup/admin.py:461
+#: ../roundup/admin.py:467
 msgid ""
 "Usage: genconfig <filename>\n"
 "        Generate a new tracker config file (ini style) with default values\n"
 "        in <filename>.\n"
 "        "
 msgstr ""
-"Uso:   genconfig <archivo>\n"
-"        Genera un nuevo archivo de configuración de tracker (en formato "
+"Uso:   genconfig <fichero>\n"
+"        Genera un nuevo fichero de configuración de tracker (en formato "
 "ini)\n"
-"        con valores por defecto en el archivo <archivo>.\n"
+"        con valores por defecto en el fichero <fichero>.\n"
 "        "
 
 #. password
-#: ../roundup/admin.py:471
+#: ../roundup/admin.py:477
 msgid ""
 "Usage: initialise [adminpw]\n"
 "        Initialise a new Roundup tracker.\n"
@@ -467,23 +467,23 @@
 "        Ejecuta la función de inicialización dbinit.init() del tracker\n"
 "        "
 
-#: ../roundup/admin.py:485
+#: ../roundup/admin.py:491
 msgid "Admin Password: "
 msgstr "Contraseña de administración: "
 
-#: ../roundup/admin.py:486
+#: ../roundup/admin.py:492
 msgid "       Confirm: "
 msgstr "       Confirmar: "
 
-#: ../roundup/admin.py:490
+#: ../roundup/admin.py:496
 msgid "Instance home does not exist"
 msgstr "El directorio base de la instancia no existe"
 
-#: ../roundup/admin.py:494
+#: ../roundup/admin.py:500
 msgid "Instance has not been installed"
 msgstr "La instancia no ha sido instalada"
 
-#: ../roundup/admin.py:499
+#: ../roundup/admin.py:505
 msgid ""
 "WARNING: The database is already initialised!\n"
 "If you re-initialise it, you will lose all the data!\n"
@@ -493,7 +493,7 @@
 "Si la reinicializa, perderá toda la información!\n"
 "Eliminar la misma? Y/N: "
 
-#: ../roundup/admin.py:520
+#: ../roundup/admin.py:526
 msgid ""
 "Usage: get property designator[,designator]*\n"
 "        Get the given property of one or more designator(s).\n"
@@ -510,7 +510,7 @@
 "        "
 
 # ../roundup/admin.py:536 :551
-#: ../roundup/admin.py:560 ../roundup/admin.py:575
+#: ../roundup/admin.py:566 ../roundup/admin.py:581 ../roundup/admin.py:566:581
 #, python-format
 msgid "property %s is not of type Multilink or Link so -d flag does not apply."
 msgstr ""
@@ -518,18 +518,18 @@
 "no puede usarse."
 
 # ../roundup/admin.py:559 :957 :1006 :1028
-#: ../roundup/admin.py:583 ../roundup/admin.py:983 ../roundup/admin.py:1032
-#: ../roundup/admin.py:1054
+#: ../roundup/admin.py:589 ../roundup/admin.py:991 ../roundup/admin.py:1042
+#: ../roundup/admin.py:1065 ../roundup/admin.py:589:991 :1042:1065
 #, python-format
 msgid "no such %(classname)s node \"%(nodeid)s\""
 msgstr "no existe nodo de clase %(classname)s llamado  \"%(nodeid)s\""
 
-#: ../roundup/admin.py:585
+#: ../roundup/admin.py:591
 #, python-format
 msgid "no such %(classname)s property \"%(propname)s\""
 msgstr "no existe propiedad de clase %(classname)s llamado  \"%(propname)s\""
 
-#: ../roundup/admin.py:594
+#: ../roundup/admin.py:600
 msgid ""
 "Usage: set items property=value property=value ...\n"
 "        Set the given properties of one or more items(s).\n"
@@ -558,7 +558,7 @@
 "        asociados como números separados por comas (\"1,2,3\").\n"
 "        "
 
-#: ../roundup/admin.py:648
+#: ../roundup/admin.py:655
 msgid ""
 "Usage: find classname propname=value ...\n"
 "        Find the nodes of the given class with a given link property value.\n"
@@ -580,13 +580,13 @@
 "        "
 
 # ../roundup/admin.py:675 :828 :840 :894
-#: ../roundup/admin.py:701 ../roundup/admin.py:854 ../roundup/admin.py:866
-#: ../roundup/admin.py:920
+#: ../roundup/admin.py:708 ../roundup/admin.py:862 ../roundup/admin.py:874
+#: ../roundup/admin.py:928 ../roundup/admin.py:708:862 :874:928
 #, python-format
 msgid "%(classname)s has no property \"%(propname)s\""
 msgstr "%(classname)s no posee la propiedad \"%(propname)s\""
 
-#: ../roundup/admin.py:708
+#: ../roundup/admin.py:715
 msgid ""
 "Usage: specification classname\n"
 "        Show the properties for a classname.\n"
@@ -600,17 +600,17 @@
 "        Visualiza las propiedades para una cierta clase.\n"
 "        "
 
-#: ../roundup/admin.py:723
+#: ../roundup/admin.py:730
 #, python-format
 msgid "%(key)s: %(value)s (key property)"
 msgstr "%(key)s: %(value)s (propiedad de clave)"
 
-#: ../roundup/admin.py:725
+#: ../roundup/admin.py:732
 #, python-format
 msgid "%(key)s: %(value)s"
-msgstr ""
+msgstr "%(key)s: %(value)s"
 
-#: ../roundup/admin.py:728
+#: ../roundup/admin.py:735
 msgid ""
 "Usage: display designator[,designator]*\n"
 "        Show the property values for the given node(s).\n"
@@ -626,12 +626,12 @@
 "especificado.\n"
 "        "
 
-#: ../roundup/admin.py:752
+#: ../roundup/admin.py:759
 #, python-format
 msgid "%(key)s: %(value)r"
-msgstr ""
+msgstr "%(key)s: %(value)r"
 
-#: ../roundup/admin.py:755
+#: ../roundup/admin.py:762
 msgid ""
 "Usage: create classname property=value ...\n"
 "        Create a new entry of a given class.\n"
@@ -650,31 +650,31 @@
 "        nombre=valor provistos en la línea de comandos luego del comando\n"
 "        \"create\" para establecer valores de propiedad(es).        "
 
-#: ../roundup/admin.py:782
+#: ../roundup/admin.py:789
 #, python-format
 msgid "%(propname)s (Password): "
 msgstr "%(propname)s (Contraseña): "
 
-#: ../roundup/admin.py:784
+#: ../roundup/admin.py:791
 #, python-format
 msgid "   %(propname)s (Again): "
 msgstr "   %(propname)s (Nuevamente): "
 
-#: ../roundup/admin.py:786
+#: ../roundup/admin.py:793
 msgid "Sorry, try again..."
 msgstr "Lo lamento, intente nuevamente..."
 
-#: ../roundup/admin.py:790
+#: ../roundup/admin.py:797
 #, python-format
 msgid "%(propname)s (%(proptype)s): "
-msgstr ""
+msgstr "%(propname)s (%(proptype)s): "
 
-#: ../roundup/admin.py:808
+#: ../roundup/admin.py:815
 #, python-format
 msgid "you must provide the \"%(propname)s\" property."
 msgstr "debe proveer la propiedad \"%(propname)s\"."
 
-#: ../roundup/admin.py:819
+#: ../roundup/admin.py:827
 msgid ""
 "Usage: list classname [property]\n"
 "        List the instances of a class.\n"
@@ -704,16 +704,16 @@
 "clase.\n"
 "        "
 
-#: ../roundup/admin.py:832
+#: ../roundup/admin.py:840
 msgid "Too many arguments supplied"
 msgstr "Demasiados argumentos"
 
-#: ../roundup/admin.py:868
+#: ../roundup/admin.py:876
 #, python-format
 msgid "%(nodeid)4s: %(value)s"
-msgstr ""
+msgstr "%(nodeid)4s: %(value)s"
 
-#: ../roundup/admin.py:872
+#: ../roundup/admin.py:880
 msgid ""
 "Usage: table classname [property[,property]*]\n"
 "        List the instances of a class in tabular form.\n"
@@ -777,12 +777,12 @@
 "        caracteres.\n"
 "        "
 
-#: ../roundup/admin.py:916
+#: ../roundup/admin.py:924
 #, python-format
 msgid "\"%(spec)s\" not name:width"
 msgstr "\"%(spec)s\" no es de la forma nombre:longitud"
 
-#: ../roundup/admin.py:966
+#: ../roundup/admin.py:974
 msgid ""
 "Usage: history designator\n"
 "        Show the history entries of a designator.\n"
@@ -798,7 +798,7 @@
 "        designador.\n"
 "        "
 
-#: ../roundup/admin.py:987
+#: ../roundup/admin.py:995
 msgid ""
 "Usage: commit\n"
 "        Commit changes made to the database during an interactive session.\n"
@@ -823,7 +823,7 @@
 "        son automáticamente escritos si resultan exitosos.\n"
 "        "
 
-#: ../roundup/admin.py:1001
+#: ../roundup/admin.py:1010
 msgid ""
 "Usage: rollback\n"
 "        Undo all changes that are pending commit to the database.\n"
@@ -845,7 +845,7 @@
 "        no introduciría cambios en la base de datos.\n"
 "        "
 
-#: ../roundup/admin.py:1013
+#: ../roundup/admin.py:1023
 msgid ""
 "Usage: retire designator[,designator]*\n"
 "        Retire the node specified by designator.\n"
@@ -862,7 +862,7 @@
 "        reusado.\n"
 "        "
 
-#: ../roundup/admin.py:1036
+#: ../roundup/admin.py:1047
 msgid ""
 "Usage: restore designator[,designator]*\n"
 "        Restore the retired node specified by designator.\n"
@@ -878,30 +878,64 @@
 "        "
 
 #. grab the directory to export to
-#: ../roundup/admin.py:1058
+#: ../roundup/admin.py:1070
 msgid ""
-"Usage: export [class[,class]] export_dir\n"
+"Usage: export [[-]class[,class]] export_dir\n"
 "        Export the database to colon-separated-value files.\n"
+"        To exclude the files (e.g. for the msg or file class),\n"
+"        use the exporttables command.\n"
 "\n"
-"        Optionally limit the export to just the names classes.\n"
+"        Optionally limit the export to just the named classes\n"
+"        or exclude the named classes, if the 1st argument starts with '-'.\n"
 "\n"
 "        This action exports the current data from the database into\n"
 "        colon-separated-value files that are placed in the nominated\n"
 "        destination directory.\n"
 "        "
 msgstr ""
-"Uso:   export [clase[,clase]] dir_exportación\n"
-"        Exporta la base de datos a archivos de valores separados por comas.\n"
+"Uso:   export [[-]clase[,clase]] dir_exportación\n"
+"        Exporta la base de datos a ficheros de valores separados por comas.\n"
+"        Para excluir los ficheros (por ej. en las clases msg o file),\n"
+"        use el comando exporttables.\n"
 "\n"
-"        Opcionalmente limita la exportación sólo a las clases\n"
-"        especificadas.\n"
+"        Opcionalmente limita la exportación sólo a las clases especifica-\n"
+"        das o las excluye si el primer argumento comienza con '-'.\n"
 "\n"
 "        Esta acción exporta los datos actuales desde la base de datos a\n"
-"        archivos de valores separados por comas que se colocarán en el\n"
+"        ficheros de valores separados por comas que se colocarán en el\n"
 "        directorio de destino especificado (dir_exportación).\n"
 "        "
 
-#: ../roundup/admin.py:1116
+#: ../roundup/admin.py:1145
+msgid ""
+"Usage: exporttables [[-]class[,class]] export_dir\n"
+"        Export the database to colon-separated-value files, excluding the\n"
+"        files below $TRACKER_HOME/db/files/ (which can be archived "
+"separately).\n"
+"        To include the files, use the export command.\n"
+"\n"
+"        Optionally limit the export to just the named classes\n"
+"        or exclude the named classes, if the 1st argument starts with '-'.\n"
+"\n"
+"        This action exports the current data from the database into\n"
+"        colon-separated-value files that are placed in the nominated\n"
+"        destination directory.\n"
+"        "
+msgstr ""
+"Uso:   export [clase[,clase]] dir_exportación\n"
+"        Exporta la base de datos a ficheros de valores separados por comas,\n"
+"        excluyendo los ficheros en $TRACKER_HOME/db/files/ (los cuales\n"
+"        pueden ser archivados por separado).\n"
+"\n"
+"        Opcionalmente limita la exportación sólo a las clases especifica-\n"
+"        das o las excluye si el primer argumento comienza con '-'.\n"
+"\n"
+"        Esta acción exporta los datos actuales desde la base de datos a\n"
+"        ficheros de valores separados por comas que se colocarán en el\n"
+"        directorio de destino especificado.\n"
+"        "
+
+#: ../roundup/admin.py:1160
 msgid ""
 "Usage: import import_dir\n"
 "        Import a database from the directory containing CSV files,\n"
@@ -924,10 +958,10 @@
 "        "
 msgstr ""
 "Uso:   import dir_importación\n"
-"        Importa una base de datos desde el directorio conteniendo archivos\n"
+"        Importa una base de datos desde el directorio conteniendo ficheros\n"
 "        CSV, dos por cada clase a importar.\n"
 "\n"
-"        Los archivos usados en la importación son:\n"
+"        Los ficheros usados en la importación son:\n"
 "\n"
 "        <clase>.csv\n"
 "          Este debe definir las mismas propiedades que la clase (esto\n"
@@ -937,7 +971,7 @@
 "          Este define los journals para los items que se están importando.\n"
 "\n"
 "        Los nodos importados tendrán los mismos id´s que los nodos según\n"
-"        se encontraban definidos en el archivo importado, por lo tanto\n"
+"        se encontraban definidos en el fichero importado, por lo tanto\n"
 "        reemplazarán todo contenido preexistente.\n"
 "\n"
 "        Los nuevos nodos son agregados a la base de datos existente - si\n"
@@ -946,7 +980,7 @@
 "        tediosamente, retirar toda los datos viejos.)\n"
 "        "
 
-#: ../roundup/admin.py:1189
+#: ../roundup/admin.py:1235
 msgid ""
 "Usage: pack period | date\n"
 "\n"
@@ -985,11 +1019,11 @@
 "\n"
 "        "
 
-#: ../roundup/admin.py:1217
+#: ../roundup/admin.py:1263
 msgid "Invalid format"
 msgstr "Formato inválido"
 
-#: ../roundup/admin.py:1227
+#: ../roundup/admin.py:1274
 msgid ""
 "Usage: reindex [classname|designator]*\n"
 "        Re-generate a tracker's search indexes.\n"
@@ -1005,12 +1039,12 @@
 "        Es un comando que por lo general se ejecuta automáticamente.\n"
 "        "
 
-#: ../roundup/admin.py:1241
+#: ../roundup/admin.py:1288
 #, python-format
 msgid "no such item \"%(designator)s\""
 msgstr "no existe un ítem llamado \"%(designator)s\""
 
-#: ../roundup/admin.py:1251
+#: ../roundup/admin.py:1298
 msgid ""
 "Usage: security [Role name]\n"
 "        Display the Permissions available to one or all Roles.\n"
@@ -1020,81 +1054,82 @@
 "        Muestra los permisos disponibles para uno o todos los Roles.\n"
 "        "
 
-#: ../roundup/admin.py:1259
+#: ../roundup/admin.py:1306
 #, python-format
 msgid "No such Role \"%(role)s\""
 msgstr "No existe un Rol llamado \"%(role)s\""
 
-#: ../roundup/admin.py:1265
+#: ../roundup/admin.py:1312
 #, python-format
 msgid "New Web users get the Roles \"%(role)s\""
 msgstr "Los nuevos usuarios creados vía Web obtiene los Roles \"%(role)s\""
 
-#: ../roundup/admin.py:1267
+#: ../roundup/admin.py:1314
 #, python-format
 msgid "New Web users get the Role \"%(role)s\""
 msgstr "Los nuevos usuarios creados vía Web obtienen el Rol \"%(role)s\""
 
-#: ../roundup/admin.py:1270
+#: ../roundup/admin.py:1317
 #, python-format
 msgid "New Email users get the Roles \"%(role)s\""
 msgstr ""
 "Los nuevos usuarios creados vía e-mail obtienen los Roles  \"%(role)s\""
 
-#: ../roundup/admin.py:1272
+#: ../roundup/admin.py:1319
 #, python-format
 msgid "New Email users get the Role \"%(role)s\""
 msgstr "Los nuevos usuarios creados vía e-mail obtienen el Rol \"%(role)s\""
 
-#: ../roundup/admin.py:1275
+#: ../roundup/admin.py:1322
 #, python-format
 msgid "Role \"%(name)s\":"
 msgstr "Rol \"%(name)s\":"
 
-#: ../roundup/admin.py:1280
+#: ../roundup/admin.py:1327
 #, python-format
 msgid " %(description)s (%(name)s for \"%(klass)s\": %(properties)s only)"
 msgstr ""
 " %(description)s (%(name)s para \"%(klass)s\": %(properties)s solamente)"
 
-#: ../roundup/admin.py:1283
+#: ../roundup/admin.py:1330
 #, python-format
 msgid " %(description)s (%(name)s for \"%(klass)s\" only)"
 msgstr " %(description)s (%(name)s para \"%(klass)s\" solamente)"
 
-#: ../roundup/admin.py:1286
+#: ../roundup/admin.py:1333
 #, python-format
 msgid " %(description)s (%(name)s)"
-msgstr ""
+msgstr " %(description)s (%(name)s)"
 
-#: ../roundup/admin.py:1315
+#: ../roundup/admin.py:1362
 #, python-format
 msgid "Unknown command \"%(command)s\" (\"help commands\" for a list)"
 msgstr ""
 "Comando desconocido \"%(command)s\" (tipee \"help commands\" para obtener "
 "una lista)"
 
-#: ../roundup/admin.py:1321
+#: ../roundup/admin.py:1368
 #, python-format
 msgid "Multiple commands match \"%(command)s\": %(list)s"
 msgstr "Coinciden mas de un comando \"%(command)s\": %(list)s"
 
-#: ../roundup/admin.py:1328
+#: ../roundup/admin.py:1375
 msgid "Enter tracker home: "
 msgstr "Ingrese directorio base del tracker: "
 
 # ../roundup/admin.py:1296 :1302 :1322
-#: ../roundup/admin.py:1335 ../roundup/admin.py:1341 ../roundup/admin.py:1361
+#: ../roundup/admin.py:1382 ../roundup/admin.py:1388 ../roundup/admin.py:1408
+#: ../roundup/admin.py:1382:1388:1408
 #, python-format
 msgid "Error: %(message)s"
-msgstr ""
+msgstr "Error: %(message)s"
 
-#: ../roundup/admin.py:1349
+#: ../roundup/admin.py:1396
 #, python-format
 msgid "Error: Couldn't open tracker: %(message)s"
 msgstr "Error: No se pudo abrir el tracker: %(message)s"
 
-#: ../roundup/admin.py:1374
+#: ../roundup/admin.py:1421
 #, python-format
 msgid ""
 "Roundup %s ready for input.\n"
@@ -1103,48 +1138,48 @@
 "Roundup %s listo para comandos.\n"
 "Tipee \"help\" para ayuda."
 
-#: ../roundup/admin.py:1379
+#: ../roundup/admin.py:1426
 msgid "Note: command history and editing not available"
 msgstr "Nota: historia y edición de comandos no disponible"
 
-#: ../roundup/admin.py:1383
+#: ../roundup/admin.py:1430
 msgid "roundup> "
-msgstr ""
+msgstr "roundup> "
 
-#: ../roundup/admin.py:1385
+#: ../roundup/admin.py:1432
 msgid "exit..."
 msgstr "salir..."
 
-#: ../roundup/admin.py:1395
+#: ../roundup/admin.py:1442
 msgid "There are unsaved changes. Commit them (y/N)? "
 msgstr "Hay cambios sin guardar. Debo guardar los mismos (y/N)? "
 
-#: ../roundup/backends/back_anydbm.py:2001
+#: ../roundup/backends/back_anydbm.py:2004
 #, python-format
 msgid "WARNING: invalid date tuple %r"
 msgstr "ATENCIÓN: tuple de fecha inválido %r"
 
-#: ../roundup/backends/rdbms_common.py:1434
+#: ../roundup/backends/rdbms_common.py:1445
 msgid "create"
 msgstr "crea"
 
-#: ../roundup/backends/rdbms_common.py:1600
+#: ../roundup/backends/rdbms_common.py:1611
 msgid "unlink"
 msgstr "desenlaza"
 
-#: ../roundup/backends/rdbms_common.py:1604
+#: ../roundup/backends/rdbms_common.py:1615
 msgid "link"
 msgstr "enlaza"
 
-#: ../roundup/backends/rdbms_common.py:1724
+#: ../roundup/backends/rdbms_common.py:1737
 msgid "set"
 msgstr "asigna"
 
-#: ../roundup/backends/rdbms_common.py:1748
+#: ../roundup/backends/rdbms_common.py:1761
 msgid "retired"
 msgstr "retira"
 
-#: ../roundup/backends/rdbms_common.py:1778
+#: ../roundup/backends/rdbms_common.py:1791
 msgid "restored"
 msgstr "restaura"
 
@@ -1177,54 +1212,56 @@
 msgstr "%(classname)s %(itemid)s ha sido retirado"
 
 # ../roundup/cgi/actions.py:163 :191
-#: ../roundup/cgi/actions.py:174 ../roundup/cgi/actions.py:202
+#: ../roundup/cgi/actions.py:169 ../roundup/cgi/actions.py:197
+#: ../roundup/cgi/actions.py:169:197
 msgid "You do not have permission to edit queries"
 msgstr "Ud. no posee los permisos necesarios para editar consultas"
 
 # ../roundup/cgi/actions.py:169 :197
-#: ../roundup/cgi/actions.py:180 ../roundup/cgi/actions.py:209
+#: ../roundup/cgi/actions.py:175 ../roundup/cgi/actions.py:204
+#: ../roundup/cgi/actions.py:175:204
 msgid "You do not have permission to store queries"
 msgstr "Ud. no posee los permisos necesarios para grabar consultas"
 
-#: ../roundup/cgi/actions.py:297
+#: ../roundup/cgi/actions.py:310
 #, python-format
 msgid "Not enough values on line %(line)s"
 msgstr "No hay valores suficientes en la línea %(line)s"
 
-#: ../roundup/cgi/actions.py:344
+#: ../roundup/cgi/actions.py:357
 msgid "Items edited OK"
 msgstr "Items editados exitosamente"
 
-#: ../roundup/cgi/actions.py:404
+#: ../roundup/cgi/actions.py:416
 #, python-format
 msgid "%(class)s %(id)s %(properties)s edited ok"
 msgstr "Edición exitosa de %(properties)s de %(class)s %(id)s"
 
-#: ../roundup/cgi/actions.py:407
+#: ../roundup/cgi/actions.py:419
 #, python-format
 msgid "%(class)s %(id)s - nothing changed"
 msgstr "%(class)s %(id)s - sin modificaciones"
 
-#: ../roundup/cgi/actions.py:419
+#: ../roundup/cgi/actions.py:431
 #, python-format
 msgid "%(class)s %(id)s created"
 msgstr "%(class)s %(id)s creado"
 
-#: ../roundup/cgi/actions.py:451
+#: ../roundup/cgi/actions.py:463
 #, python-format
 msgid "You do not have permission to edit %(class)s"
 msgstr "Ud. no posee los permisos necesarios para editar %(class)s"
 
-#: ../roundup/cgi/actions.py:463
+#: ../roundup/cgi/actions.py:475
 #, python-format
 msgid "You do not have permission to create %(class)s"
 msgstr "Ud. no posee los permisos necesarios para crear %(class)s"
 
-#: ../roundup/cgi/actions.py:487
+#: ../roundup/cgi/actions.py:499
 msgid "You do not have permission to edit user roles"
 msgstr "Ud. no posee los permisos necesarios para editar roles de usuario"
 
-#: ../roundup/cgi/actions.py:537
+#: ../roundup/cgi/actions.py:549
 #, python-format
 msgid ""
 "Edit Error: someone else has edited this %s (%s). View <a target=\"new\" "
@@ -1234,19 +1271,20 @@
 "\"new\" href=\"%s%s\">cambios</a> que dicha persona ha realizado en una "
 "ventana aparte."
 
-#: ../roundup/cgi/actions.py:565
+#: ../roundup/cgi/actions.py:577
 #, python-format
 msgid "Edit Error: %s"
 msgstr "Error de edición: %s"
 
 # ../roundup/cgi/actions.py:579 :590 :761 :780
-#: ../roundup/cgi/actions.py:596 ../roundup/cgi/actions.py:607
-#: ../roundup/cgi/actions.py:778 ../roundup/cgi/actions.py:797
+#: ../roundup/cgi/actions.py:608 ../roundup/cgi/actions.py:619
+#: ../roundup/cgi/actions.py:790 ../roundup/cgi/actions.py:809
+#: ../roundup/cgi/actions.py:608:619 :790:809
 #, python-format
 msgid "Error: %s"
-msgstr ""
+msgstr "Error: %s"
 
-#: ../roundup/cgi/actions.py:633
+#: ../roundup/cgi/actions.py:645
 msgid ""
 "Invalid One Time Key!\n"
 "(a Mozilla bug may cause this message to show up erroneously, please check "
@@ -1256,50 +1294,51 @@
 "(un bug de Mozilla puede ser el causante de que se visualice este mensaje en "
 "forma errónea, por favor verifique su casilla de e-mail)"
 
-#: ../roundup/cgi/actions.py:675
+#: ../roundup/cgi/actions.py:687
 #, python-format
 msgid "Password reset and email sent to %s"
 msgstr "Contraseña reinicializada y mensaje de e-mail enviado a %s"
 
-#: ../roundup/cgi/actions.py:684
+#: ../roundup/cgi/actions.py:696
 msgid "Unknown username"
 msgstr "Usuario desconocido"
 
-#: ../roundup/cgi/actions.py:692
+#: ../roundup/cgi/actions.py:704
 msgid "Unknown email address"
 msgstr "Dirección de e-mail desconocida"
 
-#: ../roundup/cgi/actions.py:697
+#: ../roundup/cgi/actions.py:709
 msgid "You need to specify a username or address"
 msgstr "Debe especificar un nombre de usuario o dirección de e-mail"
 
-#: ../roundup/cgi/actions.py:722
+#: ../roundup/cgi/actions.py:734
 #, python-format
 msgid "Email sent to %s"
 msgstr "Se ha enviado un mensaje de e-mail a %s"
 
-#: ../roundup/cgi/actions.py:741
+#: ../roundup/cgi/actions.py:753
 msgid "You are now registered, welcome!"
 msgstr "Ud. se ha registrado exitosamente, bienvenido!"
 
-#: ../roundup/cgi/actions.py:786
+#: ../roundup/cgi/actions.py:798
 msgid "It is not permitted to supply roles at registration."
 msgstr "No está permitido especificar roles en el momento del registro."
 
-#: ../roundup/cgi/actions.py:878
+#: ../roundup/cgi/actions.py:890
 msgid "You are logged out"
 msgstr "Ha salido del sistema exitosamente"
 
-#: ../roundup/cgi/actions.py:895
+#: ../roundup/cgi/actions.py:907
 msgid "Username required"
 msgstr "Se requiere el ingreso de un nombre de usuario"
 
 # ../roundup/cgi/actions.py:891 :895
-#: ../roundup/cgi/actions.py:930 ../roundup/cgi/actions.py:934
+#: ../roundup/cgi/actions.py:942 ../roundup/cgi/actions.py:946
+#: ../roundup/cgi/actions.py:942:946
 msgid "Invalid login"
 msgstr "nombre de usuario ó contraseña inválidos"
 
-#: ../roundup/cgi/actions.py:940
+#: ../roundup/cgi/actions.py:952
 msgid "You do not have permission to login"
 msgstr "Ud. no tiene permiso para ingresar al sistema"
 
@@ -1317,7 +1356,7 @@
 #: ../roundup/cgi/cgitb.py:64
 #, python-format
 msgid "<li>\"%(name)s\" (%(info)s)</li>"
-msgstr ""
+msgstr "<li>\"%(name)s\" (%(info)s)</li>"
 
 #: ../roundup/cgi/cgitb.py:67
 #, python-format
@@ -1360,7 +1399,7 @@
 #: ../roundup/cgi/cgitb.py:116
 #, python-format
 msgid "<font size=+1><strong>%(exc_type)s</strong>: %(exc_value)s</font>"
-msgstr ""
+msgstr "<font size=+1><strong>%(exc_type)s</strong>: %(exc_value)s</font>"
 
 #: ../roundup/cgi/cgitb.py:120
 msgid ""
@@ -1384,6 +1423,7 @@
 
 # ../roundup/cgi/cgitb.py:172 :178
 #: ../roundup/cgi/cgitb.py:172 ../roundup/cgi/cgitb.py:178
+#: ../roundup/cgi/cgitb.py:172:178
 msgid "<em>undefined</em>"
 msgstr "<em>indefinido/a</em>"
 
@@ -1402,29 +1442,29 @@
 "p>\n"
 "</body></html>"
 
-#: ../roundup/cgi/client.py:308
+#: ../roundup/cgi/client.py:339
 msgid "Form Error: "
 msgstr "Error de formulario"
 
-#: ../roundup/cgi/client.py:363
+#: ../roundup/cgi/client.py:394
 #, python-format
 msgid "Unrecognized charset: %r"
 msgstr "Conjunto de caracteres desconocido: %r"
 
-#: ../roundup/cgi/client.py:491
+#: ../roundup/cgi/client.py:522
 msgid "Anonymous users are not allowed to use the web interface"
 msgstr "Los usuarios anonimos no tienen permitido usar esta interfaz Web"
 
-#: ../roundup/cgi/client.py:646
+#: ../roundup/cgi/client.py:677
 msgid "You are not allowed to view this file."
-msgstr "Ud. no tiene permitido ver este archivo"
+msgstr "Ud. no tiene permitido ver este fichero"
 
-#: ../roundup/cgi/client.py:738
+#: ../roundup/cgi/client.py:770
 #, python-format
 msgid "%(starttag)sTime elapsed: %(seconds)fs%(endtag)s\n"
 msgstr "%(starttag)sTiempo transcurrido: %(seconds)fs%(endtag)s\n"
 
-#: ../roundup/cgi/client.py:742
+#: ../roundup/cgi/client.py:774
 #, python-format
 msgid ""
 "%(starttag)sCache hits: %(cache_hits)d, misses %(cache_misses)d. Loading "
@@ -1435,15 +1475,24 @@
 
 #: ../roundup/cgi/form_parser.py:283
 #, python-format
-msgid "link \"%(key)s\" value \"%(value)s\" not a designator"
-msgstr "el enlace \"%(key)s\" valor \"%(value)s\" no es un designador"
+msgid "link \"%(key)s\" value \"%(entry)s\" not a designator"
+msgstr "el enlace \"%(key)s\" valor \"%(entry)s\" no es un designador"
 
-#: ../roundup/cgi/form_parser.py:290
+#: ../roundup/cgi/form_parser.py:301
 #, python-format
 msgid "%(class)s %(property)s is not a link or multilink property"
 msgstr "%(property)s de %(class)s no es una propiedad enlace o multilink"
 
-#: ../roundup/cgi/form_parser.py:312
+#: ../roundup/cgi/form_parser.py:313
+#, python-format
+msgid ""
+"The form action claims to require property \"%(property)s\" which doesn't "
+"exist"
+msgstr ""
+"La accion de formulario especifica que requiere la propiedad "
+"\"%(property)s\" la cual no existe"
+
+#: ../roundup/cgi/form_parser.py:335
 #, python-format
 msgid ""
 "You have submitted a %(action)s action for the property \"%(property)s\" "
@@ -1453,24 +1502,26 @@
 "existe"
 
 # ../roundup/cgi/form_parser.py:331 :357
-#: ../roundup/cgi/form_parser.py:331 ../roundup/cgi/form_parser.py:357
+#: ../roundup/cgi/form_parser.py:354 ../roundup/cgi/form_parser.py:380
+#: ../roundup/cgi/form_parser.py:354:380
 #, python-format
 msgid "You have submitted more than one value for the %s property"
 msgstr "Ha ingresado más de un valor para la propiedad %s"
 
 # ../roundup/cgi/form_parser.py:354 :360
-#: ../roundup/cgi/form_parser.py:354 ../roundup/cgi/form_parser.py:360
+#: ../roundup/cgi/form_parser.py:377 ../roundup/cgi/form_parser.py:383
+#: ../roundup/cgi/form_parser.py:377:383
 msgid "Password and confirmation text do not match"
 msgstr "La contraseña y el texto de confirmación no coinciden"
 
-#: ../roundup/cgi/form_parser.py:395
+#: ../roundup/cgi/form_parser.py:418
 #, python-format
 msgid "property \"%(propname)s\": \"%(value)s\" not currently in list"
 msgstr ""
 "propiedad \"%(propname)s\": \"%(value)s\" no se encuentra en este momento en "
 "la lista"
 
-#: ../roundup/cgi/form_parser.py:512
+#: ../roundup/cgi/form_parser.py:551
 #, python-format
 msgid "Required %(class)s property %(property)s not supplied"
 msgid_plural "Required %(class)s properties %(property)s not supplied"
@@ -1481,120 +1532,126 @@
 "Las propiedades %(property)s de la clase %(class)s son obligatorias y no se "
 "han provisto"
 
-#: ../roundup/cgi/form_parser.py:535
+#: ../roundup/cgi/form_parser.py:574
 msgid "File is empty"
-msgstr "El archivo está vacío"
+msgstr "El fichero está vacío"
 
-#: ../roundup/cgi/templating.py:72
+#: ../roundup/cgi/templating.py:77
 #, python-format
 msgid "You are not allowed to %(action)s items of class %(class)s"
 msgstr "Ud. no tiene permitido %(action)s items de la clase %(class)s"
 
-#: ../roundup/cgi/templating.py:627
+#: ../roundup/cgi/templating.py:657
 msgid "(list)"
 msgstr "(lista)"
 
-#: ../roundup/cgi/templating.py:696
+#: ../roundup/cgi/templating.py:726
 msgid "Submit New Entry"
 msgstr "Crear nuevo elemento"
 
 # ../roundup/cgi/templating.py:673 :792 :1166 :1187 :1231 :1253 :1287 :1326
 # :1377 :1394 :1470 :1490 :1503 :1520 :1530 :1580 :1755
-#: ../roundup/cgi/templating.py:710 ../roundup/cgi/templating.py:829
-#: ../roundup/cgi/templating.py:1236 ../roundup/cgi/templating.py:1257
-#: ../roundup/cgi/templating.py:1304 ../roundup/cgi/templating.py:1327
-#: ../roundup/cgi/templating.py:1361 ../roundup/cgi/templating.py:1400
-#: ../roundup/cgi/templating.py:1453 ../roundup/cgi/templating.py:1470
-#: ../roundup/cgi/templating.py:1549 ../roundup/cgi/templating.py:1569
-#: ../roundup/cgi/templating.py:1587 ../roundup/cgi/templating.py:1619
-#: ../roundup/cgi/templating.py:1629 ../roundup/cgi/templating.py:1683
-#: ../roundup/cgi/templating.py:1875
+#: ../roundup/cgi/templating.py:740 ../roundup/cgi/templating.py:873
+#: ../roundup/cgi/templating.py:1294 ../roundup/cgi/templating.py:1323
+#: ../roundup/cgi/templating.py:1343 ../roundup/cgi/templating.py:1356
+#: ../roundup/cgi/templating.py:1407 ../roundup/cgi/templating.py:1430
+#: ../roundup/cgi/templating.py:1466 ../roundup/cgi/templating.py:1503
+#: ../roundup/cgi/templating.py:1556 ../roundup/cgi/templating.py:1573
+#: ../roundup/cgi/templating.py:1657 ../roundup/cgi/templating.py:1677
+#: ../roundup/cgi/templating.py:1695 ../roundup/cgi/templating.py:1727
+#: ../roundup/cgi/templating.py:1737 ../roundup/cgi/templating.py:1789
+#: ../roundup/cgi/templating.py:1978 ../roundup/cgi/templating.py:740:873
+#: :1294:1323 :1343:1356 :1407:1430 :1466:1503 :1556:1573 :1657:1677
+#: :1695:1727 :1737:1789:1978
 msgid "[hidden]"
 msgstr "[oculto]"
 
-#: ../roundup/cgi/templating.py:711
+#: ../roundup/cgi/templating.py:741
 msgid "New node - no history"
 msgstr "Nuevo nodo - sin historia"
 
-#: ../roundup/cgi/templating.py:811
+#: ../roundup/cgi/templating.py:855
 msgid "Submit Changes"
 msgstr "Enviar modificaciones"
 
-#: ../roundup/cgi/templating.py:893
+#: ../roundup/cgi/templating.py:937
 msgid "<em>The indicated property no longer exists</em>"
 msgstr "<em>La propiedad indicada ya no existe</em>"
 
-#: ../roundup/cgi/templating.py:894
+#: ../roundup/cgi/templating.py:938
 #, python-format
 msgid "<em>%s: %s</em>\n"
-msgstr ""
+msgstr "<em>%s: %s</em>\n"
 
-#: ../roundup/cgi/templating.py:907
+#: ../roundup/cgi/templating.py:951
 #, python-format
 msgid "The linked class %(classname)s no longer exists"
 msgstr "La clase relacionada %(classname)s ya no existe"
 
 # ../roundup/cgi/templating.py:903 :924
-#: ../roundup/cgi/templating.py:940 ../roundup/cgi/templating.py:964
+#: ../roundup/cgi/templating.py:984 ../roundup/cgi/templating.py:1008
+#: ../roundup/cgi/templating.py:984:1008
 msgid "<strike>The linked node no longer exists</strike>"
 msgstr "<strike>El nodo relacionado ya no existe</strike>"
 
-#: ../roundup/cgi/templating.py:1006 ../roundup/cgi/templating.py:1404
-#: ../roundup/cgi/templating.py:1425 ../roundup/cgi/templating.py:1431
-msgid "No"
-msgstr ""
-
-#: ../roundup/cgi/templating.py:1006 ../roundup/cgi/templating.py:1404
-#: ../roundup/cgi/templating.py:1423 ../roundup/cgi/templating.py:1428
-msgid "Yes"
-msgstr "Si"
-
-#: ../roundup/cgi/templating.py:1017
+#: ../roundup/cgi/templating.py:1061
 #, python-format
 msgid "%s: (no value)"
 msgstr "%s: (sin valor)"
 
-#: ../roundup/cgi/templating.py:1029
+#: ../roundup/cgi/templating.py:1073
 msgid ""
 "<strong><em>This event is not handled by the history display!</em></strong>"
 msgstr ""
 "<strong><em>Este evento no es soportado por la visualización de historia!</"
 "em></strong>"
 
-#: ../roundup/cgi/templating.py:1041
+#: ../roundup/cgi/templating.py:1085
 msgid "<tr><td colspan=4><strong>Note:</strong></td></tr>"
 msgstr "<tr><td colspan=4><strong>Nota:</strong></td></tr>"
 
-#: ../roundup/cgi/templating.py:1050
+#: ../roundup/cgi/templating.py:1094
 msgid "History"
 msgstr "Historia"
 
-#: ../roundup/cgi/templating.py:1052
+#: ../roundup/cgi/templating.py:1096
 msgid "<th>Date</th>"
 msgstr "<th>Fecha</th>"
 
-#: ../roundup/cgi/templating.py:1053
+#: ../roundup/cgi/templating.py:1097
 msgid "<th>User</th>"
 msgstr "<th>Usuario</th>"
 
-#: ../roundup/cgi/templating.py:1054
+#: ../roundup/cgi/templating.py:1098
 msgid "<th>Action</th>"
 msgstr "<th>Acción</th>"
 
-#: ../roundup/cgi/templating.py:1055
+#: ../roundup/cgi/templating.py:1099
 msgid "<th>Args</th>"
-msgstr ""
+msgstr "<th>Args</th>"
 
-#: ../roundup/cgi/templating.py:1097
+#: ../roundup/cgi/templating.py:1141
 #, python-format
 msgid "Copy of %(class)s %(id)s"
 msgstr "Copia de %(class)s %(id)s"
 
-#: ../roundup/cgi/templating.py:1331
+#: ../roundup/cgi/templating.py:1434
 msgid "*encrypted*"
 msgstr "*cifrado*"
 
-#: ../roundup/cgi/templating.py:1514
+#: ../roundup/cgi/templating.py:1507 ../roundup/cgi/templating.py:1528
+#: ../roundup/cgi/templating.py:1534 ../roundup/cgi/templating.py:1050:1507
+#: :1528:1534
+msgid "No"
+msgstr "No"
+
+#: ../roundup/cgi/templating.py:1507 ../roundup/cgi/templating.py:1526
+#: ../roundup/cgi/templating.py:1531 ../roundup/cgi/templating.py:1050:1507
+#: :1526:1531
+msgid "Yes"
+msgstr "Si"
+
+#: ../roundup/cgi/templating.py:1620
 msgid ""
 "default value for DateHTMLProperty must be either DateHTMLProperty or string "
 "date representation."
@@ -1602,17 +1659,17 @@
 "el valor por defecto para DateHTMLProperty debe ser un DateHTMLProperty o "
 "una cadena que represente una fecha."
 
-#: ../roundup/cgi/templating.py:1674
+#: ../roundup/cgi/templating.py:1780
 #, python-format
 msgid "Attempt to look up %(attr)s on a missing value"
 msgstr "Se intentó buscar %(attr)s en un valor faltante"
 
-#: ../roundup/cgi/templating.py:1750
+#: ../roundup/cgi/templating.py:1853
 #, python-format
 msgid "<option %svalue=\"-1\">- no selection -</option>"
 msgstr "<option %svalue=\"-1\">- sin selección -</option>"
 
-#: ../roundup/date.py:186
+#: ../roundup/date.py:300
 msgid ""
 "Not a date spec: \"yyyy-mm-dd\", \"mm-dd\", \"HH:MM\", \"HH:MM:SS\" or "
 "\"yyyy-mm-dd.HH:MM:SS.SSS\""
@@ -1620,7 +1677,7 @@
 "No es una especificación de fecha: \"aaaa-mm-dd\", \"mm-dd\", \"HH:MM\", "
 "\"HH:MM:SS\" o \"aaaa-mm-dd.HH:MM:SS.SSS\""
 
-#: ../roundup/date.py:240
+#: ../roundup/date.py:359
 #, python-format
 msgid ""
 "%r not a date / time spec \"yyyy-mm-dd\", \"mm-dd\", \"HH:MM\", \"HH:MM:SS\" "
@@ -1629,113 +1686,113 @@
 "%r no es una especificación de fecha / hora \"aaaa-mm-dd\", \"mm-dd\", \"HH:"
 "MM\", \"HH:MM:SS\" o \"aaaa-mm-dd.HH:MM:SS.SSS\""
 
-#: ../roundup/date.py:538
+#: ../roundup/date.py:666
 msgid ""
 "Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [date spec]"
 msgstr ""
 "No es una especificación de intervalo de tiempo: [+-] [#a] [#m] [#s] [#d] "
 "[[[H]H:MM]:SS] [especific. fecha]"
 
-#: ../roundup/date.py:557
+#: ../roundup/date.py:685
 msgid "Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]"
 msgstr ""
 "No es una especificación de intervalo de tiempo: [+-] [#a] [#m] [#s] [#d] "
 "[[[H]H:MM]:SS]"
 
-#: ../roundup/date.py:694
+#: ../roundup/date.py:822
 #, python-format
 msgid "%(number)s year"
 msgid_plural "%(number)s years"
 msgstr[0] "%(number)s año"
 msgstr[1] "%(number)s años"
 
-#: ../roundup/date.py:698
+#: ../roundup/date.py:826
 #, python-format
 msgid "%(number)s month"
 msgid_plural "%(number)s months"
 msgstr[0] "%(number)s mes"
 msgstr[1] "%(number)s meses"
 
-#: ../roundup/date.py:702
+#: ../roundup/date.py:830
 #, python-format
 msgid "%(number)s week"
 msgid_plural "%(number)s weeks"
 msgstr[0] "%(number)s semana"
 msgstr[1] "%(number)s semanas"
 
-#: ../roundup/date.py:706
+#: ../roundup/date.py:834
 #, python-format
 msgid "%(number)s day"
 msgid_plural "%(number)s days"
 msgstr[0] "%(number)s día"
 msgstr[1] "%(number)s días"
 
-#: ../roundup/date.py:710
+#: ../roundup/date.py:838
 msgid "tomorrow"
 msgstr "mañana"
 
-#: ../roundup/date.py:712
+#: ../roundup/date.py:840
 msgid "yesterday"
 msgstr "ayer"
 
-#: ../roundup/date.py:715
+#: ../roundup/date.py:843
 #, python-format
 msgid "%(number)s hour"
 msgid_plural "%(number)s hours"
 msgstr[0] "%(number)s hora"
 msgstr[1] "%(number)s horas"
 
-#: ../roundup/date.py:719
+#: ../roundup/date.py:847
 msgid "an hour"
 msgstr "una hora"
 
-#: ../roundup/date.py:721
+#: ../roundup/date.py:849
 msgid "1 1/2 hours"
 msgstr "1 hora y 1/2"
 
-#: ../roundup/date.py:723
+#: ../roundup/date.py:851
 #, python-format
 msgid "1 %(number)s/4 hours"
 msgid_plural "1 %(number)s/4 hours"
 msgstr[0] "1 %(number)s/4 de hora"
 msgstr[1] "1 %(number)s/4 de hora"
 
-#: ../roundup/date.py:727
+#: ../roundup/date.py:855
 msgid "in a moment"
 msgstr "en un momento"
 
-#: ../roundup/date.py:729
+#: ../roundup/date.py:857
 msgid "just now"
 msgstr "ahora"
 
-#: ../roundup/date.py:732
+#: ../roundup/date.py:860
 msgid "1 minute"
 msgstr "1 minuto"
 
-#: ../roundup/date.py:735
+#: ../roundup/date.py:863
 #, python-format
 msgid "%(number)s minute"
 msgid_plural "%(number)s minutes"
 msgstr[0] "%(number)s minuto"
 msgstr[1] "%(number)s minutos"
 
-#: ../roundup/date.py:738
+#: ../roundup/date.py:866
 msgid "1/2 an hour"
 msgstr "media hora"
 
-#: ../roundup/date.py:740
+#: ../roundup/date.py:868
 #, python-format
 msgid "%(number)s/4 hour"
 msgid_plural "%(number)s/4 hours"
 msgstr[0] "%(number)s/4 de hora"
 msgstr[1] "%(number)s/4s de hora"
 
-#: ../roundup/date.py:744
+#: ../roundup/date.py:872
 #, python-format
 msgid "%s ago"
 msgstr "hace %s"
 
-#: ../roundup/date.py:746
+#: ../roundup/date.py:874
 #, python-format
 msgid "in %s"
 msgstr "en %s"
@@ -1749,7 +1806,7 @@
 "ATENCIÓN: El directorio '%s'\n"
 "\tcontiene una plantilla con el viejo formato - se ignorará"
 
-#: ../roundup/mailgw.py:586
+#: ../roundup/mailgw.py:584
 msgid ""
 "\n"
 "Emails to Roundup trackers must include a Subject: line!\n"
@@ -1757,7 +1814,7 @@
 "\n"
 "Todos los e-mails enviados a trackers Roundup deben incluir un Asunto:!\n"
 
-#: ../roundup/mailgw.py:674
+#: ../roundup/mailgw.py:708
 #, python-format
 msgid ""
 "\n"
@@ -1787,44 +1844,72 @@
 "\n"
 "El asunto que Ud. envió es: '%(subject)s'\n"
 
-#: ../roundup/mailgw.py:705
+#: ../roundup/mailgw.py:746
 #, python-format
 msgid ""
 "\n"
-"The class name you identified in the subject line (\"%(classname)s\") does "
-"not exist in the\n"
-"database.\n"
+"The class name you identified in the subject line (\"%(classname)s\") does\n"
+"not exist in the database.\n"
 "\n"
 "Valid class names are: %(validname)s\n"
 "Subject was: \"%(subject)s\"\n"
 msgstr ""
 "\n"
-"La clase que Ud. identificó en el Asunto (\"%(classname)s\") no existe en la "
-"base de\n"
-"datos.\n"
+"La clase que Ud. identificó en el Asunto (\"%(classname)s\") \n"
+"no existe en la base de datos.\n"
 "\n"
 "Nombres válidos de clases son: %(validname)s\n"
 "El asunto que Ud. envió es: \"%(subject)s\"\n"
 
-#: ../roundup/mailgw.py:733
+#: ../roundup/mailgw.py:754
+#, python-format
+msgid ""
+"\n"
+"You did not identify a class name in the subject line and there is no\n"
+"default set for this tracker. The subject must contain a class name or\n"
+"designator to indicate the 'topic' of the message. For example:\n"
+"    Subject: [issue] This is a new issue\n"
+"      - this will create a new issue in the tracker with the title 'This is\n"
+"        a new issue'.\n"
+"    Subject: [issue1234] This is a followup to issue 1234\n"
+"      - this will append the message's contents to the existing issue 1234\n"
+"        in the tracker.\n"
+"\n"
+"Subject was: '%(subject)s'\n"
+msgstr ""
+"\n"
+"Ud. no indicó un nombre de clase en Asunto y el tracker no tiene\n"
+"configurado un valor por omisión. El asunto debe contener un nombre\n"
+"de clase o designador para indicar para indicar el 'tópico' del mensaje.\n"
+"Por ejemplo:\n"
+"    Asunto: [issue] Este es un nuevo issue\n"
+"      - Esto creará un nuevo issue en el tracker con el título 'Este es un\n"
+"        nuevo issue'.\n"
+"    Asunto: [issue1234] Esta es un agregado al issue 1234\n"
+"      - Esto anexará el contenido del e-mail al issue 1234 ya existente\n"
+"        en el tracker.\n"
+"\n"
+"El asunto que Ud. envió es: '%(subject)s'\n"
+
+#: ../roundup/mailgw.py:795
 #, python-format
 msgid ""
 "\n"
 "I cannot match your message to a node in the database - you need to either\n"
-"supply a full designator (with number, eg \"[issue123]\" or keep the\n"
+"supply a full designator (with number, eg \"[issue123]\") or keep the\n"
 "previous subject title intact so I can match that.\n"
 "\n"
 "Subject was: \"%(subject)s\"\n"
 msgstr ""
 "\n"
 "No puedo encontrar un nodo en la base de datos que coincida con el mensaje\n"
-"que Ud. ha enviado - Necesita proveer un designador válido (con número, por\n"
-"ejemplo \"[issue123]\" o mantener intacto el Asunto previo de manera que yo\n"
-"pueda encontrar una coincidencia.\n"
+"que Ud. ha enviado - Necesita proveer un designador completo (con número,\n"
+"por ejemplo \"[issue123]\" o mantener intacto el Asunto previo de manera\n"
+"que yo pueda encontrar una coincidencia.\n"
 "\n"
 "El asunto que Ud. envió es: \"%(subject)s\"\n"
 
-#: ../roundup/mailgw.py:766
+#: ../roundup/mailgw.py:828
 #, python-format
 msgid ""
 "\n"
@@ -1839,7 +1924,7 @@
 "\n"
 "El asunto que Ud. envió es: \"%(subject)s\"\n"
 
-#: ../roundup/mailgw.py:794
+#: ../roundup/mailgw.py:856
 #, python-format
 msgid ""
 "\n"
@@ -1853,7 +1938,7 @@
 "incorrecta:\n"
 "  %(current_class)s\n"
 
-#: ../roundup/mailgw.py:817
+#: ../roundup/mailgw.py:879
 #, python-format
 msgid ""
 "\n"
@@ -1867,34 +1952,34 @@
 "incorrectas:\n"
 "  %(errors)s\n"
 
-#: ../roundup/mailgw.py:847
+#: ../roundup/mailgw.py:919
 #, python-format
 msgid ""
 "\n"
-"You are not a registered user.\n"
+"You are not a registered user.%(registration_info)s\n"
 "\n"
 "Unknown address: %(from_address)s\n"
 msgstr ""
 "\n"
-"Ud. no es un usuario registrado.\n"
+"Ud. no es un usuario registrado.%(registration_info)s\n"
 "\n"
 "Dirección desconocida: %(from_address)s\n"
 
-#: ../roundup/mailgw.py:855
+#: ../roundup/mailgw.py:927
 msgid "You are not permitted to access this tracker."
 msgstr "Ud. no posee los permisos necesarios para acceder a este tracker."
 
-#: ../roundup/mailgw.py:862
+#: ../roundup/mailgw.py:934
 #, python-format
 msgid "You are not permitted to edit %(classname)s."
 msgstr "Ud. no tiene permitido editar %(classname)s."
 
-#: ../roundup/mailgw.py:866
+#: ../roundup/mailgw.py:938
 #, python-format
 msgid "You are not permitted to create %(classname)s."
 msgstr "Ud. no tiene permitido crear %(classname)s."
 
-#: ../roundup/mailgw.py:913
+#: ../roundup/mailgw.py:985
 #, python-format
 msgid ""
 "\n"
@@ -1910,7 +1995,7 @@
 "\n"
 "El Asunto que Ud. envió es: \"%(subject)s\"\n"
 
-#: ../roundup/mailgw.py:942
+#: ../roundup/mailgw.py:1013
 msgid ""
 "\n"
 "Roundup requires the submission to be plain text. The message parser could\n"
@@ -1922,20 +2007,20 @@
 "podido localizar una parte MIME text/plain en su mensaje que pueda ser "
 "usada.\n"
 
-#: ../roundup/mailgw.py:964
+#: ../roundup/mailgw.py:1030
 msgid "You are not permitted to create files."
-msgstr "Ud. no tiene permitida la creación de archivos."
+msgstr "Ud. no tiene permitida la creación de ficheros."
 
-#: ../roundup/mailgw.py:978
+#: ../roundup/mailgw.py:1044
 #, python-format
 msgid "You are not permitted to add files to %(classname)s."
-msgstr "Ud. no tiene permitido agregar archivos a %(classname)s."
+msgstr "Ud. no tiene permitido agregar ficheros a %(classname)s."
 
-#: ../roundup/mailgw.py:996
+#: ../roundup/mailgw.py:1062
 msgid "You are not permitted to create messages."
 msgstr "Ud. no tiene permitido crear mensajes."
 
-#: ../roundup/mailgw.py:1004
+#: ../roundup/mailgw.py:1070
 #, python-format
 msgid ""
 "\n"
@@ -1946,19 +2031,19 @@
 "El mensaje de e-mail ha sido rechazado por un detector.\n"
 "%(error)s\n"
 
-#: ../roundup/mailgw.py:1012
+#: ../roundup/mailgw.py:1078
 #, python-format
 msgid "You are not permitted to add messages to %(classname)s."
 msgstr "Ud. no tiene permitido agregar mensajes a %(classname)s."
 
-#: ../roundup/mailgw.py:1039
+#: ../roundup/mailgw.py:1105
 #, python-format
 msgid "You are not permitted to edit property %(prop)s of class %(classname)s."
 msgstr ""
 "Ud. no tiene permitido editar la propiedad %(prop)s de la clase %(classname)"
 "s."
 
-#: ../roundup/mailgw.py:1047
+#: ../roundup/mailgw.py:1113
 #, python-format
 msgid ""
 "\n"
@@ -1969,77 +2054,98 @@
 "Ha habido un problema con el mensaje que envíó:\n"
 "   %(message)s\n"
 
-#: ../roundup/mailgw.py:1069
+#: ../roundup/mailgw.py:1135
 msgid "not of form [arg=value,value,...;arg=value,value,...]"
 msgstr "no es de la forma [arg=valor,valor,...;arg=valor,valor,...]"
 
-#: ../roundup/roundupdb.py:142
+#: ../roundup/roundupdb.py:147
 msgid "files"
-msgstr "archivos"
+msgstr "ficheros"
 
-#: ../roundup/roundupdb.py:142
+#: ../roundup/roundupdb.py:147
 msgid "messages"
 msgstr "mensajes"
 
-#: ../roundup/roundupdb.py:142
+#: ../roundup/roundupdb.py:147
 msgid "nosy"
 msgstr "interesados"
 
-#: ../roundup/roundupdb.py:142
+#: ../roundup/roundupdb.py:147
 msgid "superseder"
 msgstr "reemplazado por"
 
-#: ../roundup/roundupdb.py:142
+#: ../roundup/roundupdb.py:147
 msgid "title"
 msgstr "título"
 
-#: ../roundup/roundupdb.py:143
+#: ../roundup/roundupdb.py:148
 msgid "assignedto"
 msgstr "asignadoa"
 
-#: ../roundup/roundupdb.py:143
+#: ../roundup/roundupdb.py:148
+msgid "keyword"
+msgstr "Palabra clave"
+
+#: ../roundup/roundupdb.py:148
 msgid "priority"
 msgstr "prioridad"
 
-#: ../roundup/roundupdb.py:143
+#: ../roundup/roundupdb.py:148
 msgid "status"
 msgstr "estado"
 
-#: ../roundup/roundupdb.py:143
-msgid "topic"
-msgstr "palabraclave"
-
-#: ../roundup/roundupdb.py:146
+#: ../roundup/roundupdb.py:151
 msgid "activity"
 msgstr "actividad"
 
 #. following properties are common for all hyperdb classes
 #. they are listed here to keep things in one place
-#: ../roundup/roundupdb.py:146
+#: ../roundup/roundupdb.py:151
 msgid "actor"
 msgstr "últimoactor"
 
-#: ../roundup/roundupdb.py:146
+#: ../roundup/roundupdb.py:151
 msgid "creation"
 msgstr "creación"
 
-#: ../roundup/roundupdb.py:146
+#: ../roundup/roundupdb.py:151
 msgid "creator"
 msgstr "creador"
 
-#: ../roundup/roundupdb.py:304
+#: ../roundup/roundupdb.py:309
 #, python-format
 msgid "New submission from %(authname)s%(authaddr)s:"
 msgstr "Nuevo aporte de %(authname)s%(authaddr)s:"
 
-#: ../roundup/roundupdb.py:307
+#: ../roundup/roundupdb.py:312
 #, python-format
 msgid "%(authname)s%(authaddr)s added the comment:"
 msgstr "%(authname)s%(authaddr)s agregó el comentario:"
 
-#: ../roundup/roundupdb.py:310
-msgid "System message:"
-msgstr "Mensaje de sistema:"
+#: ../roundup/roundupdb.py:315
+#, python-format
+msgid "Change by %(authname)s%(authaddr)s:"
+msgstr "Modificación de %(authname)s%(authaddr)s:"
+
+#: ../roundup/roundupdb.py:342
+#, python-format
+msgid "File '%(filename)s' not attached - you can download it from %(link)s."
+msgstr "Fichero '%(filename)s' no anexado - puede descargarlo de %(link)s."
+
+#: ../roundup/roundupdb.py:615
+#, python-format
+msgid ""
+"\n"
+"Now:\n"
+"%(new)s\n"
+"Was:\n"
+"%(old)s"
+msgstr ""
+"\n"
+"Ahora:\n"
+"%(new)s\n"
+"Antes:\n"
+"%(old)s"
 
 #: ../roundup/scripts/roundup_demo.py:32
 #, python-format
@@ -2060,8 +2166,8 @@
 #: ../roundup/scripts/roundup_mailgw.py:36
 #, python-format
 msgid ""
-"Usage: %(program)s [-v] [-c] [[-C class] -S field=value]* <instance home> "
-"[method]\n"
+"Usage: %(program)s [-v] [-c class] [[-C class] -S field=value]* <instance "
+"home> [method]\n"
 "\n"
 "Options:\n"
 " -v: print version and exit\n"
@@ -2107,6 +2213,10 @@
 " are both valid. The username and/or password will be prompted for if\n"
 " not supplied on the command-line.\n"
 "\n"
+"POPS:\n"
+" Connect to a POP server over ssl. This requires python 2.4 or later.\n"
+" This supports the same notation as POP.\n"
+"\n"
 "APOP:\n"
 " Same as POP, but using Authenticated POP:\n"
 "    apop username:password at server\n"
@@ -2125,8 +2235,8 @@
 "    imaps username:password at server [mailbox]\n"
 "\n"
 msgstr ""
-"Uso: %(program)s [-v] [-c] [[-C clase] -S campo=valor]* <directorio base "
-"instancia> [método]\n"
+"Uso: %(program)s [-v] [-c clase] [[-C clase] -S campo=valor]* <directorio "
+"base instancia> [método]\n"
 "\n"
 "Opciones:\n"
 " -v: imprime version y sale\n"
@@ -2137,7 +2247,7 @@
 "La pasarela de correo de roundup puede ser invocada en una de cuatro "
 "formas:\n"
 " . con un directorio base de instancia como único argumento,\n"
-" . con un directorio base de instancia y un archivo de spool de correo,\n"
+" . con un directorio base de instancia y un fichero de spool de correo,\n"
 " . con un directorio base de instancia y una cuenta de un servidor POP/APOP, "
 "o\n"
 " . con un directorio base de instancia y una cuenta de un servidor IMAP/"
@@ -2145,15 +2255,12 @@
 "\n"
 "También soporta los argumentos opcionales -C y -S que le permiten "
 "establecer\n"
-"campos para una clase creada por la pasarela de correo de Roundup\n"
-"roundup-mailgw.\n"
-"La clase por omisión es msg, pero las otras clases: issue, file, user "
-"tambien\n"
-"pueden usarse. Las opciones -S y --set usan la notación\n"
-"propiedad=valor[;propiedad=valor] aceptada por el comando roundup de línea "
-"de\n"
-"comandos o los comandos que pueden ser pasados en el campo Asunto: de un\n"
-"mensaje de correo electrónico.\n"
+"campos para una clase creada por la pasarela de correo roundup-mailgw.\n"
+"La clase por omisión es msg, pero las otras clases: issue, file, user\n"
+"tambien pueden usarse. Las opciones -S y --set usan la misma notación\n"
+"propiedad=valor[;propiedad=valor] aceptada por el comando roundup de\n"
+"línea de comandos o los comandos que pueden ser pasados en el campo\n"
+"Asunto: de un mensaje de correo electrónico.\n"
 "\n"
 "También le permite establecer el tipo de mensaje basado en la dirección de\n"
 "correo usada.\n"
@@ -2163,10 +2270,10 @@
 " estándar y lo envía al módulo roundup.mailgw.\n"
 "\n"
 "UNIX mailbox:\n"
-" En el segundo caso, la pasarela lee todos los mensajes desde el archivo de\n"
+" En el segundo caso, la pasarela lee todos los mensajes desde el fichero de\n"
 " spool de correo y envía los mismos de a uno al módulo roundup.mailgw. El\n"
-" archivo se vacía una vez que todos los mensajes han sido procesados\n"
-" exitosamente. El archivo se especifica como:\n"
+" fichero se vacía una vez que todos los mensajes han sido procesados\n"
+" exitosamente. El fichero se especifica como:\n"
 "   mailbox /ruta/al/mailbox\n"
 "\n"
 "POP:\n"
@@ -2174,7 +2281,7 @@
 " POP y envía los mismos de a uno al módulo roundup.mailgw. El servidor\n"
 " POP se especifica como:\n"
 "    pop nombreusuario:contraseña at servidor\n"
-" El nombreusuario y la contraseña pueden omitirse:\n"
+" El nombreusuario y la contraseña pueden omitirse por lo que:\n"
 "    pop nombreusuario at servidor\n"
 "    pop servidor\n"
 " son válidos. El nombre de usuario y/o la contraseña se solicitarán si no\n"
@@ -2188,29 +2295,33 @@
 " Se conecta a un servidor IMAP. Esta forma soporta la misma notación que\n"
 " correo POP\n"
 "    imap nombreusuario:contraseña at servidor\n"
-" También le permite especificar una casilla distinta a INBOX usando el\n"
+" También le permite especificar una carpeta distinta a INBOX usando el\n"
 " formato:\n"
-"    imap nombreusuario:contraseña at servidor casilla\n"
+"    imap nombreusuario:contraseña at servidor carpeta\n"
 "\n"
 "IMAPS:\n"
 " Se conecta a un servidor IMAP usando ssl.\n"
 " Esta forma soporta la misma notación que IMAP.\n"
-"    imaps nombreusuario:contraseña at servidor [casilla]\n"
+"    imaps nombreusuario:contraseña at servidor [carpeta]\n"
 "\n"
 
-#: ../roundup/scripts/roundup_mailgw.py:147
+#: ../roundup/scripts/roundup_mailgw.py:151
 msgid "Error: not enough source specification information"
 msgstr "Error: no hay información de especificación de origen suficiente"
 
-#: ../roundup/scripts/roundup_mailgw.py:163
+#: ../roundup/scripts/roundup_mailgw.py:167
+msgid "Error: a later version of python is required"
+msgstr "Error: se require una versión mas reciente de python"
+
+#: ../roundup/scripts/roundup_mailgw.py:170
 msgid "Error: pop specification not valid"
 msgstr "Error: especification pop no válida"
 
-#: ../roundup/scripts/roundup_mailgw.py:170
+#: ../roundup/scripts/roundup_mailgw.py:177
 msgid "Error: apop specification not valid"
 msgstr "Error: especification apop no válida"
 
-#: ../roundup/scripts/roundup_mailgw.py:184
+#: ../roundup/scripts/roundup_mailgw.py:189
 msgid ""
 "Error: The source must be either \"mailbox\", \"pop\", \"apop\", \"imap\" or "
 "\"imaps\""
@@ -2218,7 +2329,11 @@
 "Error: EL origen debe ser \"mailbox\", \"pop\", \"apop\", \"imap\" o \"imaps"
 "\""
 
-#: ../roundup/scripts/roundup_server.py:157
+#: ../roundup/scripts/roundup_server.py:76
+msgid "WARNING: generating temporary SSL certificate"
+msgstr "ATENCION: generando certificado SLL temporario"
+
+#: ../roundup/scripts/roundup_server.py:253
 msgid ""
 "<html><head><title>Roundup trackers index</title></head>\n"
 "<body><h1>Roundup trackers index</h1><ol>\n"
@@ -2226,53 +2341,53 @@
 "<html><head><title>Índice de trackers Roundup</title></head>\n"
 "<body><h1>Índice de trackers Roundup</h1><ol>\n"
 
-#: ../roundup/scripts/roundup_server.py:287
+#: ../roundup/scripts/roundup_server.py:389
 #, python-format
 msgid "Error: %s: %s"
-msgstr ""
+msgstr "Error: %s: %s"
 
-#: ../roundup/scripts/roundup_server.py:297
+#: ../roundup/scripts/roundup_server.py:399
 msgid "WARNING: ignoring \"-g\" argument, not root"
 msgstr "ATENCIÓN: ignorando argumento \"-g\" , Ud. no es root"
 
-#: ../roundup/scripts/roundup_server.py:303
+#: ../roundup/scripts/roundup_server.py:405
 msgid "Can't change groups - no grp module"
 msgstr "No puede cambiar grupos - el módulo grp no está presente"
 
-#: ../roundup/scripts/roundup_server.py:312
+#: ../roundup/scripts/roundup_server.py:414
 #, python-format
 msgid "Group %(group)s doesn't exist"
 msgstr "El grupo %(group)s no existe"
 
-#: ../roundup/scripts/roundup_server.py:323
+#: ../roundup/scripts/roundup_server.py:425
 msgid "Can't run as root!"
 msgstr "No puede ejecutarse como root!"
 
-#: ../roundup/scripts/roundup_server.py:326
+#: ../roundup/scripts/roundup_server.py:428
 msgid "WARNING: ignoring \"-u\" argument, not root"
 msgstr "ATENCIÓN: ignorando argumento \"-u\", Ud. no es root"
 
-#: ../roundup/scripts/roundup_server.py:331
+#: ../roundup/scripts/roundup_server.py:434
 msgid "Can't change users - no pwd module"
 msgstr "No puedo cambiar usuarios - no existe el módulo pwd"
 
-#: ../roundup/scripts/roundup_server.py:340
+#: ../roundup/scripts/roundup_server.py:443
 #, python-format
 msgid "User %(user)s doesn't exist"
 msgstr "El usuario %(user)s no existe"
 
-#: ../roundup/scripts/roundup_server.py:471
+#: ../roundup/scripts/roundup_server.py:592
 #, python-format
 msgid "Multiprocess mode \"%s\" is not available, switching to single-process"
 msgstr ""
 "El modo multiproceso \"%s\" no está disponible, conmutado a proceso simple"
 
-#: ../roundup/scripts/roundup_server.py:494
+#: ../roundup/scripts/roundup_server.py:620
 #, python-format
 msgid "Unable to bind to port %s, port already in use."
 msgstr "Imposible asociarse al puerto %s, el mismo ya está en uso."
 
-#: ../roundup/scripts/roundup_server.py:562
+#: ../roundup/scripts/roundup_server.py:688
 msgid ""
 " -c <Command>  Windows Service options.\n"
 "               If you want to run the server as a Windows Service, you\n"
@@ -2284,17 +2399,17 @@
 " -c <Comando>  Opciones de Servicio Windows.\n"
 "               Si desdea ejecutar el servidor como un Servicio Windows, debe "
 "usar\n"
-"               un archivo de configuración para especificar los directorios "
+"               un fichero de configuración para especificar los directorios "
 "base\n"
 "               de los trackers.\n"
 "               Cuando ejecuta el Roundup Tracker como un servicio deb usar "
 "la\n"
-"               opción para activar un archivo de registro.\n"
+"               opción para activar un fichero de registro.\n"
 "               Tipee \"roundup-server -c help\" para ver ayuda específica "
 "para\n"
 "               Servicios Web."
 
-#: ../roundup/scripts/roundup_server.py:569
+#: ../roundup/scripts/roundup_server.py:695
 msgid ""
 " -u <UID>      runs the Roundup web server as this UID\n"
 " -g <GID>      runs the Roundup web server as this GID\n"
@@ -2306,10 +2421,10 @@
 " -g <GID>      ejecuta el servidor web de Roundup como este GID\n"
 " -d <PIDfile>  ejecuta el servidor web de Roundup en segundo plano y escribe "
 "el\n"
-"               PID del servidor en el archivo especificado por PIDfile.\n"
+"               PID del servidor en el fichero especificado por PIDfile.\n"
 "               La opción -l *debe* ser especificada si se usa la opción -d."
 
-#: ../roundup/scripts/roundup_server.py:576
+#: ../roundup/scripts/roundup_server.py:702
 #, python-format
 msgid ""
 "%(message)sUsage: roundup-server [options] [name=tracker home]*\n"
@@ -2324,6 +2439,9 @@
 " -l <fname>    log to the file indicated by fname instead of stderr/stdout\n"
 " -N            log client machine names instead of IP addresses (much "
 "slower)\n"
+" -i <fname>    set tracker index template\n"
+" -s            enable SSL\n"
+" -e <fname>    PEM file containing SSL key and certificate\n"
 " -t <mode>     multiprocess mode (default: %(mp_def)s).\n"
 "               Allowed values: %(mp_types)s.\n"
 "%(os_part)s\n"
@@ -2369,26 +2487,29 @@
 "Opciones:\n"
 " -v            imprime el número de versión de Roundup y sale\n"
 " -h            imprime este texto y sale\n"
-" -S            crea o actualiza el archivo de configuración y sale\n"
-" -C <fname>    usa el archivo de configuración <fname>\n"
+" -S            crea o actualiza el fichero de configuración y sale\n"
+" -C <fname>    usa el fichero de configuración <fname>\n"
 " -n <name>     especifica el nombre de host de la instancia del servidor web "
 "de Roundup\n"
 " -p <port>     especifica el puerto en el cual escuchará el servidor (por "
 "omisión: %(port)s)\n"
-" -l <fname>    almacena bitácora en el archivo indicado por fname en lugar "
+" -l <fname>    almacena bitácora en el fichero indicado por fname en lugar "
 "de hacerlo a stderr/stdout\n"
 " -N            almacena en bitácora los nombres de los equipos clientes en "
 "lugar de direcciones IP (mucho mas lento)\n"
-" -t <mode>     mod multiproceso (por omisión: %(mp_def)s).\n"
+" -i <fname>    especifica la plantilla del índice del tracker\n"
+" -s            activa SSL\n"
+" -e <fname>    fichero PEM que contiene la llave y el certificado SSL\n"
+" -t <mode>     modo multiproceso (por omisión: %(mp_def)s).\n"
 "               Valores permitidos: %(mp_types)s.\n"
 "%(os_part)s\n"
 "\n"
 "Opciones largas:\n"
 " --version          imprime el número de versión de Roundup y sale\n"
 " --help             imprime este texto y sale\n"
-" --save-config      crea o actualiza el archivo de configuración y sale\n"
-" --config <fname>   usa el archivo de configuración <fname>\n"
-" Todos las variables de la sección [main] del archivo de configuración\n"
+" --save-config      crea o actualiza el fichero de configuración y sale\n"
+" --config <fname>   usa el fichero de configuración <fname>\n"
+" Todos las variables de la sección [main] del fichero de configuración\n"
 " pueden también especificarse usando la forma --<nombre>=<valor>\n"
 "\n"
 "Ejemplos:\n"
@@ -2404,11 +2525,11 @@
 " roundup-server -d /var/run/roundup.pid -l /var/log/roundup.log \\\n"
 "    support=/var/spool/roundup-trackers/support\n"
 "\n"
-"Formato de archivo de configuración:\n"
-"   El archivo de configuración del Servidor Roundup tiene un formato de "
-"archivo.ini común.\n"
-"   El archivo de configuración creado con 'roundup-server -S' contiene\n"
-"   explicaciones detalladas para cada opción. Por favor vea dicho archivo "
+"Formato de fichero de configuración:\n"
+"   El fichero de configuración del Servidor Roundup tiene un formato de "
+"fichero.ini común.\n"
+"   El fichero de configuración creado con 'roundup-server -S' contiene\n"
+"   explicaciones detalladas para cada opción. Por favor vea dicho fichero "
 "para encontrar\n"
 "   descripciones de las variables.\n"
 "\n"
@@ -2427,22 +2548,22 @@
 "   caracteres tales como espacios, dado que los mismos confunden a Internet "
 "Explorer.\n"
 
-#: ../roundup/scripts/roundup_server.py:723
+#: ../roundup/scripts/roundup_server.py:860
 msgid "Instances must be name=home"
 msgstr "Las Instancias debe ser de la forma nombre=directorio base"
 
-#: ../roundup/scripts/roundup_server.py:737
+#: ../roundup/scripts/roundup_server.py:874
 #, python-format
 msgid "Configuration saved to %s"
 msgstr "Configuración guardada en %s"
 
-#: ../roundup/scripts/roundup_server.py:755
+#: ../roundup/scripts/roundup_server.py:892
 msgid "Sorry, you can't run the server as a daemon on this Operating System"
 msgstr ""
 "Lo siento, no puede ejecutar el servidor como un demonio en este Sistema "
 "Operativo"
 
-#: ../roundup/scripts/roundup_server.py:767
+#: ../roundup/scripts/roundup_server.py:907
 #, python-format
 msgid "Roundup server started on %(HOST)s:%(PORT)s"
 msgstr "servidor Roundup iniciado en %(HOST)s:%(PORT)s"
@@ -2470,35 +2591,74 @@
 "  mientras Ud. lo editaba. Por favor <a href='${context}'>revisualice</a>\n"
 "  el nodo y revise sus modificaciones.\n"
 
-#: ../templates/classic/html/_generic.help.html:9
-#: ../templates/minimal/html/_generic.help.html:9
-msgid "${property} help - ${tracker}"
-msgstr "${property} ayuda - ${tracker}"
+#: ../templates/classic/html/_generic.help-empty.html:6
+msgid "Please specify your search parameters!"
+msgstr "¡Por favor especifique sus parámetros de búsqueda!"
 
+#: ../templates/classic/html/_generic.help-list.html:20
+#: ../templates/classic/html/_generic.index.html:14
+#: ../templates/classic/html/_generic.item.html:12
+#: ../templates/classic/html/file.item.html:9
+#: ../templates/classic/html/issue.index.html:16
+#: ../templates/classic/html/issue.item.html:28
+#: ../templates/classic/html/msg.item.html:26
+#: ../templates/classic/html/user.index.html:9
+#: ../templates/classic/html/user.item.html:35
+#: ../templates/minimal/html/_generic.index.html:14
+#: ../templates/minimal/html/_generic.item.html:12
+#: ../templates/minimal/html/user.index.html:9
+#: ../templates/minimal/html/user.item.html:35
+#: ../templates/minimal/html/user.register.html:14
+msgid "You are not allowed to view this page."
+msgstr "Ud. no posee los permisos necesarios para ver esta página."
+
+#: ../templates/classic/html/_generic.help-list.html:34
+msgid "1..25 out of 50"
+msgstr "1..25 de 50"
+
+#: ../templates/classic/html/_generic.help-search.html:9
+msgid ""
+"Generic template ${template} or version for class ${classname} is not yet "
+"implemented"
+msgstr ""
+"Aun no están implementadas una plantilla genérica ${template} o una "
+"version para la clase ${classname}"
+
+#: ../templates/classic/html/_generic.help-submit.html:57
 #: ../templates/classic/html/_generic.help.html:31
 #: ../templates/minimal/html/_generic.help.html:31
 msgid " Cancel "
 msgstr " Cancelar "
 
+#: ../templates/classic/html/_generic.help-submit.html:63
 #: ../templates/classic/html/_generic.help.html:34
 #: ../templates/minimal/html/_generic.help.html:34
 msgid " Apply "
 msgstr " Aplicar "
 
+#: ../templates/classic/html/_generic.help.html:9
+#: ../templates/classic/html/user.help.html:13
+#: ../templates/minimal/html/_generic.help.html:9
+msgid "${property} help - ${tracker}"
+msgstr "${property} ayuda - ${tracker}"
+
 #: ../templates/classic/html/_generic.help.html:41
-#: ../templates/classic/html/issue.index.html:73
+#: ../templates/classic/html/help.html:21
+#: ../templates/classic/html/issue.index.html:80
 #: ../templates/minimal/html/_generic.help.html:41
 msgid "&lt;&lt; previous"
 msgstr "&lt;&lt; anterior"
 
 #: ../templates/classic/html/_generic.help.html:53
-#: ../templates/classic/html/issue.index.html:81
+#: ../templates/classic/html/help.html:28
+#: ../templates/classic/html/issue.index.html:88
 #: ../templates/minimal/html/_generic.help.html:53
 msgid "${start}..${end} out of ${total}"
 msgstr "${start}..${end} de un total de ${total}"
 
 #: ../templates/classic/html/_generic.help.html:57
-#: ../templates/classic/html/issue.index.html:84
+#: ../templates/classic/html/help.html:32
+#: ../templates/classic/html/issue.index.html:91
 #: ../templates/minimal/html/_generic.help.html:57
 msgid "next &gt;&gt;"
 msgstr "próxima &gt;&gt;"
@@ -2517,24 +2677,24 @@
 msgid "${class} editing"
 msgstr "Edición de ${class}"
 
-#: ../templates/classic/html/_generic.index.html:14
-#: ../templates/classic/html/_generic.item.html:12
-#: ../templates/classic/html/file.item.html:9
-#: ../templates/classic/html/issue.index.html:16
-#: ../templates/classic/html/issue.item.html:28
-#: ../templates/classic/html/msg.item.html:26
-#: ../templates/classic/html/user.index.html:9
-#: ../templates/classic/html/user.item.html:28
-#: ../templates/minimal/html/_generic.index.html:14
-#: ../templates/minimal/html/_generic.item.html:12
-#: ../templates/minimal/html/user.index.html:9
-#: ../templates/minimal/html/user.item.html:28
-#: ../templates/minimal/html/user.register.html:14
-msgid "You are not allowed to view this page."
-msgstr "Ud. no posee los permisos necesarios para ver esta página."
+#: ../templates/classic/html/_generic.index.html:19
+#: ../templates/classic/html/_generic.item.html:16
+#: ../templates/classic/html/file.item.html:13
+#: ../templates/classic/html/issue.index.html:20
+#: ../templates/classic/html/issue.item.html:32
+#: ../templates/classic/html/msg.item.html:30
+#: ../templates/classic/html/user.index.html:13
+#: ../templates/classic/html/user.item.html:39
+#: ../templates/minimal/html/_generic.index.html:19
+#: ../templates/minimal/html/_generic.item.html:17
+#: ../templates/minimal/html/user.index.html:13
+#: ../templates/minimal/html/user.item.html:39
+#: ../templates/minimal/html/user.register.html:17
+msgid "Please login with your username and password."
+msgstr "Por favor identifíquese con su mombre de usuario y contraseña."
 
-#: ../templates/classic/html/_generic.index.html:22
-#: ../templates/minimal/html/_generic.index.html:22
+#: ../templates/classic/html/_generic.index.html:28
+#: ../templates/minimal/html/_generic.index.html:28
 msgid ""
 "<p class=\"form-help\"> You may edit the contents of the ${classname} class "
 "using this form. Commas, newlines and double quotes (\") must be handled "
@@ -2555,25 +2715,25 @@
 "Para eliminar elementos elimine la línea correspondiente. Para agregar "
 "nuevos elementos anéxelos a la tabla y coloque una X en la columna id. </p>"
 
-#: ../templates/classic/html/_generic.index.html:44
-#: ../templates/minimal/html/_generic.index.html:44
+#: ../templates/classic/html/_generic.index.html:50
+#: ../templates/minimal/html/_generic.index.html:50
 msgid "Edit Items"
 msgstr "Editar Items"
 
 #: ../templates/classic/html/file.index.html:4
 msgid "List of files - ${tracker}"
-msgstr "Lista de archivos - ${tracker}"
+msgstr "Lista de ficheros - ${tracker}"
 
 #: ../templates/classic/html/file.index.html:5
 msgid "List of files"
-msgstr "Lista de archivos"
+msgstr "Lista de ficheros"
 
 #: ../templates/classic/html/file.index.html:10
 msgid "Download"
 msgstr "Descargar"
 
 #: ../templates/classic/html/file.index.html:11
-#: ../templates/classic/html/file.item.html:22
+#: ../templates/classic/html/file.item.html:27
 msgid "Content Type"
 msgstr "Tipo de Contenido"
 
@@ -2582,25 +2742,24 @@
 msgstr "Subido por"
 
 #: ../templates/classic/html/file.index.html:13
-#: ../templates/classic/html/msg.item.html:43
+#: ../templates/classic/html/msg.item.html:48
 msgid "Date"
 msgstr "Fecha"
 
 #: ../templates/classic/html/file.item.html:2
 msgid "File display - ${tracker}"
-msgstr "Visualización de archivos - ${tracker}"
+msgstr "Visualización de ficheros - ${tracker}"
 
 #: ../templates/classic/html/file.item.html:4
 msgid "File display"
-msgstr "Visualización de archivos"
+msgstr "Visualización de ficheros"
 
-#: ../templates/classic/html/file.item.html:18
-#: ../templates/classic/html/user.item.html:39
+#: ../templates/classic/html/file.item.html:23
 #: ../templates/classic/html/user.register.html:17
 msgid "Name"
 msgstr "Nombre"
 
-#: ../templates/classic/html/file.item.html:40
+#: ../templates/classic/html/file.item.html:45
 msgid "download"
 msgstr "descargar"
 
@@ -2614,80 +2773,78 @@
 msgid "List of classes"
 msgstr "Lista de clases"
 
-#: ../templates/classic/html/issue.index.html:7
-msgid "List of issues - ${tracker}"
-msgstr "Lista de issues - ${tracker}"
-
-#: ../templates/classic/html/issue.index.html:11
+#: ../templates/classic/html/issue.index.html:4
+#: ../templates/classic/html/issue.index.html:10
 msgid "List of issues"
 msgstr "Lista de issues"
 
-#: ../templates/classic/html/issue.index.html:22
-#: ../templates/classic/html/issue.item.html:44
+#: ../templates/classic/html/issue.index.html:27
+#: ../templates/classic/html/issue.item.html:49
 msgid "Priority"
 msgstr "Prioridad"
 
-#: ../templates/classic/html/issue.index.html:23
+#: ../templates/classic/html/issue.index.html:28
 msgid "ID"
-msgstr ""
+msgstr "ID"
 
-#: ../templates/classic/html/issue.index.html:24
+#: ../templates/classic/html/issue.index.html:29
 msgid "Creation"
 msgstr "Creación"
 
-#: ../templates/classic/html/issue.index.html:25
+#: ../templates/classic/html/issue.index.html:30
 msgid "Activity"
 msgstr "Actividad"
 
-#: ../templates/classic/html/issue.index.html:26
+#: ../templates/classic/html/issue.index.html:31
 msgid "Actor"
 msgstr "último actor"
 
-#: ../templates/classic/html/issue.index.html:27
-msgid "Topic"
+#: ../templates/classic/html/issue.index.html:32
+#: ../templates/classic/html/keyword.item.html:37
+msgid "Keyword"
 msgstr "Palabra clave"
 
-#: ../templates/classic/html/issue.index.html:28
-#: ../templates/classic/html/issue.item.html:39
+#: ../templates/classic/html/issue.index.html:33
+#: ../templates/classic/html/issue.item.html:44
 msgid "Title"
 msgstr "Título"
 
-#: ../templates/classic/html/issue.index.html:29
-#: ../templates/classic/html/issue.item.html:46
+#: ../templates/classic/html/issue.index.html:34
+#: ../templates/classic/html/issue.item.html:51
 msgid "Status"
 msgstr "Estado"
 
-#: ../templates/classic/html/issue.index.html:30
+#: ../templates/classic/html/issue.index.html:35
 msgid "Creator"
 msgstr "Creador"
 
-#: ../templates/classic/html/issue.index.html:31
+#: ../templates/classic/html/issue.index.html:36
 msgid "Assigned&nbsp;To"
 msgstr "Asignado&nbsp;a"
 
-#: ../templates/classic/html/issue.index.html:97
+#: ../templates/classic/html/issue.index.html:104
 msgid "Download as CSV"
 msgstr "Descargar como CSV"
 
-#: ../templates/classic/html/issue.index.html:105
+#: ../templates/classic/html/issue.index.html:114
 msgid "Sort on:"
 msgstr "Ordenar por:"
 
-#: ../templates/classic/html/issue.index.html:108
-#: ../templates/classic/html/issue.index.html:125
+#: ../templates/classic/html/issue.index.html:118
+#: ../templates/classic/html/issue.index.html:139
 msgid "- nothing -"
 msgstr "- nada -"
 
-#: ../templates/classic/html/issue.index.html:116
-#: ../templates/classic/html/issue.index.html:133
+#: ../templates/classic/html/issue.index.html:126
+#: ../templates/classic/html/issue.index.html:147
 msgid "Descending:"
 msgstr "Descendente:"
 
-#: ../templates/classic/html/issue.index.html:122
+#: ../templates/classic/html/issue.index.html:135
 msgid "Group on:"
 msgstr "Agrupar por:"
 
-#: ../templates/classic/html/issue.index.html:139
+#: ../templates/classic/html/issue.index.html:154
 msgid "Redisplay"
 msgstr "Revisualizar"
 
@@ -2709,48 +2866,50 @@
 
 #: ../templates/classic/html/issue.item.html:19
 msgid "Issue${id}"
-msgstr ""
+msgstr "Issue${id}"
 
 #: ../templates/classic/html/issue.item.html:22
 msgid "Issue${id} Editing"
 msgstr "Edición de Issue${id}"
 
-#: ../templates/classic/html/issue.item.html:51
+#: ../templates/classic/html/issue.item.html:56
 msgid "Superseder"
 msgstr "Reemplazado por"
 
-#: ../templates/classic/html/issue.item.html:56
-msgid "View: ${link}"
-msgstr "Ver: ${link}"
+#: ../templates/classic/html/issue.item.html:61
+msgid "View:"
+msgstr "Ver:"
 
-#: ../templates/classic/html/issue.item.html:60
+#: ../templates/classic/html/issue.item.html:67
 msgid "Nosy List"
 msgstr "Lista de interesados"
 
-#: ../templates/classic/html/issue.item.html:69
+#: ../templates/classic/html/issue.item.html:76
 msgid "Assigned To"
 msgstr "Asignado a"
 
-#: ../templates/classic/html/issue.item.html:71
-msgid "Topics"
+#: ../templates/classic/html/issue.item.html:78
+#: ../templates/classic/html/page.html:103
+#: ../templates/minimal/html/page.html:102
+msgid "Keywords"
 msgstr "Palabras clave"
 
-#: ../templates/classic/html/issue.item.html:79
+#: ../templates/classic/html/issue.item.html:86
 msgid "Change Note"
 msgstr "Nota de modificación"
 
-#: ../templates/classic/html/issue.item.html:87
+#: ../templates/classic/html/issue.item.html:94
 msgid "File"
-msgstr "Archivo"
+msgstr "Fichero"
 
-#: ../templates/classic/html/issue.item.html:99
+#: ../templates/classic/html/issue.item.html:106
 msgid "Make a copy"
 msgstr "Hacer una copia"
 
-#: ../templates/classic/html/issue.item.html:107
-#: ../templates/classic/html/user.item.html:106
+#: ../templates/classic/html/issue.item.html:114
+#: ../templates/classic/html/user.item.html:153
 #: ../templates/classic/html/user.register.html:69
-#: ../templates/minimal/html/user.item.html:86
+#: ../templates/minimal/html/user.item.html:153
 msgid ""
 "<table class=\"form\"> <tr> <td>Note:&nbsp;</td> <th class=\"required"
 "\">highlighted</th> <td>&nbsp;fields are required.</td> </tr> </table>"
@@ -2758,7 +2917,7 @@
 "<table class=\"form\"> <tr> <td>Nota: Los campos&nbsp;</td> <th class="
 "\"required\">resaltados</th> <td>&nbsp;son obligatorios.</td> </tr> </table>"
 
-#: ../templates/classic/html/issue.item.html:121
+#: ../templates/classic/html/issue.item.html:128
 msgid ""
 "Created on <b>${creation}</b> by <b>${creator}</b>, last changed <b>"
 "${activity}</b> by <b>${actor}</b>."
@@ -2766,54 +2925,54 @@
 "Creado el <b>${creation}</b> por <b>${creator}</b>, última modificación el "
 "<b>${activity}</b> por <b>${actor}</b>."
 
-#: ../templates/classic/html/issue.item.html:125
-#: ../templates/classic/html/msg.item.html:56
+#: ../templates/classic/html/issue.item.html:132
+#: ../templates/classic/html/msg.item.html:61
 msgid "Files"
-msgstr "Archivos"
+msgstr "Ficheros"
 
-#: ../templates/classic/html/issue.item.html:127
-#: ../templates/classic/html/msg.item.html:58
+#: ../templates/classic/html/issue.item.html:134
+#: ../templates/classic/html/msg.item.html:63
 msgid "File name"
-msgstr "Nombre de archivo"
+msgstr "Nombre de fichero"
 
-#: ../templates/classic/html/issue.item.html:128
-#: ../templates/classic/html/msg.item.html:59
+#: ../templates/classic/html/issue.item.html:135
+#: ../templates/classic/html/msg.item.html:64
 msgid "Uploaded"
 msgstr "Subido"
 
-#: ../templates/classic/html/issue.item.html:129
+#: ../templates/classic/html/issue.item.html:136
 msgid "Type"
 msgstr "Tipo"
 
-#: ../templates/classic/html/issue.item.html:130
+#: ../templates/classic/html/issue.item.html:137
 #: ../templates/classic/html/query.edit.html:30
 msgid "Edit"
 msgstr "Editar"
 
-#: ../templates/classic/html/issue.item.html:131
+#: ../templates/classic/html/issue.item.html:138
 msgid "Remove"
 msgstr "Eliminar"
 
-#: ../templates/classic/html/issue.item.html:151
-#: ../templates/classic/html/issue.item.html:172
+#: ../templates/classic/html/issue.item.html:158
+#: ../templates/classic/html/issue.item.html:179
 #: ../templates/classic/html/query.edit.html:50
 msgid "remove"
 msgstr "eliminar"
 
-#: ../templates/classic/html/issue.item.html:158
+#: ../templates/classic/html/issue.item.html:165
 #: ../templates/classic/html/msg.index.html:9
 msgid "Messages"
 msgstr "Mensajes"
 
-#: ../templates/classic/html/issue.item.html:162
+#: ../templates/classic/html/issue.item.html:169
 msgid "msg${id} (view)"
 msgstr "mensaje${id} (ver)"
 
-#: ../templates/classic/html/issue.item.html:163
+#: ../templates/classic/html/issue.item.html:170
 msgid "Author: ${author}"
 msgstr "Autor: ${author}"
 
-#: ../templates/classic/html/issue.item.html:165
+#: ../templates/classic/html/issue.item.html:172
 msgid "Date: ${date}"
 msgstr "Fecha: ${date}"
 
@@ -2825,129 +2984,132 @@
 msgid "Issue searching"
 msgstr "Búsqueda de Issues"
 
-#: ../templates/classic/html/issue.search.html:25
+#: ../templates/classic/html/issue.search.html:31
 msgid "Filter on"
 msgstr "Filtrar por"
 
-#: ../templates/classic/html/issue.search.html:26
+#: ../templates/classic/html/issue.search.html:32
 msgid "Display"
 msgstr "Visualizar"
 
-#: ../templates/classic/html/issue.search.html:27
+#: ../templates/classic/html/issue.search.html:33
 msgid "Sort on"
 msgstr "Ordenar por"
 
-#: ../templates/classic/html/issue.search.html:28
+#: ../templates/classic/html/issue.search.html:34
 msgid "Group on"
 msgstr "Agrupar por"
 
-#: ../templates/classic/html/issue.search.html:32
+#: ../templates/classic/html/issue.search.html:38
 msgid "All text*:"
 msgstr "Todo el texto*:"
 
-#: ../templates/classic/html/issue.search.html:40
+#: ../templates/classic/html/issue.search.html:46
 msgid "Title:"
 msgstr "Título:"
 
-#: ../templates/classic/html/issue.search.html:50
-msgid "Topic:"
+#: ../templates/classic/html/issue.search.html:56
+msgid "Keyword:"
 msgstr "Palabra clave:"
 
 #: ../templates/classic/html/issue.search.html:58
+#: ../templates/classic/html/issue.search.html:123
+#: ../templates/classic/html/issue.search.html:139
+msgid "not selected"
+msgstr "no seleccionado"
+
+#: ../templates/classic/html/issue.search.html:67
 msgid "ID:"
-msgstr ""
+msgstr "ID:"
 
-#: ../templates/classic/html/issue.search.html:66
+#: ../templates/classic/html/issue.search.html:75
 msgid "Creation Date:"
 msgstr "Fecha de creación:"
 
-#: ../templates/classic/html/issue.search.html:77
+#: ../templates/classic/html/issue.search.html:86
 msgid "Creator:"
 msgstr "Creador:"
 
-#: ../templates/classic/html/issue.search.html:79
+#: ../templates/classic/html/issue.search.html:88
 msgid "created by me"
 msgstr "creado por mí"
 
-#: ../templates/classic/html/issue.search.html:88
+#: ../templates/classic/html/issue.search.html:97
 msgid "Activity:"
 msgstr "Actividad:"
 
-#: ../templates/classic/html/issue.search.html:99
+#: ../templates/classic/html/issue.search.html:108
 msgid "Actor:"
 msgstr "Último actor:"
 
-#: ../templates/classic/html/issue.search.html:101
+#: ../templates/classic/html/issue.search.html:110
 msgid "done by me"
 msgstr "hecho por mí"
 
-#: ../templates/classic/html/issue.search.html:112
+#: ../templates/classic/html/issue.search.html:121
 msgid "Priority:"
 msgstr "Prioridad:"
 
-#: ../templates/classic/html/issue.search.html:114
-#: ../templates/classic/html/issue.search.html:130
-msgid "not selected"
-msgstr "no seleccionado"
-
-#: ../templates/classic/html/issue.search.html:125
+#: ../templates/classic/html/issue.search.html:134
 msgid "Status:"
 msgstr "Estado:"
 
-#: ../templates/classic/html/issue.search.html:128
+#: ../templates/classic/html/issue.search.html:137
 msgid "not resolved"
 msgstr "sin resolver"
 
-#: ../templates/classic/html/issue.search.html:143
+#: ../templates/classic/html/issue.search.html:152
 msgid "Assigned to:"
 msgstr "Asignado a:"
 
-#: ../templates/classic/html/issue.search.html:146
+#: ../templates/classic/html/issue.search.html:155
 msgid "assigned to me"
 msgstr "asignado a mí"
 
-#: ../templates/classic/html/issue.search.html:148
+#: ../templates/classic/html/issue.search.html:157
 msgid "unassigned"
 msgstr "no asignado"
 
-#: ../templates/classic/html/issue.search.html:158
+#: ../templates/classic/html/issue.search.html:167
 msgid "No Sort or group:"
 msgstr "No ordenar o agrupar"
 
-#: ../templates/classic/html/issue.search.html:166
+#: ../templates/classic/html/issue.search.html:175
 msgid "Pagesize:"
 msgstr "Tamaño de página"
 
-#: ../templates/classic/html/issue.search.html:172
+#: ../templates/classic/html/issue.search.html:181
 msgid "Start With:"
 msgstr "Comenzar con:"
 
-#: ../templates/classic/html/issue.search.html:178
+#: ../templates/classic/html/issue.search.html:187
 msgid "Sort Descending:"
 msgstr "Ordenar en forma descendente:"
 
-#: ../templates/classic/html/issue.search.html:185
+#: ../templates/classic/html/issue.search.html:194
 msgid "Group Descending:"
 msgstr "Agrupar en forma descendente:"
 
-#: ../templates/classic/html/issue.search.html:192
+#: ../templates/classic/html/issue.search.html:201
 msgid "Query name**:"
 msgstr "Nombre de la consulta**:"
 
-#: ../templates/classic/html/issue.search.html:204
-#: ../templates/classic/html/page.html:31
-#: ../templates/classic/html/page.html:60
-#: ../templates/minimal/html/page.html:31
+#: ../templates/classic/html/issue.search.html:213
+#: ../templates/classic/html/page.html:43
+#: ../templates/classic/html/page.html:92
+#: ../templates/classic/html/user.help-search.html:69
+#: ../templates/minimal/html/page.html:43
+#: ../templates/minimal/html/page.html:91
 msgid "Search"
 msgstr "Buscar"
 
-#: ../templates/classic/html/issue.search.html:209
+#: ../templates/classic/html/issue.search.html:218
 msgid "*: The \"all text\" field will look in message bodies and issue titles"
 msgstr ""
 "*: El campo \"Todo el texto\" busca en los cuerpos de los mensajes y los "
 "títulos de los issues"
 
-#: ../templates/classic/html/issue.search.html:212
+#: ../templates/classic/html/issue.search.html:221
 msgid ""
 "**: If you supply a name, the query will be saved off and available as a "
 "link in the sidebar"
@@ -2981,10 +3143,6 @@
 "Para crear una nueva Palabra clave, ingrese la misma abajo y haga click en "
 "\"Crear nuevo elemento\"."
 
-#: ../templates/classic/html/keyword.item.html:37
-msgid "Keyword"
-msgstr "Palabra clave"
-
 #: ../templates/classic/html/msg.index.html:3
 msgid "List of messages - ${tracker}"
 msgstr "Lista de mensajes - ${tracker}"
@@ -3017,133 +3175,153 @@
 msgid "Message${id} Editing"
 msgstr "Edición de Mensaje${id}"
 
-#: ../templates/classic/html/msg.item.html:33
+#: ../templates/classic/html/msg.item.html:38
 msgid "Author"
 msgstr "Autor"
 
-#: ../templates/classic/html/msg.item.html:38
+#: ../templates/classic/html/msg.item.html:43
 msgid "Recipients"
 msgstr "Destinatarios"
 
-#: ../templates/classic/html/msg.item.html:49
+#: ../templates/classic/html/msg.item.html:54
 msgid "Content"
 msgstr "Contenido"
 
-#: ../templates/classic/html/page.html:41
+#: ../templates/classic/html/page.html:54
+#: ../templates/minimal/html/page.html:53
 msgid "<b>Your Queries</b> (<a href=\"query?@template=edit\">edit</a>)"
 msgstr "<b>Sus consultas</b> (<a href=\"query?@template=edit\">editar</a>)"
 
-#: ../templates/classic/html/page.html:52
+#: ../templates/classic/html/page.html:65
+#: ../templates/minimal/html/page.html:64
 msgid "Issues"
-msgstr ""
+msgstr "Issues"
 
-#: ../templates/classic/html/page.html:54
-#: ../templates/classic/html/page.html:74
+#: ../templates/classic/html/page.html:67
+#: ../templates/classic/html/page.html:105
+#: ../templates/minimal/html/page.html:66
+#: ../templates/minimal/html/page.html:104
 msgid "Create New"
 msgstr "Crear"
 
-#: ../templates/classic/html/page.html:56
+#: ../templates/classic/html/page.html:69
+#: ../templates/minimal/html/page.html:68
 msgid "Show Unassigned"
 msgstr "Mostrar no asignados"
 
-#: ../templates/classic/html/page.html:58
+#: ../templates/classic/html/page.html:81
+#: ../templates/minimal/html/page.html:80
 msgid "Show All"
 msgstr "Mostrar todos"
 
-#: ../templates/classic/html/page.html:61
+#: ../templates/classic/html/page.html:93
+#: ../templates/minimal/html/page.html:92
 msgid "Show issue:"
 msgstr "Mostrar issue:"
 
-#: ../templates/classic/html/page.html:72
-msgid "Keywords"
-msgstr "Palabras clave"
-
-#: ../templates/classic/html/page.html:78
+#: ../templates/classic/html/page.html:108
+#: ../templates/minimal/html/page.html:107
 msgid "Edit Existing"
 msgstr "Editar existentes"
 
-#: ../templates/classic/html/page.html:84
-#: ../templates/minimal/html/page.html:65
+#: ../templates/classic/html/page.html:114
+#: ../templates/minimal/html/page.html:113
 msgid "Administration"
 msgstr "Administración"
 
-#: ../templates/classic/html/page.html:86
-#: ../templates/minimal/html/page.html:66
+#: ../templates/classic/html/page.html:116
+#: ../templates/minimal/html/page.html:115
 msgid "Class List"
 msgstr "Lista de clases"
 
-#: ../templates/classic/html/page.html:90
-#: ../templates/minimal/html/page.html:68
+#: ../templates/classic/html/page.html:120
+#: ../templates/minimal/html/page.html:119
 msgid "User List"
 msgstr "Lista de usuarios"
 
-#: ../templates/classic/html/page.html:92
-#: ../templates/minimal/html/page.html:71
+#: ../templates/classic/html/page.html:122
+#: ../templates/minimal/html/page.html:121
 msgid "Add User"
 msgstr "Agregar usuario"
 
-#: ../templates/classic/html/page.html:99
-#: ../templates/classic/html/page.html:105
-#: ../templates/minimal/html/page.html:46
+#: ../templates/classic/html/page.html:129
+#: ../templates/classic/html/page.html:135
+#: ../templates/minimal/html/page.html:128
+#: ../templates/minimal/html/page.html:134
 msgid "Login"
 msgstr "Ingresar"
 
-#: ../templates/classic/html/page.html:104
-#: ../templates/minimal/html/page.html:45
+#: ../templates/classic/html/page.html:134
+#: ../templates/minimal/html/page.html:133
 msgid "Remember me?"
 msgstr "Recordarme?"
 
-#: ../templates/classic/html/page.html:108
+#: ../templates/classic/html/page.html:138
 #: ../templates/classic/html/user.register.html:63
-#: ../templates/minimal/html/page.html:50
-#: ../templates/minimal/html/user.register.html:58
+#: ../templates/minimal/html/page.html:137
+#: ../templates/minimal/html/user.register.html:61
 msgid "Register"
 msgstr "Registrarse"
 
-#: ../templates/classic/html/page.html:111
+#: ../templates/classic/html/page.html:141
+#: ../templates/minimal/html/page.html:140
 msgid "Lost&nbsp;your&nbsp;login?"
 msgstr "Olvidó&nbsp;su&nbsp;contraseña?"
 
-#: ../templates/classic/html/page.html:116
+#: ../templates/classic/html/page.html:146
+#: ../templates/minimal/html/page.html:145
 msgid "Hello, ${user}"
 msgstr "Hola, ${user}"
 
-#: ../templates/classic/html/page.html:118
+#: ../templates/classic/html/page.html:148
 msgid "Your Issues"
 msgstr "Sus Issues"
 
-#: ../templates/classic/html/page.html:119
-#: ../templates/minimal/html/page.html:57
+#: ../templates/classic/html/page.html:160
+#: ../templates/minimal/html/page.html:147
 msgid "Your Details"
 msgstr "Sus datos personales"
 
-#: ../templates/classic/html/page.html:121
-#: ../templates/minimal/html/page.html:59
+#: ../templates/classic/html/page.html:162
+#: ../templates/minimal/html/page.html:149
 msgid "Logout"
 msgstr "Salir"
 
-#: ../templates/classic/html/page.html:125
+#: ../templates/classic/html/page.html:166
+#: ../templates/minimal/html/page.html:153
 msgid "Help"
 msgstr "Ayuda"
 
-#: ../templates/classic/html/page.html:126
+#: ../templates/classic/html/page.html:167
+#: ../templates/minimal/html/page.html:154
 msgid "Roundup docs"
 msgstr "Doc. de Roundup"
 
-#: ../templates/classic/html/page.html:136
-#: ../templates/minimal/html/page.html:81
+#: ../templates/classic/html/page.html:177
+#: ../templates/minimal/html/page.html:164
 msgid "clear this message"
 msgstr "quitar este mensaje"
 
-#: ../templates/classic/html/page.html:181
+#: ../templates/classic/html/page.html:241
+#: ../templates/classic/html/page.html:256
+#: ../templates/classic/html/page.html:270
+#: ../templates/minimal/html/page.html:228
+#: ../templates/minimal/html/page.html:243
+#: ../templates/minimal/html/page.html:257
 msgid "don't care"
 msgstr "cualquier(a)"
 
-#: ../templates/classic/html/page.html:183
+#: ../templates/classic/html/page.html:243
+#: ../templates/classic/html/page.html:258
+#: ../templates/classic/html/page.html:271
+#: ../templates/minimal/html/page.html:230
+#: ../templates/minimal/html/page.html:245
+#: ../templates/minimal/html/page.html:258
 msgid "------------"
-msgstr ""
+msgstr "------------"
 
-#: ../templates/classic/html/page.html:210
+#: ../templates/classic/html/page.html:299
+#: ../templates/minimal/html/page.html:286
 msgid "no value"
 msgstr "sin valor"
 
@@ -3254,6 +3432,18 @@
 "detalladas en el mismo para completar el proceso de generación de nueva una "
 "contraseña."
 
+#: ../templates/classic/html/user.help-search.html:73
+msgid "Pagesize"
+msgstr "Tamaño de página"
+
+#: ../templates/classic/html/user.help.html:43
+msgid ""
+"Your browser is not capable of using frames; you should be redirected "
+"immediately, or visit ${link}."
+msgstr ""
+"Su navegador no tiene capacidad de manejar marcos; debería ser "
+"redireccionado de inmediato, caso contrario vaya a ${link}."
+
 #: ../templates/classic/html/user.index.html:3
 #: ../templates/minimal/html/user.index.html:3
 msgid "User listing - ${tracker}"
@@ -3264,129 +3454,92 @@
 msgid "User listing"
 msgstr "Listado de usuarios"
 
-#: ../templates/classic/html/user.index.html:14
-#: ../templates/minimal/html/user.index.html:14
+#: ../templates/classic/html/user.index.html:19
+#: ../templates/minimal/html/user.index.html:19
 msgid "Username"
 msgstr "Nombre de usuario"
 
-#: ../templates/classic/html/user.index.html:15
+#: ../templates/classic/html/user.index.html:20
 msgid "Real name"
 msgstr "Nombre real"
 
-#: ../templates/classic/html/user.index.html:16
-#: ../templates/classic/html/user.item.html:70
+#: ../templates/classic/html/user.index.html:21
 #: ../templates/classic/html/user.register.html:45
 msgid "Organisation"
 msgstr "Organización"
 
-#: ../templates/classic/html/user.index.html:17
-#: ../templates/minimal/html/user.index.html:15
+#: ../templates/classic/html/user.index.html:22
+#: ../templates/minimal/html/user.index.html:20
 msgid "Email address"
 msgstr "Dirección de e-mail"
 
-#: ../templates/classic/html/user.index.html:18
+#: ../templates/classic/html/user.index.html:23
 msgid "Phone number"
 msgstr "Nro. telefónico"
 
-#: ../templates/classic/html/user.index.html:19
+#: ../templates/classic/html/user.index.html:24
 msgid "Retire"
 msgstr "Retirar"
 
-#: ../templates/classic/html/user.index.html:32
+#: ../templates/classic/html/user.index.html:37
 msgid "retire"
 msgstr "retirar"
 
-#: ../templates/classic/html/user.item.html:7
-#: ../templates/minimal/html/user.item.html:7
+#: ../templates/classic/html/user.item.html:9
+#: ../templates/minimal/html/user.item.html:9
 msgid "User ${id}: ${title} - ${tracker}"
 msgstr "Usuario ${id}: ${title} - ${tracker}"
 
-#: ../templates/classic/html/user.item.html:10
-#: ../templates/minimal/html/user.item.html:10
+#: ../templates/classic/html/user.item.html:12
+#: ../templates/minimal/html/user.item.html:12
 msgid "New User - ${tracker}"
 msgstr "Nuevo usuario - ${tracker}"
 
-#: ../templates/classic/html/user.item.html:14
-#: ../templates/minimal/html/user.item.html:14
+#: ../templates/classic/html/user.item.html:21
+#: ../templates/minimal/html/user.item.html:21
 msgid "New User"
 msgstr "Nuevo usuario"
 
-#: ../templates/classic/html/user.item.html:16
-#: ../templates/minimal/html/user.item.html:16
+#: ../templates/classic/html/user.item.html:23
+#: ../templates/minimal/html/user.item.html:23
 msgid "New User Editing"
 msgstr "Edición de nuevo usuario"
 
-#: ../templates/classic/html/user.item.html:19
-#: ../templates/minimal/html/user.item.html:19
+#: ../templates/classic/html/user.item.html:26
+#: ../templates/minimal/html/user.item.html:26
 msgid "User${id}"
 msgstr "Usuario${id}"
 
-#: ../templates/classic/html/user.item.html:22
-#: ../templates/minimal/html/user.item.html:22
+#: ../templates/classic/html/user.item.html:29
+#: ../templates/minimal/html/user.item.html:29
 msgid "User${id} Editing"
 msgstr "Edición de Usuario${id}"
 
-#: ../templates/classic/html/user.item.html:43
-#: ../templates/classic/html/user.register.html:21
-#: ../templates/minimal/html/user.item.html:40
-#: ../templates/minimal/html/user.register.html:26
-msgid "Login Name"
-msgstr "Nombre para Login"
-
-#: ../templates/classic/html/user.item.html:47
-#: ../templates/classic/html/user.register.html:25
-#: ../templates/minimal/html/user.item.html:44
-#: ../templates/minimal/html/user.register.html:30
-msgid "Login Password"
-msgstr "Contraseña para Login"
-
-#: ../templates/classic/html/user.item.html:51
-#: ../templates/classic/html/user.register.html:29
-#: ../templates/minimal/html/user.item.html:48
-#: ../templates/minimal/html/user.register.html:34
-msgid "Confirm Password"
-msgstr "Confirmar contraseña"
-
-#: ../templates/classic/html/user.item.html:55
+#: ../templates/classic/html/user.item.html:80
 #: ../templates/classic/html/user.register.html:33
-#: ../templates/minimal/html/user.item.html:52
-#: ../templates/minimal/html/user.register.html:38
+#: ../templates/minimal/html/user.item.html:80
+#: ../templates/minimal/html/user.register.html:41
 msgid "Roles"
 msgstr "Roles"
 
-#: ../templates/classic/html/user.item.html:61
-#: ../templates/minimal/html/user.item.html:58
+#: ../templates/classic/html/user.item.html:88
+#: ../templates/minimal/html/user.item.html:88
 msgid "(to give the user more than one role, enter a comma,separated,list)"
 msgstr ""
 "(para asignar más de un rol al usuario, ingrese una lista de los mismos "
 "separados por comas)"
 
-#: ../templates/classic/html/user.item.html:66
-#: ../templates/classic/html/user.register.html:41
-msgid "Phone"
-msgstr "Teléfono"
-
-#: ../templates/classic/html/user.item.html:74
-msgid "Timezone"
-msgstr "Zona horaria"
-
-#: ../templates/classic/html/user.item.html:78
+#: ../templates/classic/html/user.item.html:109
+#: ../templates/minimal/html/user.item.html:109
 msgid "(this is a numeric hour offset, the default is ${zone})"
 msgstr ""
 "(este es un valor numérico de diferencia horaria, el valor por defecto es "
 "${zone})"
 
-#: ../templates/classic/html/user.item.html:83
-#: ../templates/classic/html/user.register.html:49
-#: ../templates/minimal/html/user.item.html:63
-#: ../templates/minimal/html/user.register.html:46
-msgid "E-mail address"
-msgstr "Dirección de e-mail"
-
-#: ../templates/classic/html/user.item.html:91
+#: ../templates/classic/html/user.item.html:130
 #: ../templates/classic/html/user.register.html:53
-#: ../templates/minimal/html/user.item.html:71
-#: ../templates/minimal/html/user.register.html:50
+#: ../templates/minimal/html/user.item.html:130
+#: ../templates/minimal/html/user.register.html:53
 msgid "Alternate E-mail addresses<br>One address per line"
 msgstr "Direcciones de e-mail alternativas <br>Una dirección por línea"
 
@@ -3397,6 +3550,30 @@
 msgid "Registering with ${tracker}"
 msgstr "Registrándose en ${tracker}"
 
+#: ../templates/classic/html/user.register.html:21
+#: ../templates/minimal/html/user.register.html:29
+msgid "Login Name"
+msgstr "Nombre para Login"
+
+#: ../templates/classic/html/user.register.html:25
+#: ../templates/minimal/html/user.register.html:33
+msgid "Login Password"
+msgstr "Contraseña para Login"
+
+#: ../templates/classic/html/user.register.html:29
+#: ../templates/minimal/html/user.register.html:37
+msgid "Confirm Password"
+msgstr "Confirmar contraseña"
+
+#: ../templates/classic/html/user.register.html:41
+msgid "Phone"
+msgstr "Teléfono"
+
+#: ../templates/classic/html/user.register.html:49
+#: ../templates/minimal/html/user.register.html:49
+msgid "E-mail address"
+msgstr "Dirección de e-mail"
+
 #: ../templates/classic/html/user.rego_progress.html:4
 #: ../templates/minimal/html/user.rego_progress.html:4
 msgid "Registration in progress - ${tracker}"
@@ -3416,91 +3593,101 @@
 "En breve recibirá un mensaje de e-mail para confirmar su registro. Para "
 "completar el proceso, visite el enlace indicado en dicho mensaje."
 
-#: ../templates/minimal/html/home.html:2
-msgid "Tracker home - ${tracker}"
-msgstr "Directorio base del tracker - ${tracker}"
-
-#: ../templates/minimal/html/home.html:4
-msgid "Tracker home"
-msgstr "Directorio base del tracker"
-
-#: ../templates/minimal/html/home.html:16
-msgid "Please select from one of the menu options on the left."
-msgstr "Por favor seleccione entre las opciones del menú a la izquierda."
-
-#: ../templates/minimal/html/home.html:19
-msgid "Please log in or register."
-msgstr "Por favor ingrese al sistema o regístrese en el mismo."
-
-#: ../templates/minimal/html/page.html:55
-msgid "Hello,<br>${user}"
-msgstr "Hola,<br>${user}"
-
 # priority translations:
 #: ../templates/classic/initial_data.py:5
-#: ../templates/classic/html/page.html:246
 msgid "critical"
-msgstr ""
+msgstr "critical"
 
 #: ../templates/classic/initial_data.py:6
-#: ../templates/classic/html/page.html:246
 msgid "urgent"
-msgstr ""
+msgstr "urgent"
 
 #: ../templates/classic/initial_data.py:7
-#: ../templates/classic/html/page.html:246
 msgid "bug"
-msgstr ""
+msgstr "bug"
 
 #: ../templates/classic/initial_data.py:8
-#: ../templates/classic/html/page.html:246
 msgid "feature"
-msgstr ""
+msgstr "feature"
 
 #: ../templates/classic/initial_data.py:9
-#: ../templates/classic/html/page.html:246
 msgid "wish"
-msgstr ""
+msgstr "wish"
 
-#: status translations: ../templates/classic/initial_data.py:12
-#: ../templates/classic/html/page.html:246
+#: ../templates/classic/initial_data.py:12
 msgid "unread"
-msgstr ""
+msgstr "unread"
 
 #: ../templates/classic/initial_data.py:13
-#: ../templates/classic/html/page.html:246
 msgid "deferred"
-msgstr ""
+msgstr "deferred"
 
 #: ../templates/classic/initial_data.py:14
-#: ../templates/classic/html/page.html:246
 msgid "chatting"
-msgstr ""
+msgstr "chatting"
 
 #: ../templates/classic/initial_data.py:15
-#: ../templates/classic/html/page.html:246
-msgid "in-progress"
-msgstr ""
+msgid "need-eg"
+msgstr "need-eg"
 
 #: ../templates/classic/initial_data.py:16
-#: ../templates/classic/html/page.html:246
-msgid "need-eg"
-msgstr ""
+msgid "in-progress"
+msgstr "in-progress"
 
 #: ../templates/classic/initial_data.py:17
-#: ../templates/classic/html/page.html:246
 msgid "testing"
-msgstr ""
+msgstr "testing"
 
 #: ../templates/classic/initial_data.py:18
-#: ../templates/classic/html/page.html:246
 msgid "done-cbb"
-msgstr ""
+msgstr "done-cbb"
 
 #: ../templates/classic/initial_data.py:19
-#: ../templates/classic/html/page.html:246
 msgid "resolved"
 msgstr "resuelto"
 
+#: ../templates/minimal/html/home.html:2
+msgid "Tracker home - ${tracker}"
+msgstr "Directorio base del tracker - ${tracker}"
+
+#: ../templates/minimal/html/home.html:4
+msgid "Tracker home"
+msgstr "Directorio base del tracker"
+
+#: ../templates/minimal/html/home.html:16
+msgid "Please select from one of the menu options on the left."
+msgstr "Por favor seleccione entre las opciones del menú a la izquierda."
+
+#: ../templates/minimal/html/home.html:19
+msgid "Please log in or register."
+msgstr "Por favor ingrese al sistema o regístrese en el mismo."
+
+#~ msgid "topic"
+#~ msgstr "palabraclave"
+
+#~ msgid "System message:"
+#~ msgstr "Mensaje de sistema:"
+
+#~ msgid "List of issues - ${tracker}"
+#~ msgstr "Lista de issues - ${tracker}"
+
+#~ msgid "Topic"
+#~ msgstr "Palabra clave"
+
+#~ msgid "View: ${link}"
+#~ msgstr "Ver: ${link}"
+
+#~ msgid "Topics"
+#~ msgstr "Palabras clave"
+
+#~ msgid "Topic:"
+#~ msgstr "Palabra clave:"
+
+#~ msgid "Timezone"
+#~ msgstr "Zona horaria"
+
+#~ msgid "Hello,<br>${user}"
+#~ msgstr "Hola,<br>${user}"
+
 #~ msgid "User editing - ${tracker}"
 #~ msgstr "Edición de usuario - ${tracker}"

Modified: tracker/roundup-src/locale/hu.po
==============================================================================
--- tracker/roundup-src/locale/hu.po	(original)
+++ tracker/roundup-src/locale/hu.po	Sun Mar  9 09:26:16 2008
@@ -1,47 +1,51 @@
+# Translation of roundup.po to Hungarian
+# Copyright © 2007 Free Software Foundation, Inc.
+# This file is distributed under the same license as the Roundup package.
+#
+# Gulácsi Tamás <T.Gulacsi at unosoft.hu>, 2006.
+# kilo aka Gabor Kmetyko <kg_kilo at freemail.hu>, 2007.
 msgid ""
 msgstr ""
-"Project-Id-Version: v0.1\n"
-"POT-Creation-Date: \n"
-"PO-Revision-Date: 2006-12-02 14:40+0100\n"
-"Last-Translator: Gulácsi Tamás <T.Gulacsi at unosoft.hu>\n"
-"Language-Team: UNO-SOFT <info at unosoft.hu>\n"
+"Project-Id-Version: Roundup 1.3.3\n"
+"Report-Msgid-Bugs-To: roundup-devel at lists.sourceforge.net\n"
+"POT-Creation-Date: 2007-09-27 11:18+0300\n"
+"PO-Revision-Date: 2007-09-20 12:30+0200\n"
+"Last-Translator: kilo aka Gabor Kmetyko <kg_kilo at freemail.hu>\n"
+"Language-Team: Hungarian\n"
 "MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=utf-8\n"
+"Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"X-Poedit-Language: Hungarian\n"
-"X-Poedit-Country: Hungary\n"
-"X-Poedit-SourceCharset: utf-8\n"
+"Plural-Forms:  nplurals=1; plural=0;\n"
+"X-Generator: KBabel 1.11.4\n"
 
 # ../roundup/admin.py:85 :981 :1030 :1052
-#: ../roundup/admin.py:85
-#: ../roundup/admin.py:981
-#: ../roundup/admin.py:1030
-#: ../roundup/admin.py:1052
+#: ../roundup/admin.py:86 ../roundup/admin.py:989 ../roundup/admin.py:1040
+#: ../roundup/admin.py:1063
 #, python-format
 msgid "no such class \"%(classname)s\""
-msgstr "nincs ilyen osztály \"%(classname)s\""
+msgstr "nincs \"%(classname)s\" osztály"
 
 # ../roundup/admin.py:95 :99
-#: ../roundup/admin.py:95
-#: ../roundup/admin.py:99
+#: ../roundup/admin.py:96 ../roundup/admin.py:100
 #, python-format
 msgid "argument \"%(arg)s\" not propname=value"
-msgstr "\"%(arg)s\" argumentum nem név=érték alakú"
+msgstr "A(z) \"%(arg)s\" argumentum nem név=érték alakú"
 
-#: ../roundup/admin.py:112
+#: ../roundup/admin.py:113
 #, python-format
 msgid ""
 "Problem: %(message)s\n"
 "\n"
 msgstr "Probléma: %(message)s\n"
 
-#: ../roundup/admin.py:113
+#: ../roundup/admin.py:114
 #, python-format
 msgid ""
 "%(message)sUsage: roundup-admin [options] [<command> <arguments>]\n"
 "\n"
 "Options:\n"
-" -i instance home  -- specify the issue tracker \"home directory\" to administer\n"
+" -i instance home  -- specify the issue tracker \"home directory\" to "
+"administer\n"
 " -u                -- the user[:password] to use for commands\n"
 " -d                -- print full designators not just class id numbers\n"
 " -c                -- when outputting lists of data, comma-separate them.\n"
@@ -63,16 +67,19 @@
 "%(message)sHasználat: roundup-admin [opciók] [<parancs><argumentumok>]\n"
 "\n"
 "Opciók:\n"
-" -i példány elérési út  -- add meg az adminisztrálni kívánt hibakövető \"könyvtárát\"\n"
+" -i példány elérési út  -- add meg az adminisztrálni kívánt hibakövető "
+"\"könyvtárát\"\n"
 " -u                -- a parancsoknál használt felhasználónév[:jelszó]\n"
 " -d                -- írd ki a teljes nevet, ne csak az osztály azonosítót\n"
 " -c                -- adatlistáknál vesszővel válaszd el az elemeket.\n"
 "                      Ugyanaz mint '-S \",\"'.\n"
-" -S <szöveg>       -- adatlistáknál a megadott szöveggel válaszd el az elemeket\n"
+" -S <szöveg>       -- adatlistáknál a megadott szöveggel válaszd el az "
+"elemeket\n"
 " -s                -- when outputting lists of data, space-separate them.\n"
 "                      Same as '-S \" \"'.\n"
 " -V                -- importálásnál legyél bőbeszédű\n"
-" -v                -- írd ki a Roundup és a Python verziószámokat (és lépj ki)\n"
+" -v                -- írd ki a Roundup és a Python verziószámokat (és lépj "
+"ki)\n"
 "\n"
 " -s, -c vagy -S közül egyszerre csak egy adható meg.\n"
 "\n"
@@ -82,17 +89,19 @@
 " roundup-admin help <command>             -- parancs-specifikus segítség\n"
 " roundup-admin help all                   -- minden elérhető segítség\n"
 
-#: ../roundup/admin.py:140
+#: ../roundup/admin.py:141
 msgid "Commands:"
 msgstr "Parancsok:"
 
-#: ../roundup/admin.py:147
+#: ../roundup/admin.py:148
 msgid ""
 "Commands may be abbreviated as long as the abbreviation\n"
 "matches only one command, e.g. l == li == lis == list."
-msgstr "A parancsok rövidíthetők mindaddig, amíg csak egy parancsra illenek, pl. l == li == lis == list."
+msgstr ""
+"A parancsok rövidíthetők mindaddig, amíg csak egy parancsra illenek, pl. l "
+"== li == lis == list."
 
-#: ../roundup/admin.py:177
+#: ../roundup/admin.py:178
 msgid ""
 "\n"
 "All commands (except help) require a tracker specifier. This is just\n"
@@ -102,7 +111,8 @@
 "directory\". It may be specified in the environment variable TRACKER_HOME\n"
 "or on the command line as \"-i tracker\".\n"
 "\n"
-"A designator is a classname and a nodeid concatenated, eg. bug1, user10, ...\n"
+"A designator is a classname and a nodeid concatenated, eg. bug1, "
+"user10, ...\n"
 "\n"
 "Property values are represented as strings in command arguments and in the\n"
 "printed results:\n"
@@ -127,8 +137,8 @@
 "           Roch\\'e Compaan  (2 tokens: Roch'e Compaan)\n"
 "           address=\"1 2 3\"  (1 token: address=1 2 3)\n"
 "           \\\\               (1 token: \\)\n"
-"           \\n"
-"\\r\\t           (1 token: a newline, carriage-return and tab)\n"
+"           \\n\\r\\t           (1 token: a newline, carriage-return and "
+"tab)\n"
 "\n"
 "When multiple nodes are specified to the roundup get or roundup set\n"
 "commands, the specified properties are retrieved or set on all the listed\n"
@@ -158,12 +168,12 @@
 "Command help:\n"
 msgstr ""
 
-#: ../roundup/admin.py:240
+#: ../roundup/admin.py:241
 #, python-format
 msgid "%s:"
 msgstr "%s:"
 
-#: ../roundup/admin.py:245
+#: ../roundup/admin.py:246
 msgid ""
 "Usage: help topic\n"
 "        Give help about topic.\n"
@@ -178,40 +188,38 @@
 "        Segítséget ad a témáról.\n"
 "\n"
 "        commands  -- parancsok listája\n"
-"        <command> -- segítség adott programhoz\n"
+"        <command> -- segítség adott parancshoz\n"
 "        initopts  -- kezdő parancs opciók\n"
 "        all       -- minden elérhető segítség\n"
 "        "
 
-#: ../roundup/admin.py:268
+#: ../roundup/admin.py:269
 #, python-format
 msgid "Sorry, no help for \"%(topic)s\""
-msgstr "Elnézést, \"%(topic)s\" témához nincs help"
+msgstr "Elnézést, \"%(topic)s\" témához nincs súgó"
 
 # ../roundup/admin.py:340 :396
-#: ../roundup/admin.py:340
-#: ../roundup/admin.py:396
+#: ../roundup/admin.py:346 ../roundup/admin.py:402
 msgid "Templates:"
 msgstr "Sablonok:"
 
 # ../roundup/admin.py:343 :407
-#: ../roundup/admin.py:343
-#: ../roundup/admin.py:407
+#: ../roundup/admin.py:349 ../roundup/admin.py:413
 msgid "Back ends:"
 msgstr "Adatbázis hátterek:"
 
-#: ../roundup/admin.py:346
+#: ../roundup/admin.py:352
 msgid ""
-"Usage: install [template [backend [admin password [key=val[,key=val]]]]]\n"
+"Usage: install [template [backend [key=val[,key=val]]]]\n"
 "        Install a new Roundup tracker.\n"
 "\n"
 "        The command will prompt for the tracker home directory\n"
 "        (if not supplied through TRACKER_HOME or the -i option).\n"
-"        The template, backend and admin password may be specified\n"
-"        on the command-line as arguments, in that order.\n"
+"        The template and backend may be specified on the command-line\n"
+"        as arguments, in that order.\n"
 "\n"
-"        The last command line argument allows to pass initial values\n"
-"        for config options.  For example, passing\n"
+"        Command line arguments following the backend allows you to\n"
+"        pass initial values for config options.  For example, passing\n"
 "        \"web_http_auth=no,rdbms_user=dinsdale\" will override defaults\n"
 "        for options http_auth in section [web] and user in section [rdbms].\n"
 "        Please be careful to not use spaces in this argument! (Enclose\n"
@@ -228,55 +236,46 @@
 
 # ../roundup/admin.py:369 :466 :527 :606 :656 :714 :735 :763 :834 :901 :972
 # :1020 :1042 :1069 :1136 :1207
-#: ../roundup/admin.py:369
-#: ../roundup/admin.py:466
-#: ../roundup/admin.py:527
-#: ../roundup/admin.py:606
-#: ../roundup/admin.py:656
-#: ../roundup/admin.py:714
-#: ../roundup/admin.py:735
-#: ../roundup/admin.py:763
-#: ../roundup/admin.py:834
-#: ../roundup/admin.py:901
-#: ../roundup/admin.py:972
-#: ../roundup/admin.py:1020
-#: ../roundup/admin.py:1042
-#: ../roundup/admin.py:1069
-#: ../roundup/admin.py:1136
-#: ../roundup/admin.py:1207
+#: ../roundup/admin.py:375 ../roundup/admin.py:472 ../roundup/admin.py:533
+#: ../roundup/admin.py:612 ../roundup/admin.py:663 ../roundup/admin.py:721
+#: ../roundup/admin.py:742 ../roundup/admin.py:770 ../roundup/admin.py:842
+#: ../roundup/admin.py:909 ../roundup/admin.py:980 ../roundup/admin.py:1030
+#: ../roundup/admin.py:1053 ../roundup/admin.py:1084 ../roundup/admin.py:1180
+#: ../roundup/admin.py:1253
 msgid "Not enough arguments supplied"
 msgstr "Nincs megadva elég argumentum"
 
-#: ../roundup/admin.py:375
+#: ../roundup/admin.py:381
 #, python-format
 msgid "Instance home parent directory \"%(parent)s\" does not exist"
 msgstr "Példány könyvtár szülője (\"%(parent)s\") nem létezik"
 
-#: ../roundup/admin.py:383
+#: ../roundup/admin.py:389
 #, python-format
 msgid ""
 "WARNING: There appears to be a tracker in \"%(tracker_home)s\"!\n"
 "If you re-install it, you will lose all the data!\n"
 "Erase it? Y/N: "
 msgstr ""
-"FIGYELEM: Úgy tűnik, már létezik egy hibakövető a \"%(tracker_home)s\" könyvtárban!\n"
+"FIGYELEM: Úgy tűnik, már létezik egy hibakövető a \"%(tracker_home)s\" "
+"könyvtárban!\n"
 "Ha újra installálod, minden adat elveszik!\n"
 "Töröljem? Y/N: "
 
-#: ../roundup/admin.py:398
+#: ../roundup/admin.py:404
 msgid "Select template [classic]: "
-msgstr "Sablon választása [classic]:"
+msgstr "Sablon választása [classic]: "
 
-#: ../roundup/admin.py:409
+#: ../roundup/admin.py:415
 msgid "Select backend [anydbm]: "
-msgstr "Adatbázis háttér választása [anydbm]:"
+msgstr "Adatbázis háttér választása [anydbm]: "
 
-#: ../roundup/admin.py:419
+#: ../roundup/admin.py:425
 #, python-format
 msgid "Error in configuration settings: \"%s\""
 msgstr "Hiba a konfigurációs beállításokban: \"%s\""
 
-#: ../roundup/admin.py:428
+#: ../roundup/admin.py:434
 #, python-format
 msgid ""
 "\n"
@@ -289,11 +288,11 @@
 " Most kell szerkesztened a konfigurációs fájlt:\n"
 "   %(config_file)s"
 
-#: ../roundup/admin.py:438
+#: ../roundup/admin.py:444
 msgid " ... at a minimum, you must set following options:"
-msgstr " ... legkevesebb a következő opciókat kell beállítani:"
+msgstr " ... legalább a következő opciókat kell beállítani:"
 
-#: ../roundup/admin.py:443
+#: ../roundup/admin.py:449
 #, python-format
 msgid ""
 "\n"
@@ -304,12 +303,13 @@
 "   %(database_init_file)s\n"
 " ... see the documentation on customizing for more information.\n"
 "\n"
-" You MUST run the \"roundup-admin initialise\" command once you've performed\n"
+" You MUST run the \"roundup-admin initialise\" command once you've "
+"performed\n"
 " the above steps.\n"
 "---------------------------------------------------------------------------\n"
 msgstr ""
 
-#: ../roundup/admin.py:461
+#: ../roundup/admin.py:467
 msgid ""
 "Usage: genconfig <filename>\n"
 "        Generate a new tracker config file (ini style) with default values\n"
@@ -317,12 +317,13 @@
 "        "
 msgstr ""
 "Használat: genconfig <fájlnév>\n"
-"        Új hibakövető konfigurációs fájl (ini stílusú) generálása alapértelmezett értékekkel\n"
+"        Új hibakövető konfigurációs fájl (ini stílusú) generálása "
+"alapértelmezett értékekkel\n"
 "        a <fájlnév> fájlba.\n"
 "        "
 
 #. password
-#: ../roundup/admin.py:471
+#: ../roundup/admin.py:477
 msgid ""
 "Usage: initialise [adminpw]\n"
 "        Initialise a new Roundup tracker.\n"
@@ -340,33 +341,33 @@
 "        Végrehajtja az adatbázist inicializáló dbinit.init() rutint\n"
 "        "
 
-#: ../roundup/admin.py:485
+#: ../roundup/admin.py:491
 msgid "Admin Password: "
-msgstr "Adminisztrátori jelszó:"
+msgstr "Adminisztrátori jelszó: "
 
-#: ../roundup/admin.py:486
+#: ../roundup/admin.py:492
 msgid "       Confirm: "
-msgstr "       Megerősítés: "
+msgstr "       Megerősítés "
 
-#: ../roundup/admin.py:490
+#: ../roundup/admin.py:496
 msgid "Instance home does not exist"
 msgstr "A példány könyvtára nem létezik"
 
-#: ../roundup/admin.py:494
+#: ../roundup/admin.py:500
 msgid "Instance has not been installed"
 msgstr "A példány nem lett installálva"
 
-#: ../roundup/admin.py:499
+#: ../roundup/admin.py:505
 msgid ""
 "WARNING: The database is already initialised!\n"
 "If you re-initialise it, you will lose all the data!\n"
 "Erase it? Y/N: "
 msgstr ""
 "FIGYELEM: Az adatbázis már inicializált!\n"
-"Ha újrainicializálod, minden adat elveszik!\n"
-"Törlöd? Y/N:"
+"Újrainicializálás esetén minden adat elvész!\n"
+"Törli? Y/N: "
 
-#: ../roundup/admin.py:520
+#: ../roundup/admin.py:526
 msgid ""
 "Usage: get property designator[,designator]*\n"
 "        Get the given property of one or more designator(s).\n"
@@ -378,32 +379,31 @@
 "Használat: get property designator[,designator]*\n"
 "        Visszaadja egy vagy több jelölő tulajdonságát.\n"
 "\n"
-"        Visszaadja az értékét a jelölő által\n"
-"        meghatározott csomópontnak.\n"
+"        Visszaadja a jelölő által meghatározott\n"
+"        csomópont értékét.\n"
 "        "
 
 # ../roundup/admin.py:560 :575
-#: ../roundup/admin.py:560
-#: ../roundup/admin.py:575
+#: ../roundup/admin.py:566 ../roundup/admin.py:581
 #, python-format
 msgid "property %s is not of type Multilink or Link so -d flag does not apply."
 msgstr ""
+"A(z) %s tulajdonság nem Multilink vagy Link típusú, ezért a -d kapcsoló nem "
+"alkalmazható."
 
 # ../roundup/admin.py:583 :983 :1032 :1054
-#: ../roundup/admin.py:583
-#: ../roundup/admin.py:983
-#: ../roundup/admin.py:1032
-#: ../roundup/admin.py:1054
+#: ../roundup/admin.py:589 ../roundup/admin.py:991 ../roundup/admin.py:1042
+#: ../roundup/admin.py:1065
 #, python-format
 msgid "no such %(classname)s node \"%(nodeid)s\""
-msgstr ""
+msgstr "nincs \"%(nodeid)s\" %(classname)s csomópont"
 
-#: ../roundup/admin.py:585
+#: ../roundup/admin.py:591
 #, python-format
 msgid "no such %(classname)s property \"%(propname)s\""
-msgstr ""
+msgstr "nincs \"%(propname)s\" %(classname)s tulajdonság"
 
-#: ../roundup/admin.py:594
+#: ../roundup/admin.py:600
 msgid ""
 "Usage: set items property=value property=value ...\n"
 "        Set the given properties of one or more items(s).\n"
@@ -412,13 +412,14 @@
 "        list of item designators (ie \"designator[,designator,...]\").\n"
 "\n"
 "        This command sets the properties to the values for all designators\n"
-"        given. If the value is missing (ie. \"property=\") then the property\n"
+"        given. If the value is missing (ie. \"property=\") then the "
+"property\n"
 "        is un-set. If the property is a multilink, you specify the linked\n"
 "        ids for the multilink as comma-separated numbers (ie \"1,2,3\").\n"
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:648
+#: ../roundup/admin.py:655
 msgid ""
 "Usage: find classname propname=value ...\n"
 "        Find the nodes of the given class with a given link property value.\n"
@@ -430,15 +431,13 @@
 msgstr ""
 
 # ../roundup/admin.py:701 :854 :866 :920
-#: ../roundup/admin.py:701
-#: ../roundup/admin.py:854
-#: ../roundup/admin.py:866
-#: ../roundup/admin.py:920
+#: ../roundup/admin.py:708 ../roundup/admin.py:862 ../roundup/admin.py:874
+#: ../roundup/admin.py:928
 #, python-format
 msgid "%(classname)s has no property \"%(propname)s\""
-msgstr ""
+msgstr "%(classname)s-nek nincs \"%(propname)s\" tulajdonsága"
 
-#: ../roundup/admin.py:708
+#: ../roundup/admin.py:715
 msgid ""
 "Usage: specification classname\n"
 "        Show the properties for a classname.\n"
@@ -446,18 +445,23 @@
 "        This lists the properties for a given class.\n"
 "        "
 msgstr ""
+"Használat: specification classname\n"
+"        Osztály tulajdonságainak megjelenítése.\n"
+"\n"
+"        Listázza az adott osztály tulajdonságait.\n"
+"        "
 
-#: ../roundup/admin.py:723
+#: ../roundup/admin.py:730
 #, python-format
 msgid "%(key)s: %(value)s (key property)"
-msgstr ""
+msgstr "%(key)s: %(value)s (kulcs tulajdonság)"
 
-#: ../roundup/admin.py:725
+#: ../roundup/admin.py:732 ../roundup/admin.py:759
 #, python-format
 msgid "%(key)s: %(value)s"
-msgstr ""
+msgstr "%(key)s: %(value)s"
 
-#: ../roundup/admin.py:728
+#: ../roundup/admin.py:735
 msgid ""
 "Usage: display designator[,designator]*\n"
 "        Show the property values for the given node(s).\n"
@@ -467,47 +471,43 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:752
-#, python-format
-msgid "%(key)s: %(value)r"
-msgstr ""
-
-#: ../roundup/admin.py:755
+#: ../roundup/admin.py:762
 msgid ""
 "Usage: create classname property=value ...\n"
 "        Create a new entry of a given class.\n"
 "\n"
 "        This creates a new entry of the given class using the property\n"
-"        name=value arguments provided on the command line after the \"create\"\n"
+"        name=value arguments provided on the command line after the \"create"
+"\"\n"
 "        command.\n"
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:782
+#: ../roundup/admin.py:789
 #, python-format
 msgid "%(propname)s (Password): "
-msgstr ""
+msgstr "%(propname)s (Jelszó): "
 
-#: ../roundup/admin.py:784
+#: ../roundup/admin.py:791
 #, python-format
 msgid "   %(propname)s (Again): "
-msgstr ""
+msgstr "   %(propname)s (Ismét): "
 
-#: ../roundup/admin.py:786
+#: ../roundup/admin.py:793
 msgid "Sorry, try again..."
-msgstr ""
+msgstr "Sajnálom, próbálja újra..."
 
-#: ../roundup/admin.py:790
+#: ../roundup/admin.py:797
 #, python-format
 msgid "%(propname)s (%(proptype)s): "
-msgstr ""
+msgstr "%(propname)s (%(proptype)s): "
 
-#: ../roundup/admin.py:808
+#: ../roundup/admin.py:815
 #, python-format
 msgid "you must provide the \"%(propname)s\" property."
-msgstr ""
+msgstr "meg kell adni a(z) \"%(propname)s\" tulajdonságot."
 
-#: ../roundup/admin.py:819
+#: ../roundup/admin.py:827
 msgid ""
 "Usage: list classname [property]\n"
 "        List the instances of a class.\n"
@@ -523,16 +523,16 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:832
+#: ../roundup/admin.py:840
 msgid "Too many arguments supplied"
-msgstr ""
+msgstr "Túl sok argumentum került megadásra"
 
-#: ../roundup/admin.py:868
+#: ../roundup/admin.py:876
 #, python-format
 msgid "%(nodeid)4s: %(value)s"
-msgstr ""
+msgstr "%(nodeid)4s: %(value)s"
 
-#: ../roundup/admin.py:872
+#: ../roundup/admin.py:880
 msgid ""
 "Usage: table classname [property[,property]*]\n"
 "        List the instances of a class in tabular form.\n"
@@ -564,21 +564,22 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:916
+#: ../roundup/admin.py:924
 #, python-format
 msgid "\"%(spec)s\" not name:width"
-msgstr ""
+msgstr "\"%(spec)s\" nem név:hossz formátumú"
 
-#: ../roundup/admin.py:966
+#: ../roundup/admin.py:974
 msgid ""
 "Usage: history designator\n"
 "        Show the history entries of a designator.\n"
 "\n"
-"        Lists the journal entries for the node identified by the designator.\n"
+"        Lists the journal entries for the node identified by the "
+"designator.\n"
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:987
+#: ../roundup/admin.py:995
 msgid ""
 "Usage: commit\n"
 "        Commit changes made to the database during an interactive session.\n"
@@ -592,7 +593,7 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:1001
+#: ../roundup/admin.py:1010
 msgid ""
 "Usage: rollback\n"
 "        Undo all changes that are pending commit to the database.\n"
@@ -604,7 +605,7 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:1013
+#: ../roundup/admin.py:1023
 msgid ""
 "Usage: retire designator[,designator]*\n"
 "        Retire the node specified by designator.\n"
@@ -614,7 +615,7 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:1036
+#: ../roundup/admin.py:1047
 msgid ""
 "Usage: restore designator[,designator]*\n"
 "        Restore the retired node specified by designator.\n"
@@ -624,12 +625,32 @@
 msgstr ""
 
 #. grab the directory to export to
-#: ../roundup/admin.py:1058
+#: ../roundup/admin.py:1070
 msgid ""
-"Usage: export [class[,class]] export_dir\n"
+"Usage: export [[-]class[,class]] export_dir\n"
 "        Export the database to colon-separated-value files.\n"
+"        To exclude the files (e.g. for the msg or file class),\n"
+"        use the exporttables command.\n"
+"\n"
+"        Optionally limit the export to just the named classes\n"
+"        or exclude the named classes, if the 1st argument starts with '-'.\n"
+"\n"
+"        This action exports the current data from the database into\n"
+"        colon-separated-value files that are placed in the nominated\n"
+"        destination directory.\n"
+"        "
+msgstr ""
+
+#: ../roundup/admin.py:1145
+msgid ""
+"Usage: exporttables [[-]class[,class]] export_dir\n"
+"        Export the database to colon-separated-value files, excluding the\n"
+"        files below $TRACKER_HOME/db/files/ (which can be archived "
+"separately).\n"
+"        To include the files, use the export command.\n"
 "\n"
-"        Optionally limit the export to just the names classes.\n"
+"        Optionally limit the export to just the named classes\n"
+"        or exclude the named classes, if the 1st argument starts with '-'.\n"
 "\n"
 "        This action exports the current data from the database into\n"
 "        colon-separated-value files that are placed in the nominated\n"
@@ -637,7 +658,7 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:1116
+#: ../roundup/admin.py:1160
 msgid ""
 "Usage: import import_dir\n"
 "        Import a database from the directory containing CSV files,\n"
@@ -660,14 +681,15 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:1189
+#: ../roundup/admin.py:1235
 msgid ""
 "Usage: pack period | date\n"
 "\n"
 "        Remove journal entries older than a period of time specified or\n"
 "        before a certain date.\n"
 "\n"
-"        A period is specified using the suffixes \"y\", \"m\", and \"d\". The\n"
+"        A period is specified using the suffixes \"y\", \"m\", and \"d\". "
+"The\n"
 "        suffix \"w\" (for \"week\") means 7 days.\n"
 "\n"
 "              \"3y\" means three years\n"
@@ -681,11 +703,11 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:1217
+#: ../roundup/admin.py:1263
 msgid "Invalid format"
 msgstr "Hibás formátum"
 
-#: ../roundup/admin.py:1227
+#: ../roundup/admin.py:1274
 msgid ""
 "Usage: reindex [classname|designator]*\n"
 "        Re-generate a tracker's search indexes.\n"
@@ -695,148 +717,185 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:1241
+#: ../roundup/admin.py:1288
 #, python-format
 msgid "no such item \"%(designator)s\""
-msgstr ""
+msgstr "nincs ilyen elem: \"%(designator)s\""
 
-#: ../roundup/admin.py:1251
+#: ../roundup/admin.py:1298
 msgid ""
 "Usage: security [Role name]\n"
 "        Display the Permissions available to one or all Roles.\n"
 "        "
 msgstr ""
+"Használat: security [szerepkör]\n"
+"        Megjeleníti a megadott vagy az összes szerepkör jogosultságait.\n"
+"        "
 
-#: ../roundup/admin.py:1259
+#: ../roundup/admin.py:1306
 #, python-format
 msgid "No such Role \"%(role)s\""
-msgstr ""
+msgstr "Nincs ilyen szerepkör: \"%(role)s\""
 
-#: ../roundup/admin.py:1265
+#: ../roundup/admin.py:1312
 #, python-format
 msgid "New Web users get the Roles \"%(role)s\""
-msgstr ""
+msgstr "Új web felhasználók ezeket a szerepköröket kapják: \"%(role)s\""
 
-#: ../roundup/admin.py:1267
+#: ../roundup/admin.py:1314
 #, python-format
 msgid "New Web users get the Role \"%(role)s\""
-msgstr ""
+msgstr "Új web felhasználók ezt a szerepkört kapják \"%(role)s\""
 
-#: ../roundup/admin.py:1270
+#: ../roundup/admin.py:1317
 #, python-format
 msgid "New Email users get the Roles \"%(role)s\""
-msgstr ""
+msgstr "Új e-mail felhasználók ezeket a szerepköröket kapják: \"%(role)s\""
 
-#: ../roundup/admin.py:1272
+#: ../roundup/admin.py:1319
 #, python-format
 msgid "New Email users get the Role \"%(role)s\""
-msgstr ""
+msgstr "Új e-mail felhasználók ezt a szerepkört kapják: \"%(role)s\""
 
-#: ../roundup/admin.py:1275
+#: ../roundup/admin.py:1322
 #, python-format
 msgid "Role \"%(name)s\":"
-msgstr ""
+msgstr "\"%(name)s\" szerepkör:"
 
-#: ../roundup/admin.py:1280
+#: ../roundup/admin.py:1327
 #, python-format
 msgid " %(description)s (%(name)s for \"%(klass)s\": %(properties)s only)"
 msgstr ""
 
-#: ../roundup/admin.py:1283
+#: ../roundup/admin.py:1330
 #, python-format
 msgid " %(description)s (%(name)s for \"%(klass)s\" only)"
 msgstr ""
 
-#: ../roundup/admin.py:1286
+#: ../roundup/admin.py:1333
 #, python-format
 msgid " %(description)s (%(name)s)"
-msgstr ""
+msgstr " %(description)s (%(name)s)"
 
-#: ../roundup/admin.py:1315
+#: ../roundup/admin.py:1362
 #, python-format
 msgid "Unknown command \"%(command)s\" (\"help commands\" for a list)"
 msgstr ""
+"\"%(command)s\": ismeretlen parancs (\"help commands\" parancsok "
+"listázásához)"
 
-#: ../roundup/admin.py:1321
+#: ../roundup/admin.py:1368
 #, python-format
 msgid "Multiple commands match \"%(command)s\": %(list)s"
-msgstr "Több parancs is illeszkedik a megadottra \"%(command)s\": %(list)s"
+msgstr ""
+"Több parancs is illeszkedik a megadott \"%(command)s\" parancsra: %(list)s"
 
-#: ../roundup/admin.py:1328
+#: ../roundup/admin.py:1375
 msgid "Enter tracker home: "
-msgstr "Add meg a hibakövető könyvtárát:"
+msgstr "Adja meg a hibakövető könyvtárát: "
 
 # ../roundup/admin.py:1335 :1341 :1361
-#: ../roundup/admin.py:1335
-#: ../roundup/admin.py:1341
-#: ../roundup/admin.py:1361
+#: ../roundup/admin.py:1382 ../roundup/admin.py:1388 ../roundup/admin.py:1408
 #, python-format
 msgid "Error: %(message)s"
 msgstr "Hiba: %(message)s"
 
-#: ../roundup/admin.py:1349
+#: ../roundup/admin.py:1396
 #, python-format
 msgid "Error: Couldn't open tracker: %(message)s"
-msgstr "Hiba: Nem tudtam megnyitni a hibakövetőt: %(message)s"
+msgstr "Hiba: Hibakövető megnyitása sikertelen: %(message)s"
 
-#: ../roundup/admin.py:1374
+#: ../roundup/admin.py:1421
 #, python-format
 msgid ""
 "Roundup %s ready for input.\n"
 "Type \"help\" for help."
 msgstr ""
-"Roundup %s fogadókész.\n"
-"Gépelj \"help\"-et a segítséghez."
+"A Roundup %s fogadókész.\n"
+"Segítségért gépeljen \"help\"-et."
 
-#: ../roundup/admin.py:1379
+#: ../roundup/admin.py:1426
 msgid "Note: command history and editing not available"
 msgstr "Megjegyzés: a parancsok története és szerkesztése nem elérhető"
 
-#: ../roundup/admin.py:1383
+#: ../roundup/admin.py:1430
 msgid "roundup> "
 msgstr "roundup> "
 
-#: ../roundup/admin.py:1385
+#: ../roundup/admin.py:1432
 msgid "exit..."
 msgstr "kilépés..."
 
-#: ../roundup/admin.py:1395
+#: ../roundup/admin.py:1442
 msgid "There are unsaved changes. Commit them (y/N)? "
 msgstr "Vannak nem mentett változtatások. Elmenti őket (y/N)? "
 
-#: ../roundup/backends/back_anydbm.py:2001
+#: ../roundup/backends/back_anydbm.py:219
+#: ../roundup/backends/sessions_dbm.py:50
+msgid "Couldn't identify database type"
+msgstr ""
+
+#: ../roundup/backends/back_anydbm.py:245
+#, python-format
+msgid "Couldn't open database - the required module '%s' is not available"
+msgstr ""
+
+# ../roundup/backends/back_anydbm.py:795:1070
+# ../roundup/backends/back_metakit.py:567:834
+# ../roundup/backends/rdbms_common.py:1320:1549 :1267:1285 :1331:1901
+# :1755:1775 :1828:2436 :866:1601
+#: ../roundup/backends/back_anydbm.py:795
+#: ../roundup/backends/back_anydbm.py:1070
+#: ../roundup/backends/back_anydbm.py:1267
+#: ../roundup/backends/back_anydbm.py:1285
+#: ../roundup/backends/back_anydbm.py:1331
+#: ../roundup/backends/back_anydbm.py:1901
+#: ../roundup/backends/back_metakit.py:567
+#: ../roundup/backends/back_metakit.py:834
+#: ../roundup/backends/back_metakit.py:866
+#: ../roundup/backends/back_metakit.py:1601
+#: ../roundup/backends/rdbms_common.py:1320
+#: ../roundup/backends/rdbms_common.py:1549
+#: ../roundup/backends/rdbms_common.py:1755
+#: ../roundup/backends/rdbms_common.py:1775
+#: ../roundup/backends/rdbms_common.py:1828
+#: ../roundup/backends/rdbms_common.py:2436
+msgid "Database open read-only"
+msgstr ""
+
+#: ../roundup/backends/back_anydbm.py:2003
 #, python-format
 msgid "WARNING: invalid date tuple %r"
 msgstr "FIGYELEM: hibás dátum tuple %r"
 
-#: ../roundup/backends/rdbms_common.py:1434
+#: ../roundup/backends/rdbms_common.py:1449
 msgid "create"
 msgstr "létrehozás"
 
-#: ../roundup/backends/rdbms_common.py:1600
+#: ../roundup/backends/rdbms_common.py:1615
 msgid "unlink"
 msgstr "törlés"
 
-#: ../roundup/backends/rdbms_common.py:1604
+#: ../roundup/backends/rdbms_common.py:1619
 msgid "link"
 msgstr "kapcsolás"
 
-#: ../roundup/backends/rdbms_common.py:1724
+#: ../roundup/backends/rdbms_common.py:1741
 msgid "set"
 msgstr "beállítás"
 
-#: ../roundup/backends/rdbms_common.py:1748
+#: ../roundup/backends/rdbms_common.py:1765
 msgid "retired"
 msgstr "visszavonult"
 
-#: ../roundup/backends/rdbms_common.py:1778
+#: ../roundup/backends/rdbms_common.py:1795
 msgid "restored"
 msgstr "visszaállított"
 
 #: ../roundup/cgi/actions.py:58
 #, python-format
 msgid "You do not have permission to %(action)s the %(classname)s class."
-msgstr ""
+msgstr "Nincs jogosultsága %(action)s műveletre a(z) %(classname)s osztályon."
 
 #: ../roundup/cgi/actions.py:89
 msgid "No type specified"
@@ -849,11 +908,11 @@
 #: ../roundup/cgi/actions.py:97
 #, python-format
 msgid "\"%(input)s\" is not an ID (%(classname)s ID required)"
-msgstr ""
+msgstr "\"%(input)s\" nem azonosító (%(classname)s azonosító szükséges)"
 
 #: ../roundup/cgi/actions.py:117
 msgid "You may not retire the admin or anonymous user"
-msgstr "Az admin és anonymous felhasználókat nem nyugdíjazhatod"
+msgstr "Az admin és anonymous felhasználókat nem lehet visszavonultatni"
 
 #: ../roundup/cgi/actions.py:124
 #, python-format
@@ -861,127 +920,127 @@
 msgstr "%(classname)s %(itemid)s visszavonásra került"
 
 # ../roundup/cgi/actions.py:174 :202
-#: ../roundup/cgi/actions.py:174
-#: ../roundup/cgi/actions.py:202
+#: ../roundup/cgi/actions.py:169 ../roundup/cgi/actions.py:197
 msgid "You do not have permission to edit queries"
-msgstr "Nincs jogod a lekérdezések szerkesztéséhez"
+msgstr "Nincs jogosultsága a lekérdezések szerkesztéséhez"
 
 # ../roundup/cgi/actions.py:180 :209
-#: ../roundup/cgi/actions.py:180
-#: ../roundup/cgi/actions.py:209
+#: ../roundup/cgi/actions.py:175 ../roundup/cgi/actions.py:204
 msgid "You do not have permission to store queries"
-msgstr "Nincs jogod a lekérdezések tárolásához"
+msgstr "Nincs jogosultsága a lekérdezések tárolásához"
 
-#: ../roundup/cgi/actions.py:297
+#: ../roundup/cgi/actions.py:310
 #, python-format
 msgid "Not enough values on line %(line)s"
 msgstr "Nincs elég érték a(z) %(line)s soron"
 
-#: ../roundup/cgi/actions.py:344
+#: ../roundup/cgi/actions.py:357
 msgid "Items edited OK"
 msgstr "Az elemek sikeresen szerkesztve"
 
-#: ../roundup/cgi/actions.py:404
+#: ../roundup/cgi/actions.py:416
 #, python-format
 msgid "%(class)s %(id)s %(properties)s edited ok"
 msgstr "%(class)s %(id)s %(properties)s sikeresen szerkesztve"
 
-#: ../roundup/cgi/actions.py:407
+#: ../roundup/cgi/actions.py:419
 #, python-format
 msgid "%(class)s %(id)s - nothing changed"
-msgstr "%(class)s %(id)s - semmi sem váltzozott"
+msgstr "%(class)s %(id)s - nincs változás"
 
-#: ../roundup/cgi/actions.py:419
+#: ../roundup/cgi/actions.py:431
 #, python-format
 msgid "%(class)s %(id)s created"
 msgstr "%(class)s %(id)s létrehozva"
 
-#: ../roundup/cgi/actions.py:451
+#: ../roundup/cgi/actions.py:463
 #, python-format
 msgid "You do not have permission to edit %(class)s"
-msgstr "Nincs jogod szerkeszteni %(class)s-t"
+msgstr "Nincs jogosultsága szerkeszteni %(class)s-t"
 
-#: ../roundup/cgi/actions.py:463
+#: ../roundup/cgi/actions.py:475
 #, python-format
 msgid "You do not have permission to create %(class)s"
-msgstr "Nincs jogod létrehozni %(class)s-t"
+msgstr "Nincs jogosultsága létrehozni %(class)s-t"
 
-#: ../roundup/cgi/actions.py:487
+#: ../roundup/cgi/actions.py:499
 msgid "You do not have permission to edit user roles"
-msgstr "Nincs jogod szerkeszteni a felhasználói szerepköröket"
+msgstr "Nincs jogosultsága a felhasználói szerepkörök szerkesztéséhez"
 
-#: ../roundup/cgi/actions.py:537
+#: ../roundup/cgi/actions.py:549
 #, python-format
-msgid "Edit Error: someone else has edited this %s (%s). View <a target=\"new\" href=\"%s%s\">their changes</a> in a new window."
-msgstr "Szerkesztési hiba: valaki már szerkesztette %s (%s). Nézd meg a <a target=\"new\" href=\"%s%s\">változtatásait</a> egy új ablakban."
+msgid ""
+"Edit Error: someone else has edited this %s (%s). View <a target=\"new\" "
+"href=\"%s%s\">their changes</a> in a new window."
+msgstr ""
+"Szerkesztési hiba: valaki már szerkesztette %s (%s). Nézze meg a <a target="
+"\"new\" href=\"%s%s\">változtatásait</a> egy új ablakban."
 
-#: ../roundup/cgi/actions.py:565
+#: ../roundup/cgi/actions.py:577
 #, python-format
 msgid "Edit Error: %s"
 msgstr "Szerkesztési hiba: %s"
 
 # ../roundup/cgi/actions.py:596 :607 :778 :797
-#: ../roundup/cgi/actions.py:596
-#: ../roundup/cgi/actions.py:607
-#: ../roundup/cgi/actions.py:778
-#: ../roundup/cgi/actions.py:797
+#: ../roundup/cgi/actions.py:608 ../roundup/cgi/actions.py:619
+#: ../roundup/cgi/actions.py:790 ../roundup/cgi/actions.py:809
 #, python-format
 msgid "Error: %s"
 msgstr "Hiba: %s"
 
-#: ../roundup/cgi/actions.py:633
+#: ../roundup/cgi/actions.py:645
 msgid ""
 "Invalid One Time Key!\n"
-"(a Mozilla bug may cause this message to show up erroneously, please check your email)"
+"(a Mozilla bug may cause this message to show up erroneously, please check "
+"your email)"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:675
+#: ../roundup/cgi/actions.py:687
 #, python-format
 msgid "Password reset and email sent to %s"
-msgstr "A jelszó törölve lett és emailt küldtünk %s-nek"
+msgstr "A jelszó törlésre került és e-mailt küldtünk %s-nek"
 
-#: ../roundup/cgi/actions.py:684
+#: ../roundup/cgi/actions.py:696
 msgid "Unknown username"
 msgstr "Ismeretlen felhasználónév"
 
-#: ../roundup/cgi/actions.py:692
+#: ../roundup/cgi/actions.py:704
 msgid "Unknown email address"
-msgstr "Ismeretlen email cím"
+msgstr "Ismeretlen e-mail cím"
 
-#: ../roundup/cgi/actions.py:697
+#: ../roundup/cgi/actions.py:709
 msgid "You need to specify a username or address"
-msgstr "Meg kell adnond egy felhasználó nevet vagy címet"
+msgstr "Meg kell adni egy felhasználónevet vagy címet"
 
-#: ../roundup/cgi/actions.py:722
+#: ../roundup/cgi/actions.py:734
 #, python-format
 msgid "Email sent to %s"
-msgstr "Email elküldve %s-nek"
+msgstr "E-mail elküldve %s-nek"
 
-#: ../roundup/cgi/actions.py:741
+#: ../roundup/cgi/actions.py:753
 msgid "You are now registered, welcome!"
-msgstr "Regisztrálva lettél, isten hozott!"
+msgstr "Regisztrálás sikeres, isten hozott!"
 
-#: ../roundup/cgi/actions.py:786
+#: ../roundup/cgi/actions.py:798
 msgid "It is not permitted to supply roles at registration."
-msgstr "Szerepkörök nem adhatók meg regisztráláskor."
+msgstr "Regisztráláskor nem adhatók meg szerepkörök."
 
-#: ../roundup/cgi/actions.py:878
+#: ../roundup/cgi/actions.py:890
 msgid "You are logged out"
-msgstr "Kijelentkeztél"
+msgstr "Kijelentkezett"
 
-#: ../roundup/cgi/actions.py:895
+#: ../roundup/cgi/actions.py:907
 msgid "Username required"
 msgstr "A felhasználónév szükséges"
 
 # ../roundup/cgi/actions.py:930 :934
-#: ../roundup/cgi/actions.py:930
-#: ../roundup/cgi/actions.py:934
+#: ../roundup/cgi/actions.py:942 ../roundup/cgi/actions.py:946
 msgid "Invalid login"
 msgstr "Hibás bejelentkezés"
 
-#: ../roundup/cgi/actions.py:940
+#: ../roundup/cgi/actions.py:952
 msgid "You do not have permission to login"
-msgstr "Nincs jogod bejelentkezni"
+msgstr "Nincs jogosultsága bejelentkezni"
 
 #: ../roundup/cgi/cgitb.py:49
 #, python-format
@@ -990,7 +1049,7 @@
 "<p><b>%(exc_type)s</b>: %(exc_value)s</p>\n"
 "<p class=\"help\">Debugging information follows</p>"
 msgstr ""
-"<h1>Template Hiba</h1>\n"
+"<h1>Sablon Hiba</h1>\n"
 "<p><b>%(exc_type)s</b>: %(exc_value)s</p>\n"
 "<p class=\"help\">Debug információk alább</p>"
 
@@ -1002,7 +1061,7 @@
 #: ../roundup/cgi/cgitb.py:67
 #, python-format
 msgid "<li>Looking for \"%(name)s\", current path:<ol>%(path)s</ol></li>"
-msgstr "<li>Keresett \"%(name)s\", aktuális elérési út:<ol>%(path)s</ol></li>"
+msgstr "<li>\"%(name)s\" keresése, aktuális elérési út:<ol>%(path)s</ol></li>"
 
 #: ../roundup/cgi/cgitb.py:71
 #, python-format
@@ -1012,7 +1071,7 @@
 #: ../roundup/cgi/cgitb.py:76
 #, python-format
 msgid "A problem occurred in your template \"%s\"."
-msgstr "Probléma merült fel a(z) \"%s\"  template-el."
+msgstr "Probléma merült fel a(z) \"%s\" sablonnal."
 
 #: ../roundup/cgi/cgitb.py:84
 #, python-format
@@ -1036,12 +1095,20 @@
 msgstr "<font size=+1><strong>%(exc_type)s</strong>: %(exc_value)s</font>"
 
 #: ../roundup/cgi/cgitb.py:120
-msgid "<p>A problem occurred while running a Python script. Here is the sequence of function calls leading up to the error, with the most recent (innermost) call first. The exception attributes are:"
-msgstr ""
+msgid ""
+"<p>A problem occurred while running a Python script. Here is the sequence of "
+"function calls leading up to the error, with the most recent (innermost) "
+"call first. The exception attributes are:"
+msgstr ""
+"<p>Probléma merült fel egy Python parancsfájl futtatása során. Alább "
+"megtekinthető a hibához vezető függvényhívások sora, a legutóbbi (legbelső) "
+"hívás látható legelőször. A kivétel tulajdonságai:"
 
 #: ../roundup/cgi/cgitb.py:129
 msgid "&lt;file is None - probably inside <tt>eval</tt> or <tt>exec</tt>&gt;"
-msgstr "&lt;file is None - probably inside <tt>eval</tt> or <tt>exec</tt>&gt;"
+msgstr ""
+"&lt;A fájl None értékű - feltehetőleg <tt>eval</tt> vagy <tt>exec</tt> "
+"utasításon belül&gt;"
 
 #: ../roundup/cgi/cgitb.py:138
 #, python-format
@@ -1049,12 +1116,11 @@
 msgstr "<strong>%s</strong>-ban"
 
 # ../roundup/cgi/cgitb.py:172 :178
-#: ../roundup/cgi/cgitb.py:172
-#: ../roundup/cgi/cgitb.py:178
+#: ../roundup/cgi/cgitb.py:172 ../roundup/cgi/cgitb.py:178
 msgid "<em>undefined</em>"
 msgstr "<em>nem definiált</em>"
 
-#: ../roundup/cgi/client.py:49
+#: ../roundup/cgi/client.py:51
 msgid ""
 "<html><head><title>An error has occurred</title></head>\n"
 "<body><h1>An error has occurred</h1>\n"
@@ -1062,331 +1128,405 @@
 "The tracker maintainers have been notified of the problem.</p>\n"
 "</body></html>"
 msgstr ""
-"<html><head><title>Hibat történt</title></head>\n"
+"<html><head><title>Hiba történt</title></head>\n"
 "<body><h1>Hiba történt</h1>\n"
-"<p>Probléma merült fel kérésének feldolgozása közben.\n"
-"A hibakövető karbantartóit értesítést kaptak a problémáról.</p>\n"
+"<p>Probléma merült fel a kérés feldolgozása közben.\n"
+"A hibakövető karbantartói értesítést kaptak a problémáról.</p>\n"
 "</body></html>"
 
-#: ../roundup/cgi/client.py:308
+#: ../roundup/cgi/client.py:377
 msgid "Form Error: "
-msgstr "Å°rlap hiba:"
+msgstr "Å°rlap hiba: "
 
-#: ../roundup/cgi/client.py:363
+#: ../roundup/cgi/client.py:432
 #, python-format
 msgid "Unrecognized charset: %r"
 msgstr "Ismeretlen karakterkészlet: %r"
 
-#: ../roundup/cgi/client.py:491
+#: ../roundup/cgi/client.py:560
 msgid "Anonymous users are not allowed to use the web interface"
 msgstr "Anonim felhasználók nem használhatják a webes felületet"
 
-#: ../roundup/cgi/client.py:646
+#: ../roundup/cgi/client.py:715
 msgid "You are not allowed to view this file."
-msgstr "Nem nézheted meg ezt a fájlt."
+msgstr "Nem nézheti meg ezt a fájlt."
 
-#: ../roundup/cgi/client.py:738
+#: ../roundup/cgi/client.py:808
 #, python-format
 msgid "%(starttag)sTime elapsed: %(seconds)fs%(endtag)s\n"
 msgstr "%(starttag)sEltelt idő: %(seconds)fs%(endtag)s\n"
 
-#: ../roundup/cgi/client.py:742
+#: ../roundup/cgi/client.py:812
 #, python-format
-msgid "%(starttag)sCache hits: %(cache_hits)d, misses %(cache_misses)d. Loading items: %(get_items)f secs. Filtering: %(filtering)f secs.%(endtag)s\n"
-msgstr "%(starttag)sCache találatok: %(cache_hits)d, tévedés %(cache_misses)d. Elemek betöltése: %(get_items)f mp. Szűrés: %(filtering)f mp.%(endtag)s\n"
+msgid ""
+"%(starttag)sCache hits: %(cache_hits)d, misses %(cache_misses)d. Loading "
+"items: %(get_items)f secs. Filtering: %(filtering)f secs.%(endtag)s\n"
+msgstr ""
+"%(starttag)sCache találatok: %(cache_hits)d, tévedés %(cache_misses)d. "
+"Elemek betöltése: %(get_items)f mp. Szűrés: %(filtering)f mp.%(endtag)s\n"
 
 #: ../roundup/cgi/form_parser.py:283
-#, python-format
-msgid "link \"%(key)s\" value \"%(value)s\" not a designator"
-msgstr ""
+#, fuzzy, python-format
+msgid "link \"%(key)s\" value \"%(entry)s\" not a designator"
+msgstr "A(z) \"%(value)s\" értékű \"%(key)s\" csatolás nem teljes név"
 
-#: ../roundup/cgi/form_parser.py:290
+#: ../roundup/cgi/form_parser.py:301
 #, python-format
 msgid "%(class)s %(property)s is not a link or multilink property"
+msgstr "A(y) %(class)s %(property)s nem link vagy multilink típusú tulajdonság"
+
+#: ../roundup/cgi/form_parser.py:313
+#, fuzzy, python-format
+msgid ""
+"The form action claims to require property \"%(property)s\" which doesn't "
+"exist"
 msgstr ""
+"%(action)s műveletet kíván a \"%(property)s\" tulajdonságon végezni, de az "
+"nem létezik"
 
-#: ../roundup/cgi/form_parser.py:312
+#: ../roundup/cgi/form_parser.py:335
 #, python-format
-msgid "You have submitted a %(action)s action for the property \"%(property)s\" which doesn't exist"
+msgid ""
+"You have submitted a %(action)s action for the property \"%(property)s\" "
+"which doesn't exist"
 msgstr ""
+"%(action)s műveletet kíván a \"%(property)s\" tulajdonságon végezni, de az "
+"nem létezik"
 
 # ../roundup/cgi/form_parser.py:331 :357
-#: ../roundup/cgi/form_parser.py:331
-#: ../roundup/cgi/form_parser.py:357
+#: ../roundup/cgi/form_parser.py:354 ../roundup/cgi/form_parser.py:380
 #, python-format
 msgid "You have submitted more than one value for the %s property"
-msgstr ""
+msgstr "Egynél több értéket adott meg a(z) %s tulajdonsághoz"
 
 # ../roundup/cgi/form_parser.py:354 :360
-#: ../roundup/cgi/form_parser.py:354
-#: ../roundup/cgi/form_parser.py:360
+#: ../roundup/cgi/form_parser.py:377 ../roundup/cgi/form_parser.py:383
 msgid "Password and confirmation text do not match"
-msgstr ""
+msgstr "A jelszó és a megerősítés nem egyezik"
 
-#: ../roundup/cgi/form_parser.py:395
+#: ../roundup/cgi/form_parser.py:418
 #, python-format
 msgid "property \"%(propname)s\": \"%(value)s\" not currently in list"
-msgstr ""
+msgstr "\"%(propname)s\" tulajdonság: \"%(value)s\" jelenleg nincs a listában"
 
-#: ../roundup/cgi/form_parser.py:512
+#: ../roundup/cgi/form_parser.py:551
 #, python-format
 msgid "Required %(class)s property %(property)s not supplied"
 msgid_plural "Required %(class)s properties %(property)s not supplied"
-msgstr[0] ""
+msgstr[0] "Nincs megadva a(z) %(class)s kötelező %(property)s tulajdonsága"
 msgstr[1] ""
+"Nincsenek megadva a(z) %(class)s kötelező %(property)s tulajdonságai"
 
-#: ../roundup/cgi/form_parser.py:535
+#: ../roundup/cgi/form_parser.py:574
 msgid "File is empty"
-msgstr ""
+msgstr "A fájl üres"
 
-#: ../roundup/cgi/templating.py:72
+#: ../roundup/cgi/templating.py:77
 #, python-format
 msgid "You are not allowed to %(action)s items of class %(class)s"
 msgstr ""
+"Nincs jogosultsága a(z) %(class)s osztály elemein %(action)s műveletet "
+"végrehajtani"
 
-#: ../roundup/cgi/templating.py:627
+#: ../roundup/cgi/templating.py:657
 msgid "(list)"
-msgstr ""
+msgstr "(lista)"
 
-#: ../roundup/cgi/templating.py:696
+#: ../roundup/cgi/templating.py:726
 msgid "Submit New Entry"
-msgstr ""
+msgstr "Létrehozás"
 
 # ../roundup/cgi/templating.py:710 :829 :1236 :1257 :1304 :1327 :1361 :1400
 # :1453 :1470 :1549 :1569 :1587 :1619 :1629 :1683 :1875
-#: ../roundup/cgi/templating.py:710
-#: ../roundup/cgi/templating.py:829
-#: ../roundup/cgi/templating.py:1236
-#: ../roundup/cgi/templating.py:1257
-#: ../roundup/cgi/templating.py:1304
-#: ../roundup/cgi/templating.py:1327
-#: ../roundup/cgi/templating.py:1361
-#: ../roundup/cgi/templating.py:1400
-#: ../roundup/cgi/templating.py:1453
-#: ../roundup/cgi/templating.py:1470
-#: ../roundup/cgi/templating.py:1549
-#: ../roundup/cgi/templating.py:1569
-#: ../roundup/cgi/templating.py:1587
-#: ../roundup/cgi/templating.py:1619
-#: ../roundup/cgi/templating.py:1629
-#: ../roundup/cgi/templating.py:1683
-#: ../roundup/cgi/templating.py:1875
+#: ../roundup/cgi/templating.py:740 ../roundup/cgi/templating.py:873
+#: ../roundup/cgi/templating.py:1294 ../roundup/cgi/templating.py:1323
+#: ../roundup/cgi/templating.py:1343 ../roundup/cgi/templating.py:1356
+#: ../roundup/cgi/templating.py:1407 ../roundup/cgi/templating.py:1430
+#: ../roundup/cgi/templating.py:1466 ../roundup/cgi/templating.py:1503
+#: ../roundup/cgi/templating.py:1556 ../roundup/cgi/templating.py:1573
+#: ../roundup/cgi/templating.py:1657 ../roundup/cgi/templating.py:1677
+#: ../roundup/cgi/templating.py:1695 ../roundup/cgi/templating.py:1727
+#: ../roundup/cgi/templating.py:1737 ../roundup/cgi/templating.py:1789
+#: ../roundup/cgi/templating.py:1978
 msgid "[hidden]"
-msgstr ""
+msgstr "[rejtett]"
 
-#: ../roundup/cgi/templating.py:711
+#: ../roundup/cgi/templating.py:741
 msgid "New node - no history"
-msgstr ""
+msgstr "Új bejegyzés - nincs történet"
 
-#: ../roundup/cgi/templating.py:811
+#: ../roundup/cgi/templating.py:855
 msgid "Submit Changes"
-msgstr ""
+msgstr "Változások mentése"
 
-#: ../roundup/cgi/templating.py:893
+#: ../roundup/cgi/templating.py:937
 msgid "<em>The indicated property no longer exists</em>"
-msgstr ""
+msgstr "<em>A jelzett tulajdonság már nem létezik</em>"
 
-#: ../roundup/cgi/templating.py:894
+#: ../roundup/cgi/templating.py:938
 #, python-format
 msgid "<em>%s: %s</em>\n"
-msgstr ""
+msgstr "<em>%s: %s</em>\n"
 
-#: ../roundup/cgi/templating.py:907
+#: ../roundup/cgi/templating.py:951
 #, python-format
 msgid "The linked class %(classname)s no longer exists"
-msgstr ""
+msgstr "A csatolt %(classname)s osztály már nem létezik"
 
 # ../roundup/cgi/templating.py:940 :964
-#: ../roundup/cgi/templating.py:940
-#: ../roundup/cgi/templating.py:964
+#: ../roundup/cgi/templating.py:984 ../roundup/cgi/templating.py:1008
 msgid "<strike>The linked node no longer exists</strike>"
-msgstr ""
-
-# ../roundup/cgi/templating.py:1006 :1404 :1425 :1431
-#: ../roundup/cgi/templating.py:1006
-#: ../roundup/cgi/templating.py:1404
-#: ../roundup/cgi/templating.py:1425
-#: ../roundup/cgi/templating.py:1431
-msgid "No"
-msgstr "Nem"
+msgstr "<strike>A csatolt bejegyzés már nem létezik</strike>"
 
-# ../roundup/cgi/templating.py:1006 :1404 :1423 :1428
-#: ../roundup/cgi/templating.py:1006
-#: ../roundup/cgi/templating.py:1404
-#: ../roundup/cgi/templating.py:1423
-#: ../roundup/cgi/templating.py:1428
-msgid "Yes"
-msgstr "Igen"
-
-#: ../roundup/cgi/templating.py:1017
+#: ../roundup/cgi/templating.py:1061
 #, python-format
 msgid "%s: (no value)"
-msgstr ""
+msgstr "%s: (nincs érték)"
 
-#: ../roundup/cgi/templating.py:1029
-msgid "<strong><em>This event is not handled by the history display!</em></strong>"
+#: ../roundup/cgi/templating.py:1073
+msgid ""
+"<strong><em>This event is not handled by the history display!</em></strong>"
 msgstr ""
+"<strong><em>Az előzmények képernyő nem kezeli ezt az eseményt!</em></strong>"
 
-#: ../roundup/cgi/templating.py:1041
+#: ../roundup/cgi/templating.py:1085
 msgid "<tr><td colspan=4><strong>Note:</strong></td></tr>"
-msgstr ""
+msgstr "<tr><td colspan=4><strong>Megjegyzés:</strong></td></tr>"
 
-#: ../roundup/cgi/templating.py:1050
+#: ../roundup/cgi/templating.py:1094
 msgid "History"
-msgstr ""
+msgstr "Előzmények"
 
-#: ../roundup/cgi/templating.py:1052
+#: ../roundup/cgi/templating.py:1096
 msgid "<th>Date</th>"
-msgstr ""
+msgstr "<th>Dátum</th>"
 
-#: ../roundup/cgi/templating.py:1053
+#: ../roundup/cgi/templating.py:1097
 msgid "<th>User</th>"
-msgstr ""
+msgstr "<th>Szerző</th>"
 
-#: ../roundup/cgi/templating.py:1054
+#: ../roundup/cgi/templating.py:1098
 msgid "<th>Action</th>"
-msgstr ""
+msgstr "<th>Művelet</th>"
 
-#: ../roundup/cgi/templating.py:1055
+#: ../roundup/cgi/templating.py:1099
 msgid "<th>Args</th>"
-msgstr ""
+msgstr "<th>Tulajdonságok</th>"
 
-#: ../roundup/cgi/templating.py:1097
+#: ../roundup/cgi/templating.py:1141
 #, python-format
 msgid "Copy of %(class)s %(id)s"
-msgstr ""
+msgstr "A(z) %(class)s %(id)s másolata"
 
-#: ../roundup/cgi/templating.py:1331
+#: ../roundup/cgi/templating.py:1434
 msgid "*encrypted*"
-msgstr ""
+msgstr "*titkosítva*"
 
-#: ../roundup/cgi/templating.py:1514
-msgid "default value for DateHTMLProperty must be either DateHTMLProperty or string date representation."
+# ../roundup/cgi/templating.py:1006 :1404 :1425 :1431
+#: ../roundup/cgi/templating.py:1507 ../roundup/cgi/templating.py:1528
+#: ../roundup/cgi/templating.py:1534 ../roundup/cgi/templating.py:1050
+msgid "No"
+msgstr "Nem"
+
+# ../roundup/cgi/templating.py:1006 :1404 :1423 :1428
+#: ../roundup/cgi/templating.py:1507 ../roundup/cgi/templating.py:1526
+#: ../roundup/cgi/templating.py:1531 ../roundup/cgi/templating.py:1050
+msgid "Yes"
+msgstr "Igen"
+
+#: ../roundup/cgi/templating.py:1620
+msgid ""
+"default value for DateHTMLProperty must be either DateHTMLProperty or string "
+"date representation."
 msgstr ""
+"a DateHTMLProperty alapértéke DateHTMLProperty vagy szöveges dátumleírás "
+"típusú kell legyen."
 
-#: ../roundup/cgi/templating.py:1674
+#: ../roundup/cgi/templating.py:1780
 #, python-format
 msgid "Attempt to look up %(attr)s on a missing value"
-msgstr ""
+msgstr "Kísérlet %(attr)s keresésére egy hiányzó értéken"
 
-#: ../roundup/cgi/templating.py:1750
+#: ../roundup/cgi/templating.py:1853
 #, python-format
 msgid "<option %svalue=\"-1\">- no selection -</option>"
-msgstr ""
+msgstr "<option %svalue=\"-1\">- nincs kiválasztás -</option>"
 
-#: ../roundup/date.py:186
-msgid "Not a date spec: \"yyyy-mm-dd\", \"mm-dd\", \"HH:MM\", \"HH:MM:SS\" or \"yyyy-mm-dd.HH:MM:SS.SSS\""
+#: ../roundup/date.py:300
+msgid ""
+"Not a date spec: \"yyyy-mm-dd\", \"mm-dd\", \"HH:MM\", \"HH:MM:SS\" or "
+"\"yyyy-mm-dd.HH:MM:SS.SSS\""
 msgstr ""
+"Nem dátum specifikáció: \"éééé-hh-nn\", \"hh-nn\", \"ÓÓ:PP\", \"ÓÓ:PP:SS\" "
+"vagy \"éééé-hh-nn.ÓÓ:PP:SS.SSS\""
 
-#: ../roundup/date.py:240
+#: ../roundup/date.py:359
 #, python-format
-msgid "%r not a date / time spec \"yyyy-mm-dd\", \"mm-dd\", \"HH:MM\", \"HH:MM:SS\" or \"yyyy-mm-dd.HH:MM:SS.SSS\""
+msgid ""
+"%r not a date / time spec \"yyyy-mm-dd\", \"mm-dd\", \"HH:MM\", \"HH:MM:SS\" "
+"or \"yyyy-mm-dd.HH:MM:SS.SSS\""
 msgstr ""
+"%r nem dátum / idő specifikáció \"éééé-hh-nn\", \"hh-nn\", \"ÓÓ:PP\", \"ÓÓ:"
+"PP:SS\" vagy \"éééé-hh-nn.ÓÓ:PP:SS.SSS\""
 
-#: ../roundup/date.py:538
-msgid "Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [date spec]"
+#: ../roundup/date.py:666
+msgid ""
+"Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [date spec]"
 msgstr ""
+"Nem időköz specifikáció: [+-] [#é] [#h] [#w] [#n] [[[Ó]Ó:PP]:SS] [dátum "
+"típus]"
 
-#: ../roundup/date.py:557
+#: ../roundup/date.py:685
 msgid "Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]"
-msgstr ""
+msgstr "Nem időköz specifikáció: [+-] [#é] [#h] [#w] [#n] [[[Ó]Ó:PP]:SS]"
 
-#: ../roundup/date.py:694
+#: ../roundup/date.py:822
 #, python-format
 msgid "%(number)s year"
 msgid_plural "%(number)s years"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%(number)s éve"
+msgstr[1] "%(number)s éve"
 
-#: ../roundup/date.py:698
+#: ../roundup/date.py:826
 #, python-format
 msgid "%(number)s month"
 msgid_plural "%(number)s months"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%(number)s hónapja"
+msgstr[1] "%(number)s hónapja"
 
-#: ../roundup/date.py:702
+#: ../roundup/date.py:830
 #, python-format
 msgid "%(number)s week"
 msgid_plural "%(number)s weeks"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%(number)s hete"
+msgstr[1] "%(number)s hete"
 
-#: ../roundup/date.py:706
+#: ../roundup/date.py:834
 #, python-format
 msgid "%(number)s day"
 msgid_plural "%(number)s days"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%(number)s napja"
+msgstr[1] "%(number)s napja"
 
-#: ../roundup/date.py:710
+#: ../roundup/date.py:838
 msgid "tomorrow"
-msgstr ""
+msgstr "holnap"
 
-#: ../roundup/date.py:712
+#: ../roundup/date.py:840
 msgid "yesterday"
-msgstr ""
+msgstr "tegnap"
 
-#: ../roundup/date.py:715
+#: ../roundup/date.py:843
 #, python-format
 msgid "%(number)s hour"
 msgid_plural "%(number)s hours"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%(number)s órája"
+msgstr[1] "%(number)s órája"
 
-#: ../roundup/date.py:719
+#: ../roundup/date.py:847
 msgid "an hour"
-msgstr ""
+msgstr "egy órája"
 
-#: ../roundup/date.py:721
+#: ../roundup/date.py:849
 msgid "1 1/2 hours"
-msgstr ""
+msgstr "1 1/2 órája"
 
-#: ../roundup/date.py:723
+#: ../roundup/date.py:851
 #, python-format
 msgid "1 %(number)s/4 hours"
 msgid_plural "1 %(number)s/4 hours"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "1 %(number)s/4 órája"
+msgstr[1] "1 %(number)s/4 órája"
 
-#: ../roundup/date.py:727
+#: ../roundup/date.py:855
 msgid "in a moment"
-msgstr ""
+msgstr "egy pillanat"
 
-#: ../roundup/date.py:729
+#: ../roundup/date.py:857
 msgid "just now"
-msgstr ""
+msgstr "épp most"
 
-#: ../roundup/date.py:732
+#: ../roundup/date.py:860
 msgid "1 minute"
-msgstr ""
+msgstr "1 perce"
 
-#: ../roundup/date.py:735
+#: ../roundup/date.py:863
 #, python-format
 msgid "%(number)s minute"
 msgid_plural "%(number)s minutes"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%(number)s perce"
+msgstr[1] "%(number)s perce"
 
-#: ../roundup/date.py:738
+#: ../roundup/date.py:866
 msgid "1/2 an hour"
-msgstr ""
+msgstr "1/2 órája"
 
-#: ../roundup/date.py:740
+#: ../roundup/date.py:868
 #, python-format
 msgid "%(number)s/4 hour"
 msgid_plural "%(number)s/4 hours"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%(number)s/4 órája"
+msgstr[1] "%(number)s/4 órája"
 
-#: ../roundup/date.py:744
+#: ../roundup/date.py:872
 #, python-format
 msgid "%s ago"
-msgstr ""
+msgstr "%s"
 
-#: ../roundup/date.py:746
+#: ../roundup/date.py:874
 #, python-format
 msgid "in %s"
+msgstr "%s-ban"
+
+#: ../roundup/hyperdb.py:87
+#, fuzzy, python-format
+msgid "property %s: %s"
+msgstr "Hiba: %s: %s"
+
+#: ../roundup/hyperdb.py:107
+#, python-format
+msgid "property %s: %r is an invalid date (%s)"
+msgstr ""
+
+#: ../roundup/hyperdb.py:124
+#, python-format
+msgid "property %s: %r is an invalid date interval (%s)"
+msgstr ""
+
+#: ../roundup/hyperdb.py:219
+#, fuzzy, python-format
+msgid "property %s: %r is not currently an element"
+msgstr "\"%(propname)s\" tulajdonság: \"%(value)s\" jelenleg nincs a listában"
+
+#: ../roundup/hyperdb.py:263
+#, python-format
+msgid "property %s: %r is not a number"
+msgstr ""
+
+#: ../roundup/hyperdb.py:276
+#, python-format
+msgid "\"%s\" not a node designator"
+msgstr ""
+
+# ../roundup/hyperdb.py:949:957
+#: ../roundup/hyperdb.py:949 ../roundup/hyperdb.py:957
+#, python-format
+msgid "Not a property name: %s"
+msgstr ""
+
+#: ../roundup/hyperdb.py:1240
+#, python-format
+msgid "property %s: %r is not a %s."
+msgstr ""
+
+#: ../roundup/hyperdb.py:1243
+#, python-format
+msgid "you may only enter ID values for property %s"
+msgstr ""
+
+#: ../roundup/hyperdb.py:1273
+#, python-format
+msgid "%r is not a property of %s"
 msgstr ""
 
 #: ../roundup/init.py:134
@@ -1395,14 +1535,51 @@
 "WARNING: directory '%s'\n"
 "\tcontains old-style template - ignored"
 msgstr ""
+"FIGYELEM: a(z) '%s' könyvtár\n"
+"\trégi típusú sablont tartalmaz - ignorálva"
+
+# ../roundup/mailgw.py:199:211
+#: ../roundup/mailgw.py:199 ../roundup/mailgw.py:211
+#, python-format
+msgid "Message signed with unknown key: %s"
+msgstr ""
+
+#: ../roundup/mailgw.py:202
+#, python-format
+msgid "Message signed with an expired key: %s"
+msgstr ""
+
+#: ../roundup/mailgw.py:205
+#, python-format
+msgid "Message signed with a revoked key: %s"
+msgstr ""
+
+#: ../roundup/mailgw.py:208
+msgid "Invalid PGP signature detected."
+msgstr ""
 
-#: ../roundup/mailgw.py:586
+#: ../roundup/mailgw.py:404
+msgid "Unknown multipart/encrypted version."
+msgstr ""
+
+#: ../roundup/mailgw.py:413
+msgid "Unable to decrypt your message."
+msgstr ""
+
+#: ../roundup/mailgw.py:442
+msgid "No PGP signature found in message."
+msgstr ""
+
+#: ../roundup/mailgw.py:749
 msgid ""
 "\n"
 "Emails to Roundup trackers must include a Subject: line!\n"
 msgstr ""
+"\n"
+"A Roundup hibakövetőkhöz küldött e-maileknek tartalmazniuk kell egy Subject: "
+"sort!\n"
 
-#: ../roundup/mailgw.py:674
+#: ../roundup/mailgw.py:873
 #, python-format
 msgid ""
 "\n"
@@ -1419,29 +1596,59 @@
 "Subject was: '%(subject)s'\n"
 msgstr ""
 
-#: ../roundup/mailgw.py:705
-#, python-format
+#: ../roundup/mailgw.py:911
+#, fuzzy, python-format
 msgid ""
 "\n"
-"The class name you identified in the subject line (\"%(classname)s\") does not exist in the\n"
-"database.\n"
+"The class name you identified in the subject line (\"%(classname)s\") does\n"
+"not exist in the database.\n"
 "\n"
 "Valid class names are: %(validname)s\n"
 "Subject was: \"%(subject)s\"\n"
 msgstr ""
+"\n"
+"A tárgy sorban megadott osztály neve (\"%(classname)s\") nem létezik\n"
+"az adatbázisban.\n"
+"\n"
+"Az érvényes osztálynevek: %(validname)s\n"
+"A tárgy ez volt: \"%(subject)s\"\n"
 
-#: ../roundup/mailgw.py:733
+#: ../roundup/mailgw.py:919
 #, python-format
 msgid ""
 "\n"
+"You did not identify a class name in the subject line and there is no\n"
+"default set for this tracker. The subject must contain a class name or\n"
+"designator to indicate the 'topic' of the message. For example:\n"
+"    Subject: [issue] This is a new issue\n"
+"      - this will create a new issue in the tracker with the title 'This is\n"
+"        a new issue'.\n"
+"    Subject: [issue1234] This is a followup to issue 1234\n"
+"      - this will append the message's contents to the existing issue 1234\n"
+"        in the tracker.\n"
+"\n"
+"Subject was: '%(subject)s'\n"
+msgstr ""
+
+#: ../roundup/mailgw.py:960
+#, fuzzy, python-format
+msgid ""
+"\n"
 "I cannot match your message to a node in the database - you need to either\n"
-"supply a full designator (with number, eg \"[issue123]\" or keep the\n"
+"supply a full designator (with number, eg \"[issue123]\") or keep the\n"
 "previous subject title intact so I can match that.\n"
 "\n"
 "Subject was: \"%(subject)s\"\n"
 msgstr ""
+"\n"
+"Nem sikerült az üzenetet párosítani egy adatbázisban szereplő ággal - vagy "
+"meg kell\n"
+"adnia egy teljes nevet (számmal együtt, pl. \"[issue123]\"vagy meg kell\n"
+"tartania a teljes előző címet, hogy ahhoz lehessen párosítani.\n"
+"\n"
+"A tárgy ez volt: \"%(subject)s\"\n"
 
-#: ../roundup/mailgw.py:766
+#: ../roundup/mailgw.py:993
 #, python-format
 msgid ""
 "\n"
@@ -1450,8 +1657,13 @@
 "\n"
 "Subject was: \"%(subject)s\"\n"
 msgstr ""
+"\n"
+"Az üzenet tárgysorában megadott ág\n"
+"(\"%(nodeid)s\") nem létezik.\n"
+"\n"
+"A tárgy ez volt: \"%(subject)s\"\n"
 
-#: ../roundup/mailgw.py:794
+#: ../roundup/mailgw.py:1021
 #, python-format
 msgid ""
 "\n"
@@ -1459,8 +1671,12 @@
 "%(mailadmin)s and have them fix the incorrect class specified as:\n"
 "  %(current_class)s\n"
 msgstr ""
+"\n"
+"A mail átjáró nincs helyesen beállítva. Vegye fel a kapcsolatot\n"
+"%(mailadmin)s-nal és javíttassa ki a hibásan megadott osztályt:\n"
+"  %(current_class)s\n"
 
-#: ../roundup/mailgw.py:817
+#: ../roundup/mailgw.py:1044
 #, python-format
 msgid ""
 "\n"
@@ -1468,31 +1684,39 @@
 "%(mailadmin)s and have them fix the incorrect properties:\n"
 "  %(errors)s\n"
 msgstr ""
+"\n"
+"A mail átjáró nincs helyesen beállítva. Vegye fel a kapcsolatot\n"
+"%(mailadmin)s-nal és javíttassa ki a hibás tulajdonságokat:\n"
+"  %(errors)s\n"
 
-#: ../roundup/mailgw.py:847
-#, python-format
+#: ../roundup/mailgw.py:1084
+#, fuzzy, python-format
 msgid ""
 "\n"
-"You are not a registered user.\n"
+"You are not a registered user.%(registration_info)s\n"
 "\n"
 "Unknown address: %(from_address)s\n"
 msgstr ""
+"\n"
+"Ön nem bejegyzett felhasználó.\n"
+"\n"
+"Ismeretlen cím: %(from_address)s\n"
 
-#: ../roundup/mailgw.py:855
+#: ../roundup/mailgw.py:1092
 msgid "You are not permitted to access this tracker."
-msgstr ""
+msgstr "Ehhez a hibakövetőhöz hozzáférése nem engedélyezett."
 
-#: ../roundup/mailgw.py:862
+#: ../roundup/mailgw.py:1099
 #, python-format
 msgid "You are not permitted to edit %(classname)s."
-msgstr ""
+msgstr "Nincs jogosultsága %(classname)s szerkesztéséhez."
 
-#: ../roundup/mailgw.py:866
+#: ../roundup/mailgw.py:1103
 #, python-format
 msgid "You are not permitted to create %(classname)s."
-msgstr ""
+msgstr "Nincs jogosultsága %(classname)s létrehozásához."
 
-#: ../roundup/mailgw.py:913
+#: ../roundup/mailgw.py:1150
 #, python-format
 msgid ""
 "\n"
@@ -1501,148 +1725,189 @@
 "\n"
 "Subject was: \"%(subject)s\"\n"
 msgstr ""
+"\n"
+"Probléma merült fel a tárgysor argumentum listájának feldolgozása során:\n"
+"- %(errors)s\n"
+"\n"
+"A tárgy ez volt: \"%(subject)s\"\n"
 
-#: ../roundup/mailgw.py:942
+#: ../roundup/mailgw.py:1203
+msgid ""
+"\n"
+"This tracker has been configured to require all email be PGP signed or\n"
+"encrypted."
+msgstr ""
+
+#: ../roundup/mailgw.py:1209
 msgid ""
 "\n"
 "Roundup requires the submission to be plain text. The message parser could\n"
 "not find a text/plain part to use.\n"
 msgstr ""
+"\n"
+"A Roundup egyszerű szövegként tudja fogadni a kérelmet. Az üzenet értelmező\n"
+"nem talált használható, egyszerű szöveg formátumú részt.\n"
 
-#: ../roundup/mailgw.py:964
+#: ../roundup/mailgw.py:1226
 msgid "You are not permitted to create files."
-msgstr ""
+msgstr "Nincs jogosultsága fájlok létrehozására."
 
-#: ../roundup/mailgw.py:978
+#: ../roundup/mailgw.py:1240
 #, python-format
 msgid "You are not permitted to add files to %(classname)s."
-msgstr ""
+msgstr "Nincs jogosultsága fájlok hozzáadására %(classname)s-hez."
 
-#: ../roundup/mailgw.py:996
+#: ../roundup/mailgw.py:1258
 msgid "You are not permitted to create messages."
-msgstr ""
+msgstr "Nincs jogosultsága üzenetek létrehozására."
 
-#: ../roundup/mailgw.py:1004
+#: ../roundup/mailgw.py:1266
 #, python-format
 msgid ""
 "\n"
 "Mail message was rejected by a detector.\n"
 "%(error)s\n"
 msgstr ""
+"\n"
+"A mail üzenetet a felderítő visszutasította.\n"
+"%(error)s\n"
 
-#: ../roundup/mailgw.py:1012
+#: ../roundup/mailgw.py:1274
 #, python-format
 msgid "You are not permitted to add messages to %(classname)s."
-msgstr ""
+msgstr "Nincs jogosultsága üzenet hozzáadására %(classname)s-hez."
 
-#: ../roundup/mailgw.py:1039
+#: ../roundup/mailgw.py:1301
 #, python-format
 msgid "You are not permitted to edit property %(prop)s of class %(classname)s."
 msgstr ""
+"Nincs jogosultsága %(classname)s osztály %(prop)s tulajdonságát szerkeszteni."
 
-#: ../roundup/mailgw.py:1047
+#: ../roundup/mailgw.py:1309
 #, python-format
 msgid ""
 "\n"
 "There was a problem with the message you sent:\n"
 "   %(message)s\n"
 msgstr ""
+"\n"
+"Probléma volt az Ön által küldött üzenettel:\n"
+"   %(message)s\n"
 
-#: ../roundup/mailgw.py:1069
+#: ../roundup/mailgw.py:1331
 msgid "not of form [arg=value,value,...;arg=value,value,...]"
-msgstr ""
+msgstr "nem [arg=érték,érték,...;arg=érték,érték,...] formátumú"
 
-#: ../roundup/roundupdb.py:142
+#: ../roundup/roundupdb.py:147
 msgid "files"
-msgstr ""
+msgstr "fájlok"
 
-#: ../roundup/roundupdb.py:142
+#: ../roundup/roundupdb.py:147
 msgid "messages"
-msgstr ""
+msgstr "üzenetek"
 
-#: ../roundup/roundupdb.py:142
+#: ../roundup/roundupdb.py:147
 msgid "nosy"
-msgstr ""
+msgstr "kíváncsi"
 
-#: ../roundup/roundupdb.py:142
+#: ../roundup/roundupdb.py:147
 msgid "superseder"
-msgstr ""
+msgstr "helyettes"
 
-#: ../roundup/roundupdb.py:142
+#: ../roundup/roundupdb.py:147
 msgid "title"
-msgstr ""
+msgstr "cím"
 
-#: ../roundup/roundupdb.py:143
+#: ../roundup/roundupdb.py:148
 msgid "assignedto"
-msgstr ""
+msgstr "kiosztva"
 
-#: ../roundup/roundupdb.py:143
+#: ../roundup/roundupdb.py:148
+#, fuzzy
+msgid "keyword"
+msgstr "Téma"
+
+#: ../roundup/roundupdb.py:148
 msgid "priority"
-msgstr ""
+msgstr "prioritás"
 
-#: ../roundup/roundupdb.py:143
+#: ../roundup/roundupdb.py:148
 msgid "status"
-msgstr ""
-
-#: ../roundup/roundupdb.py:143
-msgid "topic"
-msgstr "téma"
+msgstr "állapot"
 
-#: ../roundup/roundupdb.py:146
+#: ../roundup/roundupdb.py:151
 msgid "activity"
-msgstr ""
+msgstr "művelet"
 
 #. following properties are common for all hyperdb classes
 #. they are listed here to keep things in one place
-#: ../roundup/roundupdb.py:146
+#: ../roundup/roundupdb.py:151
 msgid "actor"
-msgstr ""
+msgstr "végezte"
 
-#: ../roundup/roundupdb.py:146
+#: ../roundup/roundupdb.py:151
 msgid "creation"
-msgstr ""
+msgstr "létrehozás"
 
-#: ../roundup/roundupdb.py:146
+#: ../roundup/roundupdb.py:151
 msgid "creator"
-msgstr ""
+msgstr "létrehozó"
 
-#: ../roundup/roundupdb.py:304
+#: ../roundup/roundupdb.py:309
 #, python-format
 msgid "New submission from %(authname)s%(authaddr)s:"
-msgstr ""
+msgstr "Új beadvány %(authname)s%(authaddr)s részéről:"
 
-#: ../roundup/roundupdb.py:307
+#: ../roundup/roundupdb.py:312
 #, python-format
 msgid "%(authname)s%(authaddr)s added the comment:"
+msgstr "%(authname)s%(authaddr)s ezt a megjegyzést írta:"
+
+#: ../roundup/roundupdb.py:315
+#, fuzzy, python-format
+msgid "Change by %(authname)s%(authaddr)s:"
+msgstr "Új beadvány %(authname)s%(authaddr)s részéről:"
+
+#: ../roundup/roundupdb.py:342
+#, python-format
+msgid "File '%(filename)s' not attached - you can download it from %(link)s."
 msgstr ""
 
-#: ../roundup/roundupdb.py:310
-msgid "System message:"
+#: ../roundup/roundupdb.py:615
+#, python-format
+msgid ""
+"\n"
+"Now:\n"
+"%(new)s\n"
+"Was:\n"
+"%(old)s"
 msgstr ""
 
 #: ../roundup/scripts/roundup_demo.py:32
 #, python-format
 msgid "Enter directory path to create demo tracker [%s]: "
-msgstr ""
+msgstr "Adja meg az elérési utat a bemutató tracker [%s] létrehozásához: "
 
 #: ../roundup/scripts/roundup_gettext.py:22
 #, python-format
 msgid "Usage: %(program)s <tracker home>"
-msgstr ""
+msgstr "Használat: %(program)s <tracker elérési út>"
 
 #: ../roundup/scripts/roundup_gettext.py:37
 #, python-format
 msgid "No tracker templates found in directory %s"
-msgstr ""
+msgstr "Nem található tracker sablon a(z) %s könyvtárban"
 
 #: ../roundup/scripts/roundup_mailgw.py:36
 #, python-format
 msgid ""
-"Usage: %(program)s [-v] [-c] [[-C class] -S field=value]* <instance home> [method]\n"
+"Usage: %(program)s [-v] [-c class] [[-C class] -S field=value]* <instance "
+"home> [method]\n"
 "\n"
 "Options:\n"
 " -v: print version and exit\n"
-" -c: default class of item to create (else the tracker's MAIL_DEFAULT_CLASS)\n"
+" -c: default class of item to create (else the tracker's "
+"MAIL_DEFAULT_CLASS)\n"
 " -C / -S: see below\n"
 "\n"
 "The roundup mail gateway may be called in one of four ways:\n"
@@ -1683,6 +1948,10 @@
 " are both valid. The username and/or password will be prompted for if\n"
 " not supplied on the command-line.\n"
 "\n"
+"POPS:\n"
+" Connect to a POP server over ssl. This requires python 2.4 or later.\n"
+" This supports the same notation as POP.\n"
+"\n"
 "APOP:\n"
 " Same as POP, but using Authenticated POP:\n"
 "    apop username:password at server\n"
@@ -1702,23 +1971,35 @@
 "\n"
 msgstr ""
 
-#: ../roundup/scripts/roundup_mailgw.py:147
+#: ../roundup/scripts/roundup_mailgw.py:151
 msgid "Error: not enough source specification information"
-msgstr "Hiba: nincs elég forrás specifikációs informácó"
+msgstr "Hiba: nincs elég forrás specifikációs információ"
 
-#: ../roundup/scripts/roundup_mailgw.py:163
-msgid "Error: pop specification not valid"
-msgstr "Hiba: a pop specifikáció nem valós"
+#: ../roundup/scripts/roundup_mailgw.py:167
+msgid "Error: a later version of python is required"
+msgstr ""
 
 #: ../roundup/scripts/roundup_mailgw.py:170
+msgid "Error: pop specification not valid"
+msgstr "Hiba: a pop specifikáció nem érvényes"
+
+#: ../roundup/scripts/roundup_mailgw.py:177
 msgid "Error: apop specification not valid"
-msgstr "Hiba: az apop specifikáció nem valós"
+msgstr "Hiba: az apop specifikáció nem érvényes"
 
-#: ../roundup/scripts/roundup_mailgw.py:184
-msgid "Error: The source must be either \"mailbox\", \"pop\", \"apop\", \"imap\" or \"imaps\""
-msgstr "Hiba: A forrás a következők egyike kell legyen: \"mailbox\", \"pop\", \"apop\", \"imap\" vagy \"imaps\""
+#: ../roundup/scripts/roundup_mailgw.py:189
+msgid ""
+"Error: The source must be either \"mailbox\", \"pop\", \"apop\", \"imap\" or "
+"\"imaps\""
+msgstr ""
+"Hiba: A forrás a következők egyike kell legyen: \"mailbox\", \"pop\", \"apop"
+"\", \"imap\" vagy \"imaps\""
 
-#: ../roundup/scripts/roundup_server.py:157
+#: ../roundup/scripts/roundup_server.py:76
+msgid "WARNING: generating temporary SSL certificate"
+msgstr ""
+
+#: ../roundup/scripts/roundup_server.py:253
 msgid ""
 "<html><head><title>Roundup trackers index</title></head>\n"
 "<body><h1>Roundup trackers index</h1><ol>\n"
@@ -1726,52 +2007,52 @@
 "<html><head><title>Roundup hibakövetők listája</title></head>\n"
 "<body><h1>Roundup hibakövetők listája</h1><ol>\n"
 
-#: ../roundup/scripts/roundup_server.py:287
+#: ../roundup/scripts/roundup_server.py:389
 #, python-format
 msgid "Error: %s: %s"
 msgstr "Hiba: %s: %s"
 
-#: ../roundup/scripts/roundup_server.py:297
+#: ../roundup/scripts/roundup_server.py:399
 msgid "WARNING: ignoring \"-g\" argument, not root"
-msgstr "FIGYELEM: \"-g\" opciót figyelmen kívül hagyom, nem root"
+msgstr "FIGYELEM: \"-g\" opció figyelmen kívül hagyásra került, nem root"
 
-#: ../roundup/scripts/roundup_server.py:303
+#: ../roundup/scripts/roundup_server.py:405
 msgid "Can't change groups - no grp module"
 msgstr "Nem lehet csoportot váltani - nincs meg a grp modul"
 
-#: ../roundup/scripts/roundup_server.py:312
+#: ../roundup/scripts/roundup_server.py:414
 #, python-format
 msgid "Group %(group)s doesn't exist"
 msgstr "%(group)s csoport nem létezik"
 
-#: ../roundup/scripts/roundup_server.py:323
+#: ../roundup/scripts/roundup_server.py:425
 msgid "Can't run as root!"
 msgstr "Nem futhat root-ként!"
 
-#: ../roundup/scripts/roundup_server.py:326
+#: ../roundup/scripts/roundup_server.py:428
 msgid "WARNING: ignoring \"-u\" argument, not root"
-msgstr "FIGYELEM: \"-u\" opciót figyelmen kívül hagyom, nem root"
+msgstr "FIGYELEM: \"-u\" opció figyelmen kívül hagyásra került, nem root"
 
-#: ../roundup/scripts/roundup_server.py:331
+#: ../roundup/scripts/roundup_server.py:434
 msgid "Can't change users - no pwd module"
-msgstr ""
+msgstr "Felhasználóváltás nem sikerült - nincs pwd modul"
 
-#: ../roundup/scripts/roundup_server.py:340
+#: ../roundup/scripts/roundup_server.py:443
 #, python-format
 msgid "User %(user)s doesn't exist"
 msgstr "A(z) %(user)s felhasználó nem létezik"
 
-#: ../roundup/scripts/roundup_server.py:471
+#: ../roundup/scripts/roundup_server.py:592
 #, python-format
 msgid "Multiprocess mode \"%s\" is not available, switching to single-process"
-msgstr ""
+msgstr "\"%s\" többszálú mód nem érhető el, áttérés egyszálú módra"
 
-#: ../roundup/scripts/roundup_server.py:494
+#: ../roundup/scripts/roundup_server.py:620
 #, python-format
 msgid "Unable to bind to port %s, port already in use."
-msgstr ""
+msgstr "Nem sikerült a(z) %s portra csatlakozni, a port már használatban van."
 
-#: ../roundup/scripts/roundup_server.py:562
+#: ../roundup/scripts/roundup_server.py:688
 msgid ""
 " -c <Command>  Windows Service options.\n"
 "               If you want to run the server as a Windows Service, you\n"
@@ -1781,7 +2062,7 @@
 "               specifics."
 msgstr ""
 
-#: ../roundup/scripts/roundup_server.py:569
+#: ../roundup/scripts/roundup_server.py:695
 msgid ""
 " -u <UID>      runs the Roundup web server as this UID\n"
 " -g <GID>      runs the Roundup web server as this GID\n"
@@ -1790,7 +2071,7 @@
 "               specified if -d is used."
 msgstr ""
 
-#: ../roundup/scripts/roundup_server.py:576
+#: ../roundup/scripts/roundup_server.py:702
 #, python-format
 msgid ""
 "%(message)sUsage: roundup-server [options] [name=tracker home]*\n"
@@ -1803,7 +2084,11 @@
 " -n <name>     set the host name of the Roundup web server instance\n"
 " -p <port>     set the port to listen on (default: %(port)s)\n"
 " -l <fname>    log to the file indicated by fname instead of stderr/stdout\n"
-" -N            log client machine names instead of IP addresses (much slower)\n"
+" -N            log client machine names instead of IP addresses (much "
+"slower)\n"
+" -i <fname>    set tracker index template\n"
+" -s            enable SSL\n"
+" -e <fname>    PEM file containing SSL key and certificate\n"
 " -t <mode>     multiprocess mode (default: %(mp_def)s).\n"
 "               Allowed values: %(mp_types)s.\n"
 "%(os_part)s\n"
@@ -1844,23 +2129,24 @@
 "   any url-unsafe characters like spaces, as these confuse IE.\n"
 msgstr ""
 
-#: ../roundup/scripts/roundup_server.py:723
+#: ../roundup/scripts/roundup_server.py:860
 msgid "Instances must be name=home"
-msgstr ""
+msgstr "A példányoknak név=home formában kell lenniük"
 
-#: ../roundup/scripts/roundup_server.py:737
+#: ../roundup/scripts/roundup_server.py:874
 #, python-format
 msgid "Configuration saved to %s"
-msgstr "A konfiguráció elmentve %s"
+msgstr "Beállítások elmentve ide: %s"
 
-#: ../roundup/scripts/roundup_server.py:755
+#: ../roundup/scripts/roundup_server.py:892
 msgid "Sorry, you can't run the server as a daemon on this Operating System"
-msgstr "Elnézést, ezen az operációs rendszeren nem indíthatod el démonként a szervert"
+msgstr ""
+"Elnézést, ezen az operációs rendszeren a szerver nem indítható démonként"
 
-#: ../roundup/scripts/roundup_server.py:767
+#: ../roundup/scripts/roundup_server.py:907
 #, python-format
 msgid "Roundup server started on %(HOST)s:%(PORT)s"
-msgstr "Roundup server a %(HOST)s:%(PORT)s gépen"
+msgstr "Roundup server elindítva a(z) %(HOST)s:%(PORT)s gépen"
 
 #: ../templates/classic/html/_generic.collision.html:4
 #: ../templates/minimal/html/_generic.collision.html:4
@@ -1880,36 +2166,77 @@
 "  while you were editing. Please <a href='${context}'>reload</a>\n"
 "  the node and review your edits.\n"
 msgstr ""
+"\n"
+"  Ütközés történt. Egy másik felhasználó módosította ezt a bejegyzést\n"
+"  mialatt Ön is szerkesztette. <a href='${context}'>Olvassa újra</a>\n"
+"  a bejegyzést és szerkessze újra azt.\n"
 
-#: ../templates/classic/html/_generic.help.html:9
-#: ../templates/minimal/html/_generic.help.html:9
-msgid "${property} help - ${tracker}"
-msgstr "${property} segítség - ${tracker}"
+#: ../templates/classic/html/_generic.help-empty.html:6
+msgid "Please specify your search parameters!"
+msgstr ""
+
+#: ../templates/classic/html/_generic.help-list.html:20
+#: ../templates/classic/html/_generic.index.html:14
+#: ../templates/classic/html/_generic.item.html:12
+#: ../templates/classic/html/file.item.html:9
+#: ../templates/classic/html/issue.index.html:16
+#: ../templates/classic/html/issue.item.html:28
+#: ../templates/classic/html/msg.item.html:26
+#: ../templates/classic/html/user.index.html:9
+#: ../templates/classic/html/user.item.html:35
+#: ../templates/minimal/html/_generic.index.html:14
+#: ../templates/minimal/html/_generic.item.html:12
+#: ../templates/minimal/html/user.index.html:9
+#: ../templates/minimal/html/user.item.html:35
+#: ../templates/minimal/html/user.register.html:14
+msgid "You are not allowed to view this page."
+msgstr "Nincs jogosultsága az oldal megjelenítéséhez."
 
+#: ../templates/classic/html/_generic.help-list.html:34
+msgid "1..25 out of 50"
+msgstr ""
+
+#: ../templates/classic/html/_generic.help-search.html:9
+msgid ""
+"Generic template ${template} or version for class ${classname} is not yet "
+"implemented"
+msgstr ""
+
+#: ../templates/classic/html/_generic.help-submit.html:57
 #: ../templates/classic/html/_generic.help.html:31
 #: ../templates/minimal/html/_generic.help.html:31
 msgid " Cancel "
-msgstr "Mégse"
+msgstr " Mégsem "
 
+#: ../templates/classic/html/_generic.help-submit.html:63
 #: ../templates/classic/html/_generic.help.html:34
 #: ../templates/minimal/html/_generic.help.html:34
 msgid " Apply "
-msgstr "Alkalmaz"
+msgstr " Alkalmaz "
+
+#: ../templates/classic/html/_generic.help.html:9
+#: ../templates/classic/html/user.help.html:13
+#: ../templates/minimal/html/_generic.help.html:9
+msgid "${property} help - ${tracker}"
+msgstr "${property} segítség - ${tracker}"
 
 #: ../templates/classic/html/_generic.help.html:41
-#: ../templates/classic/html/issue.index.html:73
+#: ../templates/classic/html/help.html:21
+#: ../templates/classic/html/issue.index.html:81
 #: ../templates/minimal/html/_generic.help.html:41
 msgid "&lt;&lt; previous"
 msgstr "&lt;&lt; előző"
 
 #: ../templates/classic/html/_generic.help.html:53
-#: ../templates/classic/html/issue.index.html:81
+#: ../templates/classic/html/help.html:28
+#: ../templates/classic/html/issue.index.html:89
 #: ../templates/minimal/html/_generic.help.html:53
 msgid "${start}..${end} out of ${total}"
 msgstr "${start}..${end}, összesen ${total}"
 
 #: ../templates/classic/html/_generic.help.html:57
-#: ../templates/classic/html/issue.index.html:84
+#: ../templates/classic/html/help.html:32
+#: ../templates/classic/html/issue.index.html:92
 #: ../templates/minimal/html/_generic.help.html:57
 msgid "next &gt;&gt;"
 msgstr "következő &gt;&gt;"
@@ -1928,29 +2255,37 @@
 msgid "${class} editing"
 msgstr "${class} szerkesztése"
 
-#: ../templates/classic/html/_generic.index.html:14
-#: ../templates/classic/html/_generic.item.html:12
-#: ../templates/classic/html/file.item.html:9
-#: ../templates/classic/html/issue.index.html:16
-#: ../templates/classic/html/issue.item.html:28
-#: ../templates/classic/html/msg.item.html:26
-#: ../templates/classic/html/user.index.html:9
-#: ../templates/classic/html/user.item.html:28
-#: ../templates/minimal/html/_generic.index.html:14
-#: ../templates/minimal/html/_generic.item.html:12
-#: ../templates/minimal/html/user.index.html:9
-#: ../templates/minimal/html/user.item.html:28
-#: ../templates/minimal/html/user.register.html:14
-msgid "You are not allowed to view this page."
-msgstr "Nem nézheted meg ezt az oldalt."
-
-#: ../templates/classic/html/_generic.index.html:22
-#: ../templates/minimal/html/_generic.index.html:22
-msgid "<p class=\"form-help\"> You may edit the contents of the ${classname} class using this form. Commas, newlines and double quotes (\") must be handled delicately. You may include commas and newlines by enclosing the values in double-quotes (\"). Double quotes themselves must be quoted by doubling (\"\"). </p> <p class=\"form-help\"> Multilink properties have their multiple values colon (\":\") separated (... ,\"one:two:three\", ...) </p> <p class=\"form-help\"> Remove entries by deleting their line. Add new entries by appending them to the table - put an X in the id column. </p>"
+#: ../templates/classic/html/_generic.index.html:19
+#: ../templates/classic/html/_generic.item.html:16
+#: ../templates/classic/html/file.item.html:13
+#: ../templates/classic/html/issue.index.html:20
+#: ../templates/classic/html/issue.item.html:32
+#: ../templates/classic/html/msg.item.html:30
+#: ../templates/classic/html/user.index.html:13
+#: ../templates/classic/html/user.item.html:39
+#: ../templates/minimal/html/_generic.index.html:19
+#: ../templates/minimal/html/_generic.item.html:17
+#: ../templates/minimal/html/user.index.html:13
+#: ../templates/minimal/html/user.item.html:39
+#: ../templates/minimal/html/user.register.html:17
+msgid "Please login with your username and password."
+msgstr ""
+
+#: ../templates/classic/html/_generic.index.html:28
+#: ../templates/minimal/html/_generic.index.html:28
+msgid ""
+"<p class=\"form-help\"> You may edit the contents of the ${classname} class "
+"using this form. Commas, newlines and double quotes (\") must be handled "
+"delicately. You may include commas and newlines by enclosing the values in "
+"double-quotes (\"). Double quotes themselves must be quoted by doubling "
+"(\"\"). </p> <p class=\"form-help\"> Multilink properties have their "
+"multiple values colon (\":\") separated (... ,\"one:two:three\", ...) </p> "
+"<p class=\"form-help\"> Remove entries by deleting their line. Add new "
+"entries by appending them to the table - put an X in the id column. </p>"
 msgstr ""
 
-#: ../templates/classic/html/_generic.index.html:44
-#: ../templates/minimal/html/_generic.index.html:44
+#: ../templates/classic/html/_generic.index.html:50
+#: ../templates/minimal/html/_generic.index.html:50
 msgid "Edit Items"
 msgstr "Elemek szerkesztése"
 
@@ -1967,16 +2302,16 @@
 msgstr "Letöltés"
 
 #: ../templates/classic/html/file.index.html:11
-#: ../templates/classic/html/file.item.html:22
+#: ../templates/classic/html/file.item.html:27
 msgid "Content Type"
 msgstr "Tartalom típus"
 
 #: ../templates/classic/html/file.index.html:12
 msgid "Uploaded By"
-msgstr "Feltöltve"
+msgstr "Feltöltötte"
 
 #: ../templates/classic/html/file.index.html:13
-#: ../templates/classic/html/msg.item.html:43
+#: ../templates/classic/html/msg.item.html:48
 msgid "Date"
 msgstr "Dátum"
 
@@ -1988,13 +2323,12 @@
 msgid "File display"
 msgstr "Fájl megjelenítés"
 
-#: ../templates/classic/html/file.item.html:18
-#: ../templates/classic/html/user.item.html:39
+#: ../templates/classic/html/file.item.html:23
 #: ../templates/classic/html/user.register.html:17
 msgid "Name"
 msgstr "Név"
 
-#: ../templates/classic/html/file.item.html:40
+#: ../templates/classic/html/file.item.html:45
 msgid "download"
 msgstr "letöltés"
 
@@ -2008,80 +2342,78 @@
 msgid "List of classes"
 msgstr "Osztályok listája"
 
-#: ../templates/classic/html/issue.index.html:7
-msgid "List of issues - ${tracker}"
-msgstr "Ügyek listája - ${tracker}"
-
-#: ../templates/classic/html/issue.index.html:11
+#: ../templates/classic/html/issue.index.html:4
+#: ../templates/classic/html/issue.index.html:10
 msgid "List of issues"
 msgstr "Ügyek listája"
 
-#: ../templates/classic/html/issue.index.html:22
-#: ../templates/classic/html/issue.item.html:44
+#: ../templates/classic/html/issue.index.html:27
+#: ../templates/classic/html/issue.item.html:49
 msgid "Priority"
 msgstr "Prioritás"
 
-#: ../templates/classic/html/issue.index.html:23
+#: ../templates/classic/html/issue.index.html:28
 msgid "ID"
 msgstr "Azonosító"
 
-#: ../templates/classic/html/issue.index.html:24
+#: ../templates/classic/html/issue.index.html:29
 msgid "Creation"
 msgstr "Létrehozás"
 
-#: ../templates/classic/html/issue.index.html:25
+#: ../templates/classic/html/issue.index.html:30
 msgid "Activity"
 msgstr "Aktivitás"
 
-#: ../templates/classic/html/issue.index.html:26
+#: ../templates/classic/html/issue.index.html:31
 msgid "Actor"
 msgstr "Hozzászóló"
 
-#: ../templates/classic/html/issue.index.html:27
-msgid "Topic"
+#: ../templates/classic/html/issue.index.html:32
+#: ../templates/classic/html/keyword.item.html:37
+msgid "Keyword"
 msgstr "Téma"
 
-#: ../templates/classic/html/issue.index.html:28
-#: ../templates/classic/html/issue.item.html:39
+#: ../templates/classic/html/issue.index.html:33
+#: ../templates/classic/html/issue.item.html:44
 msgid "Title"
 msgstr "Cím"
 
-#: ../templates/classic/html/issue.index.html:29
-#: ../templates/classic/html/issue.item.html:46
+#: ../templates/classic/html/issue.index.html:34
+#: ../templates/classic/html/issue.item.html:51
 msgid "Status"
-msgstr "Állapt"
+msgstr "Állapot"
 
-#: ../templates/classic/html/issue.index.html:30
+#: ../templates/classic/html/issue.index.html:35
 msgid "Creator"
-msgstr "Készítő"
+msgstr "Létrehozó"
 
-#: ../templates/classic/html/issue.index.html:31
+#: ../templates/classic/html/issue.index.html:36
 msgid "Assigned&nbsp;To"
-msgstr "Hozzárendelve"
+msgstr "Kiosztva"
 
-#: ../templates/classic/html/issue.index.html:97
+#: ../templates/classic/html/issue.index.html:105
 msgid "Download as CSV"
 msgstr "Letöltés CSV-ként"
 
-#: ../templates/classic/html/issue.index.html:105
+#: ../templates/classic/html/issue.index.html:115
 msgid "Sort on:"
 msgstr "Rendezés:"
 
-#: ../templates/classic/html/issue.index.html:108
-#: ../templates/classic/html/issue.index.html:125
+#: ../templates/classic/html/issue.index.html:119
+#: ../templates/classic/html/issue.index.html:140
 msgid "- nothing -"
 msgstr "- semmi -"
 
-#: ../templates/classic/html/issue.index.html:116
-#: ../templates/classic/html/issue.index.html:133
+#: ../templates/classic/html/issue.index.html:127
+#: ../templates/classic/html/issue.index.html:148
 msgid "Descending:"
-msgstr "Csökkenőleg:"
+msgstr "Csökkenő:"
 
-#: ../templates/classic/html/issue.index.html:122
+#: ../templates/classic/html/issue.index.html:136
 msgid "Group on:"
 msgstr "Csoportosítás:"
 
-#: ../templates/classic/html/issue.index.html:139
+#: ../templates/classic/html/issue.index.html:155
 msgid "Redisplay"
 msgstr "Megjelenítés újra"
 
@@ -2091,15 +2423,15 @@
 
 #: ../templates/classic/html/issue.item.html:10
 msgid "New Issue - ${tracker}"
-msgstr "Új bejelentés - ${tracker}"
+msgstr "Új ügy - ${tracker}"
 
 #: ../templates/classic/html/issue.item.html:14
 msgid "New Issue"
-msgstr "Új bejelentés"
+msgstr "Új ügy"
 
 #: ../templates/classic/html/issue.item.html:16
 msgid "New Issue Editing"
-msgstr "Új bejelentés szerkesztése"
+msgstr "Új ügy szerkesztése"
 
 #: ../templates/classic/html/issue.item.html:19
 msgid "Issue${id}"
@@ -2109,255 +2441,275 @@
 msgid "Issue${id} Editing"
 msgstr "${id}. ügy szerkesztése"
 
-#: ../templates/classic/html/issue.item.html:51
+#: ../templates/classic/html/issue.item.html:56
 msgid "Superseder"
 msgstr "Helyettesítő"
 
-#: ../templates/classic/html/issue.item.html:56
-msgid "View: ${link}"
-msgstr "Mitasd: ${link}"
+#: ../templates/classic/html/issue.item.html:61
+msgid "View:"
+msgstr ""
 
-#: ../templates/classic/html/issue.item.html:60
+#: ../templates/classic/html/issue.item.html:67
 msgid "Nosy List"
 msgstr "Kíváncsiak listája"
 
-#: ../templates/classic/html/issue.item.html:69
+#: ../templates/classic/html/issue.item.html:76
 msgid "Assigned To"
 msgstr "Kiosztva"
 
-#: ../templates/classic/html/issue.item.html:71
-msgid "Topics"
-msgstr "Témák:"
+#: ../templates/classic/html/issue.item.html:78
+#: ../templates/classic/html/page.html:103
+#: ../templates/minimal/html/page.html:102
+msgid "Keywords"
+msgstr "Témák"
 
-#: ../templates/classic/html/issue.item.html:79
+#: ../templates/classic/html/issue.item.html:86
 msgid "Change Note"
 msgstr "Megjegyzés módosítása"
 
-#: ../templates/classic/html/issue.item.html:87
+#: ../templates/classic/html/issue.item.html:94
 msgid "File"
 msgstr "Fájl"
 
-#: ../templates/classic/html/issue.item.html:99
+#: ../templates/classic/html/issue.item.html:106
 msgid "Make a copy"
 msgstr "Másolat készítése"
 
-#: ../templates/classic/html/issue.item.html:107
-#: ../templates/classic/html/user.item.html:106
+#: ../templates/classic/html/issue.item.html:114
+#: ../templates/classic/html/user.item.html:153
 #: ../templates/classic/html/user.register.html:69
-#: ../templates/minimal/html/user.item.html:86
-msgid "<table class=\"form\"> <tr> <td>Note:&nbsp;</td> <th class=\"required\">highlighted</th> <td>&nbsp;fields are required.</td> </tr> </table>"
-msgstr "<table class=\"form\"> <tr> <td>Megjegyzés:&nbsp;</td> <th class=\"required\">a kiemelt</th> <td>&nbsp;mezők szükségesek.</td> </tr> </table>"
-
-#: ../templates/classic/html/issue.item.html:121
-msgid "Created on <b>${creation}</b> by <b>${creator}</b>, last changed <b>${activity}</b> by <b>${actor}</b>."
-msgstr "<b>${creation}</b> készítette <b>${creator}</b>, utoljára <b>${actor}</b> módosította <b>${activity}</b>-kor."
+#: ../templates/minimal/html/user.item.html:153
+msgid ""
+"<table class=\"form\"> <tr> <td>Note:&nbsp;</td> <th class=\"required"
+"\">highlighted</th> <td>&nbsp;fields are required.</td> </tr> </table>"
+msgstr ""
+"<table class=\"form\"> <tr> <td>Megjegyzés:&nbsp;</td> <th class=\"required"
+"\">a kiemelt</th> <td>&nbsp;mezők szükségesek.</td> </tr> </table>"
+
+#: ../templates/classic/html/issue.item.html:128
+msgid ""
+"Created on <b>${creation}</b> by <b>${creator}</b>, last changed <b>"
+"${activity}</b> by <b>${actor}</b>."
+msgstr ""
+"<b>${creation}</b> létrehozta <b>${creator}</b>, utoljára <b>${actor}</b> "
+"módosította <b>${activity}</b>-kor."
 
-#: ../templates/classic/html/issue.item.html:125
-#: ../templates/classic/html/msg.item.html:56
+#: ../templates/classic/html/issue.item.html:132
+#: ../templates/classic/html/msg.item.html:61
 msgid "Files"
 msgstr "Fájlok"
 
-#: ../templates/classic/html/issue.item.html:127
-#: ../templates/classic/html/msg.item.html:58
+#: ../templates/classic/html/issue.item.html:134
+#: ../templates/classic/html/msg.item.html:63
 msgid "File name"
-msgstr "Fájl neve"
+msgstr "Fájlnév"
 
-#: ../templates/classic/html/issue.item.html:128
-#: ../templates/classic/html/msg.item.html:59
+#: ../templates/classic/html/issue.item.html:135
+#: ../templates/classic/html/msg.item.html:64
 msgid "Uploaded"
 msgstr "Feltöltve"
 
-#: ../templates/classic/html/issue.item.html:129
+#: ../templates/classic/html/issue.item.html:136
 msgid "Type"
 msgstr "Típus"
 
-#: ../templates/classic/html/issue.item.html:130
+#: ../templates/classic/html/issue.item.html:137
 #: ../templates/classic/html/query.edit.html:30
 msgid "Edit"
 msgstr "Szerkesztés"
 
-#: ../templates/classic/html/issue.item.html:131
+#: ../templates/classic/html/issue.item.html:138
 msgid "Remove"
-msgstr "Eldobás"
+msgstr "Törlés"
 
-#: ../templates/classic/html/issue.item.html:151
-#: ../templates/classic/html/issue.item.html:172
+#: ../templates/classic/html/issue.item.html:158
+#: ../templates/classic/html/issue.item.html:179
 #: ../templates/classic/html/query.edit.html:50
 msgid "remove"
-msgstr "eldobás"
+msgstr "Törlés"
 
-#: ../templates/classic/html/issue.item.html:158
+#: ../templates/classic/html/issue.item.html:165
 #: ../templates/classic/html/msg.index.html:9
 msgid "Messages"
 msgstr "Ãœzenetek"
 
-#: ../templates/classic/html/issue.item.html:162
+#: ../templates/classic/html/issue.item.html:169
 msgid "msg${id} (view)"
-msgstr "${id}. üzenet (view)"
+msgstr "${id}. üzenet"
 
-#: ../templates/classic/html/issue.item.html:163
+#: ../templates/classic/html/issue.item.html:170
 msgid "Author: ${author}"
 msgstr "Szerző: ${author}"
 
-#: ../templates/classic/html/issue.item.html:165
+#: ../templates/classic/html/issue.item.html:172
 msgid "Date: ${date}"
 msgstr "Dátum: ${date}"
 
 #: ../templates/classic/html/issue.search.html:2
 msgid "Issue searching - ${tracker}"
-msgstr "Bejelentés keresése - ${tracker}"
+msgstr "Ügy keresése - ${tracker}"
 
 #: ../templates/classic/html/issue.search.html:4
 msgid "Issue searching"
-msgstr "Bejelentés keresése"
+msgstr "Ügy keresése"
 
-#: ../templates/classic/html/issue.search.html:25
+#: ../templates/classic/html/issue.search.html:31
 msgid "Filter on"
 msgstr "Szűrés"
 
-#: ../templates/classic/html/issue.search.html:26
+#: ../templates/classic/html/issue.search.html:32
 msgid "Display"
 msgstr "Megjelenítés"
 
-#: ../templates/classic/html/issue.search.html:27
+#: ../templates/classic/html/issue.search.html:33
 msgid "Sort on"
 msgstr "Rendezés"
 
-#: ../templates/classic/html/issue.search.html:28
+#: ../templates/classic/html/issue.search.html:34
 msgid "Group on"
 msgstr "Csoportosítás"
 
-#: ../templates/classic/html/issue.search.html:32
+#: ../templates/classic/html/issue.search.html:38
 msgid "All text*:"
 msgstr "Minden szöveg*:"
 
-#: ../templates/classic/html/issue.search.html:40
+#: ../templates/classic/html/issue.search.html:46
 msgid "Title:"
 msgstr "Cím:"
 
-#: ../templates/classic/html/issue.search.html:50
-msgid "Topic:"
-msgstr "Téma:"
+#: ../templates/classic/html/issue.search.html:56
+#, fuzzy
+msgid "Keyword:"
+msgstr "Téma"
 
 #: ../templates/classic/html/issue.search.html:58
+#: ../templates/classic/html/issue.search.html:123
+#: ../templates/classic/html/issue.search.html:139
+msgid "not selected"
+msgstr "nem kijelölt"
+
+#: ../templates/classic/html/issue.search.html:67
 msgid "ID:"
 msgstr "Azonosító:"
 
-#: ../templates/classic/html/issue.search.html:66
+#: ../templates/classic/html/issue.search.html:75
 msgid "Creation Date:"
-msgstr "Készítés dátuma:"
+msgstr "Létrehozás dátuma:"
 
-#: ../templates/classic/html/issue.search.html:77
+#: ../templates/classic/html/issue.search.html:86
 msgid "Creator:"
-msgstr "Készítő:"
+msgstr "Létrehozó:"
 
-#: ../templates/classic/html/issue.search.html:79
+#: ../templates/classic/html/issue.search.html:88
 msgid "created by me"
 msgstr "én készítettem"
 
-#: ../templates/classic/html/issue.search.html:88
+#: ../templates/classic/html/issue.search.html:97
 msgid "Activity:"
-msgstr "Aktivitás:"
+msgstr "Művelet:"
 
-#: ../templates/classic/html/issue.search.html:99
+#: ../templates/classic/html/issue.search.html:108
 msgid "Actor:"
 msgstr "Hozzászóló:"
 
-#: ../templates/classic/html/issue.search.html:101
+#: ../templates/classic/html/issue.search.html:110
 msgid "done by me"
-msgstr "én tettem"
+msgstr "saját magam"
 
-#: ../templates/classic/html/issue.search.html:112
+#: ../templates/classic/html/issue.search.html:121
 msgid "Priority:"
 msgstr "Prioritás:"
 
-#: ../templates/classic/html/issue.search.html:114
-#: ../templates/classic/html/issue.search.html:130
-msgid "not selected"
-msgstr "nem kijelölt"
-
-#: ../templates/classic/html/issue.search.html:125
+#: ../templates/classic/html/issue.search.html:134
 msgid "Status:"
 msgstr "Állapot:"
 
-#: ../templates/classic/html/issue.search.html:128
+#: ../templates/classic/html/issue.search.html:137
 msgid "not resolved"
 msgstr "nem megoldott"
 
-#: ../templates/classic/html/issue.search.html:143
+#: ../templates/classic/html/issue.search.html:152
 msgid "Assigned to:"
 msgstr "Kiadva:"
 
-#: ../templates/classic/html/issue.search.html:146
+#: ../templates/classic/html/issue.search.html:155
 msgid "assigned to me"
 msgstr "nekem adva"
 
-#: ../templates/classic/html/issue.search.html:148
+#: ../templates/classic/html/issue.search.html:157
 msgid "unassigned"
 msgstr "gazdátlan"
 
-#: ../templates/classic/html/issue.search.html:158
+#: ../templates/classic/html/issue.search.html:167
 msgid "No Sort or group:"
-msgstr "Ne rendezd vagy csoportosítsd:"
+msgstr "Ne rendezze vagy csoportosítsa:"
 
-#: ../templates/classic/html/issue.search.html:166
+#: ../templates/classic/html/issue.search.html:175
 msgid "Pagesize:"
 msgstr "Oldalméret:"
 
-#: ../templates/classic/html/issue.search.html:172
+#: ../templates/classic/html/issue.search.html:181
 msgid "Start With:"
 msgstr "Kezdés:"
 
-#: ../templates/classic/html/issue.search.html:178
+#: ../templates/classic/html/issue.search.html:187
 msgid "Sort Descending:"
-msgstr "Csökkenőleg rendzve:"
+msgstr "Csökkenő rendezés:"
 
-#: ../templates/classic/html/issue.search.html:185
+#: ../templates/classic/html/issue.search.html:194
 msgid "Group Descending:"
-msgstr "Csoport csökkenőleg:"
+msgstr "Csökkenő csoportosítás:"
 
-#: ../templates/classic/html/issue.search.html:192
+#: ../templates/classic/html/issue.search.html:201
 msgid "Query name**:"
 msgstr "Lekérdezés neve**:"
 
-#: ../templates/classic/html/issue.search.html:204
-#: ../templates/classic/html/page.html:31
-#: ../templates/classic/html/page.html:60
-#: ../templates/minimal/html/page.html:31
+#: ../templates/classic/html/issue.search.html:213
+#: ../templates/classic/html/page.html:43
+#: ../templates/classic/html/page.html:92
+#: ../templates/classic/html/user.help-search.html:69
+#: ../templates/minimal/html/page.html:43
+#: ../templates/minimal/html/page.html:91
 msgid "Search"
 msgstr "Keresés"
 
-#: ../templates/classic/html/issue.search.html:209
+#: ../templates/classic/html/issue.search.html:218
 msgid "*: The \"all text\" field will look in message bodies and issue titles"
-msgstr "*: The \"all text\" field will look in message bodies and issue titles"
+msgstr ""
+"*: A \"Minden szöveg\" mező az üzenetek címsorában és belsejében is keres"
 
-#: ../templates/classic/html/issue.search.html:212
-msgid "**: If you supply a name, the query will be saved off and available as a link in the sidebar"
-msgstr "**: Ha megadsz egy nevet, a lekérdezést elmentjük és az oldalsávon elérhető lesz"
+#: ../templates/classic/html/issue.search.html:221
+msgid ""
+"**: If you supply a name, the query will be saved off and available as a "
+"link in the sidebar"
+msgstr ""
+"**: Ha megad egy nevet, a lekérdezés elmentésre kerül és az oldalsávon "
+"elérhető lesz"
 
 #: ../templates/classic/html/keyword.item.html:3
 msgid "Keyword editing - ${tracker}"
-msgstr "Kulcsszó szerkesztése - ${tracker}"
+msgstr "Témák szerkesztése - ${tracker}"
 
 #: ../templates/classic/html/keyword.item.html:5
 msgid "Keyword editing"
-msgstr "Kulcsszó szerkesztése"
+msgstr "Téma szerkesztése"
 
 #: ../templates/classic/html/keyword.item.html:11
 msgid "Existing Keywords"
-msgstr "Létező kulcsszavak"
+msgstr "Létező témák"
 
 #: ../templates/classic/html/keyword.item.html:20
-msgid "To edit an existing keyword (for spelling or typing errors), click on its entry above."
-msgstr "Meglévő kulcsszó szerkesztéséhez (helyesírási hibák) kattints a fenti elemre."
+msgid ""
+"To edit an existing keyword (for spelling or typing errors), click on its "
+"entry above."
+msgstr ""
+"Meglévő téma szerkesztéséhez (helyesírási hibák) kattintson a fenti elemre."
 
 #: ../templates/classic/html/keyword.item.html:27
 msgid "To create a new keyword, enter it below and click \"Submit New Entry\"."
-msgstr "Új kulcsszó létrehozásához add meg alább és kattints az \"Új elem küldése\" gombra."
-
-#: ../templates/classic/html/keyword.item.html:37
-msgid "Keyword"
-msgstr "Kulcsszó"
+msgstr ""
+"Új téma létrehozásához adja meg alább, majd kattintson a \"Létrehozás\" "
+"gombra."
 
 #: ../templates/classic/html/msg.index.html:3
 msgid "List of messages - ${tracker}"
@@ -2365,7 +2717,7 @@
 
 #: ../templates/classic/html/msg.index.html:5
 msgid "Message listing"
-msgstr "Üzenetek listázása"
+msgstr "Üzenetek listája"
 
 #: ../templates/classic/html/msg.item.html:6
 msgid "Message ${id} - ${tracker}"
@@ -2391,147 +2743,167 @@
 msgid "Message${id} Editing"
 msgstr "${id}. üzenet szerkesztése"
 
-#: ../templates/classic/html/msg.item.html:33
+#: ../templates/classic/html/msg.item.html:38
 msgid "Author"
 msgstr "Szerző"
 
-#: ../templates/classic/html/msg.item.html:38
+#: ../templates/classic/html/msg.item.html:43
 msgid "Recipients"
 msgstr "Címzettek"
 
-#: ../templates/classic/html/msg.item.html:49
+#: ../templates/classic/html/msg.item.html:54
 msgid "Content"
 msgstr "Tartalom"
 
-#: ../templates/classic/html/page.html:41
+#: ../templates/classic/html/page.html:54
+#: ../templates/minimal/html/page.html:53
 msgid "<b>Your Queries</b> (<a href=\"query?@template=edit\">edit</a>)"
-msgstr "<b>Te lekérdezéseid</b> (<a href=\"query?@template=edit\">szerkesztés</a>)"
+msgstr "<b>Lekérdezések</b> (<a href=\"query?@template=edit\">szerk.</a>)"
 
-#: ../templates/classic/html/page.html:52
+#: ../templates/classic/html/page.html:65
+#: ../templates/minimal/html/page.html:64
 msgid "Issues"
-msgstr "Témák"
+msgstr "Ãœgyek"
 
-#: ../templates/classic/html/page.html:54
-#: ../templates/classic/html/page.html:74
+#: ../templates/classic/html/page.html:67
+#: ../templates/classic/html/page.html:105
+#: ../templates/minimal/html/page.html:66
+#: ../templates/minimal/html/page.html:104
 msgid "Create New"
 msgstr "Új létrehozása"
 
-#: ../templates/classic/html/page.html:56
+#: ../templates/classic/html/page.html:69
+#: ../templates/minimal/html/page.html:68
 msgid "Show Unassigned"
 msgstr "Gazdátlanok mutatása"
 
-#: ../templates/classic/html/page.html:58
+#: ../templates/classic/html/page.html:81
+#: ../templates/minimal/html/page.html:80
 msgid "Show All"
 msgstr "Mutasd mind"
 
-#: ../templates/classic/html/page.html:61
+#: ../templates/classic/html/page.html:93
+#: ../templates/minimal/html/page.html:92
 msgid "Show issue:"
-msgstr "Téma mutatása:"
+msgstr "Ügy mutatása:"
 
-#: ../templates/classic/html/page.html:72
-msgid "Keywords"
-msgstr "Kulcsszavak"
-
-#: ../templates/classic/html/page.html:78
+#: ../templates/classic/html/page.html:108
+#: ../templates/minimal/html/page.html:107
 msgid "Edit Existing"
 msgstr "Meglévők szerkesztése"
 
-#: ../templates/classic/html/page.html:84
-#: ../templates/minimal/html/page.html:65
+#: ../templates/classic/html/page.html:114
+#: ../templates/minimal/html/page.html:113
 msgid "Administration"
 msgstr "Adminisztráció"
 
-#: ../templates/classic/html/page.html:86
-#: ../templates/minimal/html/page.html:66
+#: ../templates/classic/html/page.html:116
+#: ../templates/minimal/html/page.html:115
 msgid "Class List"
-msgstr "Osztálzok listája"
+msgstr "Osztályok listája"
 
-#: ../templates/classic/html/page.html:90
-#: ../templates/minimal/html/page.html:68
+#: ../templates/classic/html/page.html:120
+#: ../templates/minimal/html/page.html:119
 msgid "User List"
 msgstr "Felhasználók listája"
 
-#: ../templates/classic/html/page.html:92
-#: ../templates/minimal/html/page.html:71
+#: ../templates/classic/html/page.html:122
+#: ../templates/minimal/html/page.html:121
 msgid "Add User"
 msgstr "Felhasználó hozzáadása"
 
-#: ../templates/classic/html/page.html:99
-#: ../templates/classic/html/page.html:105
-#: ../templates/minimal/html/page.html:46
+#: ../templates/classic/html/page.html:129
+#: ../templates/classic/html/page.html:135
+#: ../templates/minimal/html/page.html:128
+#: ../templates/minimal/html/page.html:134
 msgid "Login"
 msgstr "Bejelentkezés"
 
-#: ../templates/classic/html/page.html:104
-#: ../templates/minimal/html/page.html:45
+#: ../templates/classic/html/page.html:134
+#: ../templates/minimal/html/page.html:133
 msgid "Remember me?"
-msgstr "Emlékezzek rád?"
+msgstr "Emlékezzen?"
 
-#: ../templates/classic/html/page.html:108
+#: ../templates/classic/html/page.html:138
 #: ../templates/classic/html/user.register.html:63
-#: ../templates/minimal/html/page.html:50
-#: ../templates/minimal/html/user.register.html:58
+#: ../templates/minimal/html/page.html:137
+#: ../templates/minimal/html/user.register.html:61
 msgid "Register"
-msgstr "Regisztrálás"
+msgstr "Regisztráció"
 
-#: ../templates/classic/html/page.html:111
+#: ../templates/classic/html/page.html:141
+#: ../templates/minimal/html/page.html:140
 msgid "Lost&nbsp;your&nbsp;login?"
-msgstr "Elveszett&nbsp;a&nbsp;jelszavad?"
+msgstr "Elveszett&nbsp;a&nbsp;jelszava?"
 
-#: ../templates/classic/html/page.html:116
+#: ../templates/classic/html/page.html:146
+#: ../templates/minimal/html/page.html:145
 msgid "Hello, ${user}"
 msgstr "Helló, ${user}"
 
-#: ../templates/classic/html/page.html:118
+#: ../templates/classic/html/page.html:148
 msgid "Your Issues"
-msgstr "Témáid"
+msgstr "Saját ügyek"
 
-#: ../templates/classic/html/page.html:119
-#: ../templates/minimal/html/page.html:57
+#: ../templates/classic/html/page.html:160
+#: ../templates/minimal/html/page.html:147
 msgid "Your Details"
-msgstr "Adataid"
+msgstr "Saját adatok"
 
-#: ../templates/classic/html/page.html:121
-#: ../templates/minimal/html/page.html:59
+#: ../templates/classic/html/page.html:162
+#: ../templates/minimal/html/page.html:149
 msgid "Logout"
 msgstr "Kijelentkezés"
 
-#: ../templates/classic/html/page.html:125
+#: ../templates/classic/html/page.html:166
+#: ../templates/minimal/html/page.html:153
 msgid "Help"
 msgstr "Segítség"
 
-#: ../templates/classic/html/page.html:126
+#: ../templates/classic/html/page.html:167
+#: ../templates/minimal/html/page.html:154
 msgid "Roundup docs"
 msgstr "Roundup dokumentáció"
 
-#: ../templates/classic/html/page.html:136
-#: ../templates/minimal/html/page.html:81
+#: ../templates/classic/html/page.html:177
+#: ../templates/minimal/html/page.html:164
 msgid "clear this message"
 msgstr "üzenet törlése"
 
-#: ../templates/classic/html/page.html:181
+#: ../templates/classic/html/page.html:241
+#: ../templates/classic/html/page.html:256
+#: ../templates/classic/html/page.html:270
+#: ../templates/minimal/html/page.html:228
+#: ../templates/minimal/html/page.html:243
+#: ../templates/minimal/html/page.html:257
 msgid "don't care"
-msgstr "ne törődj vele"
+msgstr "mindegy"
 
-#: ../templates/classic/html/page.html:183
+#: ../templates/classic/html/page.html:243
+#: ../templates/classic/html/page.html:258
+#: ../templates/classic/html/page.html:271
+#: ../templates/minimal/html/page.html:230
+#: ../templates/minimal/html/page.html:245
+#: ../templates/minimal/html/page.html:258
 msgid "------------"
 msgstr "------------"
 
-#: ../templates/classic/html/page.html:210
+#: ../templates/classic/html/page.html:299
+#: ../templates/minimal/html/page.html:286
 msgid "no value"
 msgstr "nincs érték"
 
 #: ../templates/classic/html/query.edit.html:4
 msgid "\"Your Queries\" Editing - ${tracker}"
-msgstr "\"Te lekérdezéseid\" szerkesztése - ${tracker}"
+msgstr "\"Saját lekérdezések\" szerkesztése - ${tracker}"
 
 #: ../templates/classic/html/query.edit.html:6
 msgid "\"Your Queries\" Editing"
-msgstr "\"Te lekérdezéseid\" szerkesztése"
+msgstr "\"Saját lekérdezések\" szerkesztése"
 
 #: ../templates/classic/html/query.edit.html:11
 msgid "You are not allowed to edit queries."
-msgstr "Nem szerkeszthetsz lekérdezéseket."
+msgstr "Nincs jogosultsága lekérdezések szerkesztéséhez."
 
 #: ../templates/classic/html/query.edit.html:28
 msgid "Query"
@@ -2539,11 +2911,11 @@
 
 #: ../templates/classic/html/query.edit.html:29
 msgid "Include in \"Your Queries\""
-msgstr "Bevesz a \"Te lekérdezéseid\" közé"
+msgstr "Hozzáadás a \"Saját lekérdezések\"-hez"
 
 #: ../templates/classic/html/query.edit.html:31
 msgid "Private to you?"
-msgstr "Csak neked?"
+msgstr "Csak saját használatra?"
 
 #: ../templates/classic/html/query.edit.html:44
 msgid "leave out"
@@ -2562,7 +2934,7 @@
 msgstr "[lekérdezés visszavonva]"
 
 #: ../templates/classic/html/query.edit.html:67
-#: ../templates/classic/html/query.edit.html:92
+#: ../templates/classic/html/query.edit.html:94
 msgid "edit"
 msgstr "szerkesztés"
 
@@ -2578,11 +2950,11 @@
 msgid "Delete"
 msgstr "Törlés"
 
-#: ../templates/classic/html/query.edit.html:94
+#: ../templates/classic/html/query.edit.html:96
 msgid "[not yours to edit]"
-msgstr "[nem a tied hogy szerkeszd]"
+msgstr "[nem saját szerkesztésű]"
 
-#: ../templates/classic/html/query.edit.html:102
+#: ../templates/classic/html/query.edit.html:104
 msgid "Save Selection"
 msgstr "Kijelölés mentése"
 
@@ -2595,29 +2967,48 @@
 msgstr "Jelszó törlés kérése"
 
 #: ../templates/classic/html/user.forgotten.html:9
-msgid "You have two options if you have forgotten your password. If you know the email address you registered with, enter it below."
-msgstr "Két lehetőséged van, ha elfelejtetted a jelszavad. Ha tudod a regisztrációs jelszavad, add meg alább."
+msgid ""
+"You have two options if you have forgotten your password. If you know the "
+"email address you registered with, enter it below."
+msgstr ""
+"Két lehetősége van, ha elfelejtette a jelszavát. Ha tudja a regisztrációs e-"
+"mail címét, adja meg alább."
 
 #: ../templates/classic/html/user.forgotten.html:16
 msgid "Email Address:"
-msgstr "Email cím:"
+msgstr "E-mail cím:"
 
 #: ../templates/classic/html/user.forgotten.html:24
 #: ../templates/classic/html/user.forgotten.html:34
 msgid "Request password reset"
-msgstr "Kérj jelszó törlést."
+msgstr "Kérjen jelszó törlést"
 
 #: ../templates/classic/html/user.forgotten.html:30
 msgid "Or, if you know your username, then enter it below."
-msgstr "Vagy, ha ismered a felhasználó nevet, add meg alább."
+msgstr "Vagy, ha ismeri a felhasználónevet, adja meg alább."
 
 #: ../templates/classic/html/user.forgotten.html:33
 msgid "Username:"
-msgstr "Felhasználó név:"
+msgstr "Felhasználónév:"
 
 #: ../templates/classic/html/user.forgotten.html:39
-msgid "A confirmation email will be sent to you - please follow the instructions within it to complete the reset process."
-msgstr "Egy megerősítő emailt küldünk neked - kérlek kövesd a benne foglaltakat hogy befejezhesd a törlést."
+msgid ""
+"A confirmation email will be sent to you - please follow the instructions "
+"within it to complete the reset process."
+msgstr ""
+"A rendszer egy megerősítő e-mailt küld Önnek - kövesse a benne foglaltakat a "
+"visszaállítási folyamat befejezéséhez."
+
+#: ../templates/classic/html/user.help-search.html:73
+#, fuzzy
+msgid "Pagesize"
+msgstr "Oldalméret:"
+
+#: ../templates/classic/html/user.help.html:43
+msgid ""
+"Your browser is not capable of using frames; you should be redirected "
+"immediately, or visit ${link}."
+msgstr ""
 
 #: ../templates/classic/html/user.index.html:3
 #: ../templates/minimal/html/user.index.html:3
@@ -2629,135 +3020,123 @@
 msgid "User listing"
 msgstr "Felhasználók listája"
 
-#: ../templates/classic/html/user.index.html:14
-#: ../templates/minimal/html/user.index.html:14
+#: ../templates/classic/html/user.index.html:19
+#: ../templates/minimal/html/user.index.html:19
 msgid "Username"
 msgstr "Felhasználónév"
 
-#: ../templates/classic/html/user.index.html:15
+#: ../templates/classic/html/user.index.html:20
 msgid "Real name"
 msgstr "Valódi név"
 
-#: ../templates/classic/html/user.index.html:16
-#: ../templates/classic/html/user.item.html:70
+#: ../templates/classic/html/user.index.html:21
 #: ../templates/classic/html/user.register.html:45
 msgid "Organisation"
 msgstr "Szervezet"
 
-#: ../templates/classic/html/user.index.html:17
-#: ../templates/minimal/html/user.index.html:15
+#: ../templates/classic/html/user.index.html:22
+#: ../templates/minimal/html/user.index.html:20
 msgid "Email address"
-msgstr "Email cím"
+msgstr "E-mail cím"
 
-#: ../templates/classic/html/user.index.html:18
+#: ../templates/classic/html/user.index.html:23
 msgid "Phone number"
 msgstr "Telefonszám"
 
-#: ../templates/classic/html/user.index.html:19
+#: ../templates/classic/html/user.index.html:24
 msgid "Retire"
 msgstr "Visszavonulás"
 
-#: ../templates/classic/html/user.index.html:32
+#: ../templates/classic/html/user.index.html:37
 msgid "retire"
 msgstr "visszavonulás"
 
-#: ../templates/classic/html/user.item.html:7
-#: ../templates/minimal/html/user.item.html:7
+#: ../templates/classic/html/user.item.html:9
+#: ../templates/minimal/html/user.item.html:9
 msgid "User ${id}: ${title} - ${tracker}"
 msgstr "${id}. felhasználó: ${title} - ${tracker}"
 
-#: ../templates/classic/html/user.item.html:10
-#: ../templates/minimal/html/user.item.html:10
+#: ../templates/classic/html/user.item.html:12
+#: ../templates/minimal/html/user.item.html:12
 msgid "New User - ${tracker}"
 msgstr "Új felhasználó - ${tracker}"
 
-#: ../templates/classic/html/user.item.html:14
-#: ../templates/minimal/html/user.item.html:14
+#: ../templates/classic/html/user.item.html:21
+#: ../templates/minimal/html/user.item.html:21
 msgid "New User"
 msgstr "Új felhasználó"
 
-#: ../templates/classic/html/user.item.html:16
-#: ../templates/minimal/html/user.item.html:16
+#: ../templates/classic/html/user.item.html:23
+#: ../templates/minimal/html/user.item.html:23
 msgid "New User Editing"
 msgstr "Új felhasználó szerkesztése"
 
-#: ../templates/classic/html/user.item.html:19
-#: ../templates/minimal/html/user.item.html:19
+#: ../templates/classic/html/user.item.html:26
+#: ../templates/minimal/html/user.item.html:26
 msgid "User${id}"
 msgstr "${id}. felhasználó"
 
-#: ../templates/classic/html/user.item.html:22
-#: ../templates/minimal/html/user.item.html:22
+#: ../templates/classic/html/user.item.html:29
+#: ../templates/minimal/html/user.item.html:29
 msgid "User${id} Editing"
 msgstr "${id}. felhasználó szerkesztése"
 
-#: ../templates/classic/html/user.item.html:43
+#: ../templates/classic/html/user.item.html:80
+#: ../templates/classic/html/user.register.html:33
+#: ../templates/minimal/html/user.item.html:80
+#: ../templates/minimal/html/user.register.html:41
+msgid "Roles"
+msgstr "Szerepkörök"
+
+#: ../templates/classic/html/user.item.html:88
+#: ../templates/minimal/html/user.item.html:88
+msgid "(to give the user more than one role, enter a comma,separated,list)"
+msgstr ""
+"(egynél több szerepkör megadásához vesszővel,elválasztott,listát,adjon,meg)"
+
+#: ../templates/classic/html/user.item.html:109
+#: ../templates/minimal/html/user.item.html:109
+msgid "(this is a numeric hour offset, the default is ${zone})"
+msgstr "(ez egy numerikus óra eltolás, ${zone} az alapértelmezett)"
+
+#: ../templates/classic/html/user.item.html:130
+#: ../templates/classic/html/user.register.html:53
+#: ../templates/minimal/html/user.item.html:130
+#: ../templates/minimal/html/user.register.html:53
+msgid "Alternate E-mail addresses<br>One address per line"
+msgstr "Alternatív e-mail címek <br>soronként egy cím"
+
+#: ../templates/classic/html/user.register.html:4
+#: ../templates/classic/html/user.register.html:7
+#: ../templates/minimal/html/user.register.html:4
+#: ../templates/minimal/html/user.register.html:7
+msgid "Registering with ${tracker}"
+msgstr "Regisztrálás a következőnél: ${tracker}"
+
 #: ../templates/classic/html/user.register.html:21
-#: ../templates/minimal/html/user.item.html:40
-#: ../templates/minimal/html/user.register.html:26
+#: ../templates/minimal/html/user.register.html:29
 msgid "Login Name"
 msgstr "Bejelentkezési név"
 
-#: ../templates/classic/html/user.item.html:47
 #: ../templates/classic/html/user.register.html:25
-#: ../templates/minimal/html/user.item.html:44
-#: ../templates/minimal/html/user.register.html:30
+#: ../templates/minimal/html/user.register.html:33
 msgid "Login Password"
 msgstr "Bejelentkezési jelszó"
 
-#: ../templates/classic/html/user.item.html:51
 #: ../templates/classic/html/user.register.html:29
-#: ../templates/minimal/html/user.item.html:48
-#: ../templates/minimal/html/user.register.html:34
+#: ../templates/minimal/html/user.register.html:37
 msgid "Confirm Password"
 msgstr "Jelszó megerősítése"
 
-#: ../templates/classic/html/user.item.html:55
-#: ../templates/classic/html/user.register.html:33
-#: ../templates/minimal/html/user.item.html:52
-#: ../templates/minimal/html/user.register.html:38
-msgid "Roles"
-msgstr "Szerepkörök"
-
-#: ../templates/classic/html/user.item.html:61
-#: ../templates/minimal/html/user.item.html:58
-msgid "(to give the user more than one role, enter a comma,separated,list)"
-msgstr "(egynél több szerepkör megadásához vesszővel,elválasztott,listát,adj,meg)"
-
-#: ../templates/classic/html/user.item.html:66
 #: ../templates/classic/html/user.register.html:41
 msgid "Phone"
 msgstr "Telefon"
 
-#: ../templates/classic/html/user.item.html:74
-msgid "Timezone"
-msgstr "Időzóna"
-
-#: ../templates/classic/html/user.item.html:78
-msgid "(this is a numeric hour offset, the default is ${zone})"
-msgstr "(ez egy numerikus óra eltolás, ${zone} az alapértelmezett)"
-
-#: ../templates/classic/html/user.item.html:83
 #: ../templates/classic/html/user.register.html:49
-#: ../templates/minimal/html/user.item.html:63
-#: ../templates/minimal/html/user.register.html:46
+#: ../templates/minimal/html/user.register.html:49
 msgid "E-mail address"
 msgstr "E-mail címek"
 
-#: ../templates/classic/html/user.item.html:91
-#: ../templates/classic/html/user.register.html:53
-#: ../templates/minimal/html/user.item.html:71
-#: ../templates/minimal/html/user.register.html:50
-msgid "Alternate E-mail addresses<br>One address per line"
-msgstr "Alternatív e-mail címek <br>soronként egy cím "
-
-#: ../templates/classic/html/user.register.html:4
-#: ../templates/classic/html/user.register.html:7
-#: ../templates/minimal/html/user.register.html:4
-#: ../templates/minimal/html/user.register.html:7
-msgid "Registering with ${tracker}"
-msgstr "Regisztrálás a következőnél: ${tracker}"
-
 #: ../templates/classic/html/user.rego_progress.html:4
 #: ../templates/minimal/html/user.rego_progress.html:4
 msgid "Registration in progress - ${tracker}"
@@ -2770,93 +3149,79 @@
 
 #: ../templates/classic/html/user.rego_progress.html:10
 #: ../templates/minimal/html/user.rego_progress.html:10
-msgid "You will shortly receive an email to confirm your registration. To complete the registration process, visit the link indicated in the email."
-msgstr "Rövidesen kapni fog egy e-mailt a regisztrációjának megerősítésére.. A regisztrálás befejezéséhet kérem kövesse a levélben lévő linket.."
-
-#: ../templates/minimal/html/home.html:2
-msgid "Tracker home - ${tracker}"
-msgstr "Hibakövető - ${tracker}"
-
-#: ../templates/minimal/html/home.html:4
-msgid "Tracker home"
-msgstr "Hibakövető otthona"
-
-#: ../templates/minimal/html/home.html:16
-msgid "Please select from one of the menu options on the left."
-msgstr "Kérem válasson a bal oldali menüből."
-
-#: ../templates/minimal/html/home.html:19
-msgid "Please log in or register."
-msgstr "Kérem jelentkezzen be vagy regisztráljon."
-
-#: ../templates/minimal/html/page.html:55
-msgid "Hello,<br>${user}"
-msgstr "Helló, <br>${user}"
+msgid ""
+"You will shortly receive an email to confirm your registration. To complete "
+"the registration process, visit the link indicated in the email."
+msgstr ""
+"Rövidesen kapni fog egy e-mailt a regisztrációjának megerősítésére. A "
+"regisztráció befejezéséhet kövesse a levélben lévő linket."
 
 # priority translations:
 #: ../templates/classic/initial_data.py:5
-#: ../templates/classic/html/page.html:246
 msgid "critical"
 msgstr "kritikus"
 
 #: ../templates/classic/initial_data.py:6
-#: ../templates/classic/html/page.html:246
 msgid "urgent"
 msgstr "sürgős"
 
 #: ../templates/classic/initial_data.py:7
-#: ../templates/classic/html/page.html:246
 msgid "bug"
 msgstr "hiba"
 
 #: ../templates/classic/initial_data.py:8
-#: ../templates/classic/html/page.html:246
 msgid "feature"
 msgstr "szolgáltatás"
 
 #: ../templates/classic/initial_data.py:9
-#: ../templates/classic/html/page.html:246
 msgid "wish"
 msgstr "óhaj"
 
 # status translations:
-#: status ../templates/classic/initial_data.py:12
-#: ../templates/classic/html/page.html:246
+#: ../templates/classic/initial_data.py:12
 msgid "unread"
 msgstr "nem olvasott"
 
 #: ../templates/classic/initial_data.py:13
-#: ../templates/classic/html/page.html:246
 msgid "deferred"
 msgstr "elutasítva"
 
 #: ../templates/classic/initial_data.py:14
-#: ../templates/classic/html/page.html:246
 msgid "chatting"
 msgstr "megbeszélés"
 
 #: ../templates/classic/initial_data.py:15
-#: ../templates/classic/html/page.html:246
-msgid "in-progress"
-msgstr "folyamatban"
-
-#: ../templates/classic/initial_data.py:16
-#: ../templates/classic/html/page.html:246
 msgid "need-eg"
 msgstr "megerősítésre vár"
 
+#: ../templates/classic/initial_data.py:16
+msgid "in-progress"
+msgstr "folyamatban"
+
 #: ../templates/classic/initial_data.py:17
-#: ../templates/classic/html/page.html:246
 msgid "testing"
 msgstr "tesztelés"
 
 #: ../templates/classic/initial_data.py:18
-#: ../templates/classic/html/page.html:246
 msgid "done-cbb"
-msgstr "elkészült"
+msgstr "elkészült-lehetne jobb"
 
 #: ../templates/classic/initial_data.py:19
-#: ../templates/classic/html/page.html:246
 msgid "resolved"
 msgstr "megoldva"
 
+#: ../templates/minimal/html/home.html:2
+msgid "Tracker home - ${tracker}"
+msgstr "Hibakövető - ${tracker}"
+
+#: ../templates/minimal/html/home.html:4
+msgid "Tracker home"
+msgstr "Hibakövető"
+
+#: ../templates/minimal/html/home.html:16
+msgid "Please select from one of the menu options on the left."
+msgstr "Kérem válasszon a bal oldali menüből."
+
+#: ../templates/minimal/html/home.html:19
+msgid "Please log in or register."
+msgstr "Jelentkezzen be vagy regisztráljon."

Modified: tracker/roundup-src/locale/lt.po
==============================================================================
--- tracker/roundup-src/locale/lt.po	(original)
+++ tracker/roundup-src/locale/lt.po	Sun Mar  9 09:26:16 2008
@@ -1,7 +1,7 @@
 # Lithuanian message file for Roundup Issue Tracker
 # Aiste Kesminaite <aiste at pov.lt>, 2005
 #
-# $Id: lt.po,v 1.7 2006/11/23 05:23:26 a1s Exp $
+# $Id: lt.po,v 1.8 2007/01/04 17:14:29 a1s Exp $
 #
 # roundup.pot revision 1.20
 #
@@ -2947,7 +2947,7 @@
 
 #: ../templates/classic/html/issue.search.html:215
 msgid "*: The \"all text\" field will look in message bodies and issue titles"
-msgstr "*: Laukas „visas tekstas“ iššoks pranešimų tekste ir kreipinių pavadinimuose"
+msgstr "*: Laukas „visas tekstas“ ieškos pranešimų tekste ir kreipinių pavadinimuose"
 
 #: ../templates/classic/html/issue.search.html:218
 msgid "**: If you supply a name, the query will be saved off and available as a link in the sidebar"

Modified: tracker/roundup-src/locale/roundup.pot
==============================================================================
--- tracker/roundup-src/locale/roundup.pot	(original)
+++ tracker/roundup-src/locale/roundup.pot	Sun Mar  9 09:26:16 2008
@@ -8,7 +8,7 @@
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: roundup-devel at lists.sourceforge.net\n"
-"POT-Creation-Date: 2006-12-18 13:36+0200\n"
+"POT-Creation-Date: 2007-09-27 11:18+0300\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL at ADDRESS>\n"
 "Language-Team: LANGUAGE <LL at li.org>\n"
@@ -17,25 +17,25 @@
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
 
-#: ../roundup/admin.py:85 ../roundup/admin.py:981 ../roundup/admin.py:1030
-#: ../roundup/admin.py:1052 ../roundup/admin.py:85:981 :1030:1052
+#: ../roundup/admin.py:86 ../roundup/admin.py:989 ../roundup/admin.py:1040
+#: ../roundup/admin.py:1063 ../roundup/admin.py:86:989 :1040:1063
 #, python-format
 msgid "no such class \"%(classname)s\""
 msgstr ""
 
-#: ../roundup/admin.py:95 ../roundup/admin.py:99 ../roundup/admin.py:95:99
+#: ../roundup/admin.py:96 ../roundup/admin.py:100 ../roundup/admin.py:96:100
 #, python-format
 msgid "argument \"%(arg)s\" not propname=value"
 msgstr ""
 
-#: ../roundup/admin.py:112
+#: ../roundup/admin.py:113
 #, python-format
 msgid ""
 "Problem: %(message)s\n"
 "\n"
 msgstr ""
 
-#: ../roundup/admin.py:113
+#: ../roundup/admin.py:114
 #, python-format
 msgid ""
 "%(message)sUsage: roundup-admin [options] [<command> <arguments>]\n"
@@ -62,17 +62,17 @@
 " roundup-admin help all                   -- all available help\n"
 msgstr ""
 
-#: ../roundup/admin.py:140
+#: ../roundup/admin.py:141
 msgid "Commands:"
 msgstr ""
 
-#: ../roundup/admin.py:147
+#: ../roundup/admin.py:148
 msgid ""
 "Commands may be abbreviated as long as the abbreviation\n"
 "matches only one command, e.g. l == li == lis == list."
 msgstr ""
 
-#: ../roundup/admin.py:177
+#: ../roundup/admin.py:178
 msgid ""
 "\n"
 "All commands (except help) require a tracker specifier. This is just\n"
@@ -137,12 +137,12 @@
 "Command help:\n"
 msgstr ""
 
-#: ../roundup/admin.py:240
+#: ../roundup/admin.py:241
 #, python-format
 msgid "%s:"
 msgstr ""
 
-#: ../roundup/admin.py:245
+#: ../roundup/admin.py:246
 msgid ""
 "Usage: help topic\n"
 "        Give help about topic.\n"
@@ -154,20 +154,20 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:268
+#: ../roundup/admin.py:269
 #, python-format
 msgid "Sorry, no help for \"%(topic)s\""
 msgstr ""
 
-#: ../roundup/admin.py:340 ../roundup/admin.py:396 ../roundup/admin.py:340:396
+#: ../roundup/admin.py:346 ../roundup/admin.py:402 ../roundup/admin.py:346:402
 msgid "Templates:"
 msgstr ""
 
-#: ../roundup/admin.py:343 ../roundup/admin.py:407 ../roundup/admin.py:343:407
+#: ../roundup/admin.py:349 ../roundup/admin.py:413 ../roundup/admin.py:349:413
 msgid "Back ends:"
 msgstr ""
 
-#: ../roundup/admin.py:346
+#: ../roundup/admin.py:352
 msgid ""
 "Usage: install [template [backend [key=val[,key=val]]]]\n"
 "        Install a new Roundup tracker.\n"
@@ -193,22 +193,22 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:369 ../roundup/admin.py:466 ../roundup/admin.py:527
-#: ../roundup/admin.py:606 ../roundup/admin.py:656 ../roundup/admin.py:714
-#: ../roundup/admin.py:735 ../roundup/admin.py:763 ../roundup/admin.py:834
-#: ../roundup/admin.py:901 ../roundup/admin.py:972 ../roundup/admin.py:1020
-#: ../roundup/admin.py:1042 ../roundup/admin.py:1072 ../roundup/admin.py:1171
-#: ../roundup/admin.py:1243 ../roundup/admin.py:369:466 :1020:1042 :1072:1171
-#: :1243 :527:606 :656:714 :735:763 :834:901 :972
+#: ../roundup/admin.py:375 ../roundup/admin.py:472 ../roundup/admin.py:533
+#: ../roundup/admin.py:612 ../roundup/admin.py:663 ../roundup/admin.py:721
+#: ../roundup/admin.py:742 ../roundup/admin.py:770 ../roundup/admin.py:842
+#: ../roundup/admin.py:909 ../roundup/admin.py:980 ../roundup/admin.py:1030
+#: ../roundup/admin.py:1053 ../roundup/admin.py:1084 ../roundup/admin.py:1180
+#: ../roundup/admin.py:1253 ../roundup/admin.py:375:472 :1030:1053 :1084:1180
+#: :1253 :533:612 :663:721 :742:770 :842:909 :980
 msgid "Not enough arguments supplied"
 msgstr ""
 
-#: ../roundup/admin.py:375
+#: ../roundup/admin.py:381
 #, python-format
 msgid "Instance home parent directory \"%(parent)s\" does not exist"
 msgstr ""
 
-#: ../roundup/admin.py:383
+#: ../roundup/admin.py:389
 #, python-format
 msgid ""
 "WARNING: There appears to be a tracker in \"%(tracker_home)s\"!\n"
@@ -216,20 +216,20 @@
 "Erase it? Y/N: "
 msgstr ""
 
-#: ../roundup/admin.py:398
+#: ../roundup/admin.py:404
 msgid "Select template [classic]: "
 msgstr ""
 
-#: ../roundup/admin.py:409
+#: ../roundup/admin.py:415
 msgid "Select backend [anydbm]: "
 msgstr ""
 
-#: ../roundup/admin.py:419
+#: ../roundup/admin.py:425
 #, python-format
 msgid "Error in configuration settings: \"%s\""
 msgstr ""
 
-#: ../roundup/admin.py:428
+#: ../roundup/admin.py:434
 #, python-format
 msgid ""
 "\n"
@@ -238,11 +238,11 @@
 "   %(config_file)s"
 msgstr ""
 
-#: ../roundup/admin.py:438
+#: ../roundup/admin.py:444
 msgid " ... at a minimum, you must set following options:"
 msgstr ""
 
-#: ../roundup/admin.py:443
+#: ../roundup/admin.py:449
 #, python-format
 msgid ""
 "\n"
@@ -258,7 +258,7 @@
 "---------------------------------------------------------------------------\n"
 msgstr ""
 
-#: ../roundup/admin.py:461
+#: ../roundup/admin.py:467
 msgid ""
 "Usage: genconfig <filename>\n"
 "        Generate a new tracker config file (ini style) with default values\n"
@@ -267,7 +267,7 @@
 msgstr ""
 
 #. password
-#: ../roundup/admin.py:471
+#: ../roundup/admin.py:477
 msgid ""
 "Usage: initialise [adminpw]\n"
 "        Initialise a new Roundup tracker.\n"
@@ -278,30 +278,30 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:485
+#: ../roundup/admin.py:491
 msgid "Admin Password: "
 msgstr ""
 
-#: ../roundup/admin.py:486
+#: ../roundup/admin.py:492
 msgid "       Confirm: "
 msgstr ""
 
-#: ../roundup/admin.py:490
+#: ../roundup/admin.py:496
 msgid "Instance home does not exist"
 msgstr ""
 
-#: ../roundup/admin.py:494
+#: ../roundup/admin.py:500
 msgid "Instance has not been installed"
 msgstr ""
 
-#: ../roundup/admin.py:499
+#: ../roundup/admin.py:505
 msgid ""
 "WARNING: The database is already initialised!\n"
 "If you re-initialise it, you will lose all the data!\n"
 "Erase it? Y/N: "
 msgstr ""
 
-#: ../roundup/admin.py:520
+#: ../roundup/admin.py:526
 msgid ""
 "Usage: get property designator[,designator]*\n"
 "        Get the given property of one or more designator(s).\n"
@@ -311,23 +311,23 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:560 ../roundup/admin.py:575 ../roundup/admin.py:560:575
+#: ../roundup/admin.py:566 ../roundup/admin.py:581 ../roundup/admin.py:566:581
 #, python-format
 msgid "property %s is not of type Multilink or Link so -d flag does not apply."
 msgstr ""
 
-#: ../roundup/admin.py:583 ../roundup/admin.py:983 ../roundup/admin.py:1032
-#: ../roundup/admin.py:1054 ../roundup/admin.py:583:983 :1032:1054
+#: ../roundup/admin.py:589 ../roundup/admin.py:991 ../roundup/admin.py:1042
+#: ../roundup/admin.py:1065 ../roundup/admin.py:589:991 :1042:1065
 #, python-format
 msgid "no such %(classname)s node \"%(nodeid)s\""
 msgstr ""
 
-#: ../roundup/admin.py:585
+#: ../roundup/admin.py:591
 #, python-format
 msgid "no such %(classname)s property \"%(propname)s\""
 msgstr ""
 
-#: ../roundup/admin.py:594
+#: ../roundup/admin.py:600
 msgid ""
 "Usage: set items property=value property=value ...\n"
 "        Set the given properties of one or more items(s).\n"
@@ -342,7 +342,7 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:648
+#: ../roundup/admin.py:655
 msgid ""
 "Usage: find classname propname=value ...\n"
 "        Find the nodes of the given class with a given link property value.\n"
@@ -353,13 +353,13 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:701 ../roundup/admin.py:854 ../roundup/admin.py:866
-#: ../roundup/admin.py:920 ../roundup/admin.py:701:854 :866:920
+#: ../roundup/admin.py:708 ../roundup/admin.py:862 ../roundup/admin.py:874
+#: ../roundup/admin.py:928 ../roundup/admin.py:708:862 :874:928
 #, python-format
 msgid "%(classname)s has no property \"%(propname)s\""
 msgstr ""
 
-#: ../roundup/admin.py:708
+#: ../roundup/admin.py:715
 msgid ""
 "Usage: specification classname\n"
 "        Show the properties for a classname.\n"
@@ -368,17 +368,17 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:723
+#: ../roundup/admin.py:730
 #, python-format
 msgid "%(key)s: %(value)s (key property)"
 msgstr ""
 
-#: ../roundup/admin.py:725
+#: ../roundup/admin.py:732 ../roundup/admin.py:759 ../roundup/admin.py:732:759
 #, python-format
 msgid "%(key)s: %(value)s"
 msgstr ""
 
-#: ../roundup/admin.py:728
+#: ../roundup/admin.py:735
 msgid ""
 "Usage: display designator[,designator]*\n"
 "        Show the property values for the given node(s).\n"
@@ -388,12 +388,7 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:752
-#, python-format
-msgid "%(key)s: %(value)r"
-msgstr ""
-
-#: ../roundup/admin.py:755
+#: ../roundup/admin.py:762
 msgid ""
 "Usage: create classname property=value ...\n"
 "        Create a new entry of a given class.\n"
@@ -405,31 +400,31 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:782
+#: ../roundup/admin.py:789
 #, python-format
 msgid "%(propname)s (Password): "
 msgstr ""
 
-#: ../roundup/admin.py:784
+#: ../roundup/admin.py:791
 #, python-format
 msgid "   %(propname)s (Again): "
 msgstr ""
 
-#: ../roundup/admin.py:786
+#: ../roundup/admin.py:793
 msgid "Sorry, try again..."
 msgstr ""
 
-#: ../roundup/admin.py:790
+#: ../roundup/admin.py:797
 #, python-format
 msgid "%(propname)s (%(proptype)s): "
 msgstr ""
 
-#: ../roundup/admin.py:808
+#: ../roundup/admin.py:815
 #, python-format
 msgid "you must provide the \"%(propname)s\" property."
 msgstr ""
 
-#: ../roundup/admin.py:819
+#: ../roundup/admin.py:827
 msgid ""
 "Usage: list classname [property]\n"
 "        List the instances of a class.\n"
@@ -445,16 +440,16 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:832
+#: ../roundup/admin.py:840
 msgid "Too many arguments supplied"
 msgstr ""
 
-#: ../roundup/admin.py:868
+#: ../roundup/admin.py:876
 #, python-format
 msgid "%(nodeid)4s: %(value)s"
 msgstr ""
 
-#: ../roundup/admin.py:872
+#: ../roundup/admin.py:880
 msgid ""
 "Usage: table classname [property[,property]*]\n"
 "        List the instances of a class in tabular form.\n"
@@ -486,12 +481,12 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:916
+#: ../roundup/admin.py:924
 #, python-format
 msgid "\"%(spec)s\" not name:width"
 msgstr ""
 
-#: ../roundup/admin.py:966
+#: ../roundup/admin.py:974
 msgid ""
 "Usage: history designator\n"
 "        Show the history entries of a designator.\n"
@@ -500,7 +495,7 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:987
+#: ../roundup/admin.py:995
 msgid ""
 "Usage: commit\n"
 "        Commit changes made to the database during an interactive session.\n"
@@ -514,7 +509,7 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:1001
+#: ../roundup/admin.py:1010
 msgid ""
 "Usage: rollback\n"
 "        Undo all changes that are pending commit to the database.\n"
@@ -526,7 +521,7 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:1013
+#: ../roundup/admin.py:1023
 msgid ""
 "Usage: retire designator[,designator]*\n"
 "        Retire the node specified by designator.\n"
@@ -536,7 +531,7 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:1036
+#: ../roundup/admin.py:1047
 msgid ""
 "Usage: restore designator[,designator]*\n"
 "        Restore the retired node specified by designator.\n"
@@ -546,7 +541,7 @@
 msgstr ""
 
 #. grab the directory to export to
-#: ../roundup/admin.py:1058
+#: ../roundup/admin.py:1070
 msgid ""
 "Usage: export [[-]class[,class]] export_dir\n"
 "        Export the database to colon-separated-value files.\n"
@@ -562,7 +557,7 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:1136
+#: ../roundup/admin.py:1145
 msgid ""
 "Usage: exporttables [[-]class[,class]] export_dir\n"
 "        Export the database to colon-separated-value files, excluding the\n"
@@ -579,7 +574,7 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:1151
+#: ../roundup/admin.py:1160
 msgid ""
 "Usage: import import_dir\n"
 "        Import a database from the directory containing CSV files,\n"
@@ -602,7 +597,7 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:1225
+#: ../roundup/admin.py:1235
 msgid ""
 "Usage: pack period | date\n"
 "\n"
@@ -624,11 +619,11 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:1253
+#: ../roundup/admin.py:1263
 msgid "Invalid format"
 msgstr ""
 
-#: ../roundup/admin.py:1263
+#: ../roundup/admin.py:1274
 msgid ""
 "Usage: reindex [classname|designator]*\n"
 "        Re-generate a tracker's search indexes.\n"
@@ -638,137 +633,170 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:1277
+#: ../roundup/admin.py:1288
 #, python-format
 msgid "no such item \"%(designator)s\""
 msgstr ""
 
-#: ../roundup/admin.py:1287
+#: ../roundup/admin.py:1298
 msgid ""
 "Usage: security [Role name]\n"
 "        Display the Permissions available to one or all Roles.\n"
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:1295
+#: ../roundup/admin.py:1306
 #, python-format
 msgid "No such Role \"%(role)s\""
 msgstr ""
 
-#: ../roundup/admin.py:1301
+#: ../roundup/admin.py:1312
 #, python-format
 msgid "New Web users get the Roles \"%(role)s\""
 msgstr ""
 
-#: ../roundup/admin.py:1303
+#: ../roundup/admin.py:1314
 #, python-format
 msgid "New Web users get the Role \"%(role)s\""
 msgstr ""
 
-#: ../roundup/admin.py:1306
+#: ../roundup/admin.py:1317
 #, python-format
 msgid "New Email users get the Roles \"%(role)s\""
 msgstr ""
 
-#: ../roundup/admin.py:1308
+#: ../roundup/admin.py:1319
 #, python-format
 msgid "New Email users get the Role \"%(role)s\""
 msgstr ""
 
-#: ../roundup/admin.py:1311
+#: ../roundup/admin.py:1322
 #, python-format
 msgid "Role \"%(name)s\":"
 msgstr ""
 
-#: ../roundup/admin.py:1316
+#: ../roundup/admin.py:1327
 #, python-format
 msgid " %(description)s (%(name)s for \"%(klass)s\": %(properties)s only)"
 msgstr ""
 
-#: ../roundup/admin.py:1319
+#: ../roundup/admin.py:1330
 #, python-format
 msgid " %(description)s (%(name)s for \"%(klass)s\" only)"
 msgstr ""
 
-#: ../roundup/admin.py:1322
+#: ../roundup/admin.py:1333
 #, python-format
 msgid " %(description)s (%(name)s)"
 msgstr ""
 
-#: ../roundup/admin.py:1351
+#: ../roundup/admin.py:1362
 #, python-format
 msgid "Unknown command \"%(command)s\" (\"help commands\" for a list)"
 msgstr ""
 
-#: ../roundup/admin.py:1357
+#: ../roundup/admin.py:1368
 #, python-format
 msgid "Multiple commands match \"%(command)s\": %(list)s"
 msgstr ""
 
-#: ../roundup/admin.py:1364
+#: ../roundup/admin.py:1375
 msgid "Enter tracker home: "
 msgstr ""
 
-#: ../roundup/admin.py:1371 ../roundup/admin.py:1377 ../roundup/admin.py:1397
-#: ../roundup/admin.py:1371:1377 :1397
+#: ../roundup/admin.py:1382 ../roundup/admin.py:1388 ../roundup/admin.py:1408
+#: ../roundup/admin.py:1382:1388 :1408
 #, python-format
 msgid "Error: %(message)s"
 msgstr ""
 
-#: ../roundup/admin.py:1385
+#: ../roundup/admin.py:1396
 #, python-format
 msgid "Error: Couldn't open tracker: %(message)s"
 msgstr ""
 
-#: ../roundup/admin.py:1410
+#: ../roundup/admin.py:1421
 #, python-format
 msgid ""
 "Roundup %s ready for input.\n"
 "Type \"help\" for help."
 msgstr ""
 
-#: ../roundup/admin.py:1415
+#: ../roundup/admin.py:1426
 msgid "Note: command history and editing not available"
 msgstr ""
 
-#: ../roundup/admin.py:1419
+#: ../roundup/admin.py:1430
 msgid "roundup> "
 msgstr ""
 
-#: ../roundup/admin.py:1421
+#: ../roundup/admin.py:1432
 msgid "exit..."
 msgstr ""
 
-#: ../roundup/admin.py:1431
+#: ../roundup/admin.py:1442
 msgid "There are unsaved changes. Commit them (y/N)? "
 msgstr ""
 
-#: ../roundup/backends/back_anydbm.py:2000
+#: ../roundup/backends/back_anydbm.py:219
+#: ../roundup/backends/sessions_dbm.py:50
+msgid "Couldn't identify database type"
+msgstr ""
+
+#: ../roundup/backends/back_anydbm.py:245
+#, python-format
+msgid "Couldn't open database - the required module '%s' is not available"
+msgstr ""
+
+#: ../roundup/backends/back_anydbm.py:795
+#: ../roundup/backends/back_anydbm.py:1070
+#: ../roundup/backends/back_anydbm.py:1267
+#: ../roundup/backends/back_anydbm.py:1285
+#: ../roundup/backends/back_anydbm.py:1331
+#: ../roundup/backends/back_anydbm.py:1901
+#: ../roundup/backends/back_anydbm.py:795:1070
+#: ../roundup/backends/back_metakit.py:567
+#: ../roundup/backends/back_metakit.py:834
+#: ../roundup/backends/back_metakit.py:866
+#: ../roundup/backends/back_metakit.py:1601
+#: ../roundup/backends/back_metakit.py:567:834
+#: ../roundup/backends/rdbms_common.py:1320
+#: ../roundup/backends/rdbms_common.py:1549
+#: ../roundup/backends/rdbms_common.py:1755
+#: ../roundup/backends/rdbms_common.py:1775
+#: ../roundup/backends/rdbms_common.py:1828
+#: ../roundup/backends/rdbms_common.py:2436
+#: ../roundup/backends/rdbms_common.py:1320:1549 :1267:1285 :1331:1901
+#: :1755:1775 :1828:2436 :866:1601
+msgid "Database open read-only"
+msgstr ""
+
+#: ../roundup/backends/back_anydbm.py:2003
 #, python-format
 msgid "WARNING: invalid date tuple %r"
 msgstr ""
 
-#: ../roundup/backends/rdbms_common.py:1442
+#: ../roundup/backends/rdbms_common.py:1449
 msgid "create"
 msgstr ""
 
-#: ../roundup/backends/rdbms_common.py:1608
+#: ../roundup/backends/rdbms_common.py:1615
 msgid "unlink"
 msgstr ""
 
-#: ../roundup/backends/rdbms_common.py:1612
+#: ../roundup/backends/rdbms_common.py:1619
 msgid "link"
 msgstr ""
 
-#: ../roundup/backends/rdbms_common.py:1732
+#: ../roundup/backends/rdbms_common.py:1741
 msgid "set"
 msgstr ""
 
-#: ../roundup/backends/rdbms_common.py:1756
+#: ../roundup/backends/rdbms_common.py:1765
 msgid "retired"
 msgstr ""
 
-#: ../roundup/backends/rdbms_common.py:1786
+#: ../roundup/backends/rdbms_common.py:1795
 msgid "restored"
 msgstr ""
 
@@ -799,124 +827,124 @@
 msgid "%(classname)s %(itemid)s has been retired"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:174 ../roundup/cgi/actions.py:202
-#: ../roundup/cgi/actions.py:174:202
+#: ../roundup/cgi/actions.py:169 ../roundup/cgi/actions.py:197
+#: ../roundup/cgi/actions.py:169:197
 msgid "You do not have permission to edit queries"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:180 ../roundup/cgi/actions.py:209
-#: ../roundup/cgi/actions.py:180:209
+#: ../roundup/cgi/actions.py:175 ../roundup/cgi/actions.py:204
+#: ../roundup/cgi/actions.py:175:204
 msgid "You do not have permission to store queries"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:298
+#: ../roundup/cgi/actions.py:310
 #, python-format
 msgid "Not enough values on line %(line)s"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:345
+#: ../roundup/cgi/actions.py:357
 msgid "Items edited OK"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:405
+#: ../roundup/cgi/actions.py:416
 #, python-format
 msgid "%(class)s %(id)s %(properties)s edited ok"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:408
+#: ../roundup/cgi/actions.py:419
 #, python-format
 msgid "%(class)s %(id)s - nothing changed"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:420
+#: ../roundup/cgi/actions.py:431
 #, python-format
 msgid "%(class)s %(id)s created"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:452
+#: ../roundup/cgi/actions.py:463
 #, python-format
 msgid "You do not have permission to edit %(class)s"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:464
+#: ../roundup/cgi/actions.py:475
 #, python-format
 msgid "You do not have permission to create %(class)s"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:488
+#: ../roundup/cgi/actions.py:499
 msgid "You do not have permission to edit user roles"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:538
+#: ../roundup/cgi/actions.py:549
 #, python-format
 msgid ""
 "Edit Error: someone else has edited this %s (%s). View <a target=\"new\" href="
 "\"%s%s\">their changes</a> in a new window."
 msgstr ""
 
-#: ../roundup/cgi/actions.py:566
+#: ../roundup/cgi/actions.py:577
 #, python-format
 msgid "Edit Error: %s"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:597 ../roundup/cgi/actions.py:608
-#: ../roundup/cgi/actions.py:779 ../roundup/cgi/actions.py:798
-#: ../roundup/cgi/actions.py:597:608 :779:798
+#: ../roundup/cgi/actions.py:608 ../roundup/cgi/actions.py:619
+#: ../roundup/cgi/actions.py:790 ../roundup/cgi/actions.py:809
+#: ../roundup/cgi/actions.py:608:619 :790:809
 #, python-format
 msgid "Error: %s"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:634
+#: ../roundup/cgi/actions.py:645
 msgid ""
 "Invalid One Time Key!\n"
 "(a Mozilla bug may cause this message to show up erroneously, please check "
 "your email)"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:676
+#: ../roundup/cgi/actions.py:687
 #, python-format
 msgid "Password reset and email sent to %s"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:685
+#: ../roundup/cgi/actions.py:696
 msgid "Unknown username"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:693
+#: ../roundup/cgi/actions.py:704
 msgid "Unknown email address"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:698
+#: ../roundup/cgi/actions.py:709
 msgid "You need to specify a username or address"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:723
+#: ../roundup/cgi/actions.py:734
 #, python-format
 msgid "Email sent to %s"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:742
+#: ../roundup/cgi/actions.py:753
 msgid "You are now registered, welcome!"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:787
+#: ../roundup/cgi/actions.py:798
 msgid "It is not permitted to supply roles at registration."
 msgstr ""
 
-#: ../roundup/cgi/actions.py:879
+#: ../roundup/cgi/actions.py:890
 msgid "You are logged out"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:896
+#: ../roundup/cgi/actions.py:907
 msgid "Username required"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:931 ../roundup/cgi/actions.py:935
-#: ../roundup/cgi/actions.py:931:935
+#: ../roundup/cgi/actions.py:942 ../roundup/cgi/actions.py:946
+#: ../roundup/cgi/actions.py:942:946
 msgid "Invalid login"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:941
+#: ../roundup/cgi/actions.py:952
 msgid "You do not have permission to login"
 msgstr ""
 
@@ -990,7 +1018,7 @@
 msgid "<em>undefined</em>"
 msgstr ""
 
-#: ../roundup/cgi/client.py:49
+#: ../roundup/cgi/client.py:51
 msgid ""
 "<html><head><title>An error has occurred</title></head>\n"
 "<body><h1>An error has occurred</h1>\n"
@@ -999,29 +1027,29 @@
 "</body></html>"
 msgstr ""
 
-#: ../roundup/cgi/client.py:326
+#: ../roundup/cgi/client.py:377
 msgid "Form Error: "
 msgstr ""
 
-#: ../roundup/cgi/client.py:381
+#: ../roundup/cgi/client.py:432
 #, python-format
 msgid "Unrecognized charset: %r"
 msgstr ""
 
-#: ../roundup/cgi/client.py:509
+#: ../roundup/cgi/client.py:560
 msgid "Anonymous users are not allowed to use the web interface"
 msgstr ""
 
-#: ../roundup/cgi/client.py:664
+#: ../roundup/cgi/client.py:715
 msgid "You are not allowed to view this file."
 msgstr ""
 
-#: ../roundup/cgi/client.py:758
+#: ../roundup/cgi/client.py:808
 #, python-format
 msgid "%(starttag)sTime elapsed: %(seconds)fs%(endtag)s\n"
 msgstr ""
 
-#: ../roundup/cgi/client.py:762
+#: ../roundup/cgi/client.py:812
 #, python-format
 msgid ""
 "%(starttag)sCache hits: %(cache_hits)d, misses %(cache_misses)d. Loading "
@@ -1079,251 +1107,303 @@
 msgid "File is empty"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:73
+#: ../roundup/cgi/templating.py:77
 #, python-format
 msgid "You are not allowed to %(action)s items of class %(class)s"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:645
+#: ../roundup/cgi/templating.py:657
 msgid "(list)"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:714
+#: ../roundup/cgi/templating.py:726
 msgid "Submit New Entry"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:728 ../roundup/cgi/templating.py:862
-#: ../roundup/cgi/templating.py:1269 ../roundup/cgi/templating.py:1298
-#: ../roundup/cgi/templating.py:1318 ../roundup/cgi/templating.py:1364
-#: ../roundup/cgi/templating.py:1387 ../roundup/cgi/templating.py:1423
-#: ../roundup/cgi/templating.py:1460 ../roundup/cgi/templating.py:1513
-#: ../roundup/cgi/templating.py:1530 ../roundup/cgi/templating.py:1614
-#: ../roundup/cgi/templating.py:1634 ../roundup/cgi/templating.py:1652
-#: ../roundup/cgi/templating.py:1684 ../roundup/cgi/templating.py:1694
-#: ../roundup/cgi/templating.py:1746 ../roundup/cgi/templating.py:1935
-#: ../roundup/cgi/templating.py:728:862 :1269:1298 :1318:1364 :1387:1423
-#: :1460:1513 :1530:1614 :1634:1652 :1684:1694 :1746:1935
+#: ../roundup/cgi/templating.py:740 ../roundup/cgi/templating.py:873
+#: ../roundup/cgi/templating.py:1294 ../roundup/cgi/templating.py:1323
+#: ../roundup/cgi/templating.py:1343 ../roundup/cgi/templating.py:1356
+#: ../roundup/cgi/templating.py:1407 ../roundup/cgi/templating.py:1430
+#: ../roundup/cgi/templating.py:1466 ../roundup/cgi/templating.py:1503
+#: ../roundup/cgi/templating.py:1556 ../roundup/cgi/templating.py:1573
+#: ../roundup/cgi/templating.py:1657 ../roundup/cgi/templating.py:1677
+#: ../roundup/cgi/templating.py:1695 ../roundup/cgi/templating.py:1727
+#: ../roundup/cgi/templating.py:1737 ../roundup/cgi/templating.py:1789
+#: ../roundup/cgi/templating.py:1978 ../roundup/cgi/templating.py:740:873
+#: :1294:1323 :1343:1356 :1407:1430 :1466:1503 :1556:1573 :1657:1677 :1695:1727
+#: :1737:1789 :1978
 msgid "[hidden]"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:729
+#: ../roundup/cgi/templating.py:741
 msgid "New node - no history"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:844
+#: ../roundup/cgi/templating.py:855
 msgid "Submit Changes"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:926
+#: ../roundup/cgi/templating.py:937
 msgid "<em>The indicated property no longer exists</em>"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:927
+#: ../roundup/cgi/templating.py:938
 #, python-format
 msgid "<em>%s: %s</em>\n"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:940
+#: ../roundup/cgi/templating.py:951
 #, python-format
 msgid "The linked class %(classname)s no longer exists"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:973 ../roundup/cgi/templating.py:997
-#: ../roundup/cgi/templating.py:973:997
+#: ../roundup/cgi/templating.py:984 ../roundup/cgi/templating.py:1008
+#: ../roundup/cgi/templating.py:984:1008
 msgid "<strike>The linked node no longer exists</strike>"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:1050
+#: ../roundup/cgi/templating.py:1061
 #, python-format
 msgid "%s: (no value)"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:1062
+#: ../roundup/cgi/templating.py:1073
 msgid ""
 "<strong><em>This event is not handled by the history display!</em></strong>"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:1074
+#: ../roundup/cgi/templating.py:1085
 msgid "<tr><td colspan=4><strong>Note:</strong></td></tr>"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:1083
+#: ../roundup/cgi/templating.py:1094
 msgid "History"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:1085
+#: ../roundup/cgi/templating.py:1096
 msgid "<th>Date</th>"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:1086
+#: ../roundup/cgi/templating.py:1097
 msgid "<th>User</th>"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:1087
+#: ../roundup/cgi/templating.py:1098
 msgid "<th>Action</th>"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:1088
+#: ../roundup/cgi/templating.py:1099
 msgid "<th>Args</th>"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:1130
+#: ../roundup/cgi/templating.py:1141
 #, python-format
 msgid "Copy of %(class)s %(id)s"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:1391
+#: ../roundup/cgi/templating.py:1434
 msgid "*encrypted*"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:1464 ../roundup/cgi/templating.py:1485
-#: ../roundup/cgi/templating.py:1491 ../roundup/cgi/templating.py:1039:1464
-#: :1485:1491
+#: ../roundup/cgi/templating.py:1507 ../roundup/cgi/templating.py:1528
+#: ../roundup/cgi/templating.py:1534 ../roundup/cgi/templating.py:1050:1507
+#: :1528:1534
 msgid "No"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:1464 ../roundup/cgi/templating.py:1483
-#: ../roundup/cgi/templating.py:1488 ../roundup/cgi/templating.py:1039:1464
-#: :1483:1488
+#: ../roundup/cgi/templating.py:1507 ../roundup/cgi/templating.py:1526
+#: ../roundup/cgi/templating.py:1531 ../roundup/cgi/templating.py:1050:1507
+#: :1526:1531
 msgid "Yes"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:1577
+#: ../roundup/cgi/templating.py:1620
 msgid ""
 "default value for DateHTMLProperty must be either DateHTMLProperty or string "
 "date representation."
 msgstr ""
 
-#: ../roundup/cgi/templating.py:1737
+#: ../roundup/cgi/templating.py:1780
 #, python-format
 msgid "Attempt to look up %(attr)s on a missing value"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:1810
+#: ../roundup/cgi/templating.py:1853
 #, python-format
 msgid "<option %svalue=\"-1\">- no selection -</option>"
 msgstr ""
 
-#: ../roundup/date.py:301
+#: ../roundup/date.py:300
 msgid ""
 "Not a date spec: \"yyyy-mm-dd\", \"mm-dd\", \"HH:MM\", \"HH:MM:SS\" or \"yyyy-"
 "mm-dd.HH:MM:SS.SSS\""
 msgstr ""
 
-#: ../roundup/date.py:363
+#: ../roundup/date.py:359
 #, python-format
 msgid ""
 "%r not a date / time spec \"yyyy-mm-dd\", \"mm-dd\", \"HH:MM\", \"HH:MM:SS\" "
 "or \"yyyy-mm-dd.HH:MM:SS.SSS\""
 msgstr ""
 
-#: ../roundup/date.py:662
+#: ../roundup/date.py:666
 msgid ""
 "Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [date spec]"
 msgstr ""
 
-#: ../roundup/date.py:681
+#: ../roundup/date.py:685
 msgid "Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]"
 msgstr ""
 
-#: ../roundup/date.py:818
+#: ../roundup/date.py:822
 #, python-format
 msgid "%(number)s year"
 msgid_plural "%(number)s years"
 msgstr[0] ""
 msgstr[1] ""
 
-#: ../roundup/date.py:822
+#: ../roundup/date.py:826
 #, python-format
 msgid "%(number)s month"
 msgid_plural "%(number)s months"
 msgstr[0] ""
 msgstr[1] ""
 
-#: ../roundup/date.py:826
+#: ../roundup/date.py:830
 #, python-format
 msgid "%(number)s week"
 msgid_plural "%(number)s weeks"
 msgstr[0] ""
 msgstr[1] ""
 
-#: ../roundup/date.py:830
+#: ../roundup/date.py:834
 #, python-format
 msgid "%(number)s day"
 msgid_plural "%(number)s days"
 msgstr[0] ""
 msgstr[1] ""
 
-#: ../roundup/date.py:834
+#: ../roundup/date.py:838
 msgid "tomorrow"
 msgstr ""
 
-#: ../roundup/date.py:836
+#: ../roundup/date.py:840
 msgid "yesterday"
 msgstr ""
 
-#: ../roundup/date.py:839
+#: ../roundup/date.py:843
 #, python-format
 msgid "%(number)s hour"
 msgid_plural "%(number)s hours"
 msgstr[0] ""
 msgstr[1] ""
 
-#: ../roundup/date.py:843
+#: ../roundup/date.py:847
 msgid "an hour"
 msgstr ""
 
-#: ../roundup/date.py:845
+#: ../roundup/date.py:849
 msgid "1 1/2 hours"
 msgstr ""
 
-#: ../roundup/date.py:847
+#: ../roundup/date.py:851
 #, python-format
 msgid "1 %(number)s/4 hours"
 msgid_plural "1 %(number)s/4 hours"
 msgstr[0] ""
 msgstr[1] ""
 
-#: ../roundup/date.py:851
+#: ../roundup/date.py:855
 msgid "in a moment"
 msgstr ""
 
-#: ../roundup/date.py:853
+#: ../roundup/date.py:857
 msgid "just now"
 msgstr ""
 
-#: ../roundup/date.py:856
+#: ../roundup/date.py:860
 msgid "1 minute"
 msgstr ""
 
-#: ../roundup/date.py:859
+#: ../roundup/date.py:863
 #, python-format
 msgid "%(number)s minute"
 msgid_plural "%(number)s minutes"
 msgstr[0] ""
 msgstr[1] ""
 
-#: ../roundup/date.py:862
+#: ../roundup/date.py:866
 msgid "1/2 an hour"
 msgstr ""
 
-#: ../roundup/date.py:864
+#: ../roundup/date.py:868
 #, python-format
 msgid "%(number)s/4 hour"
 msgid_plural "%(number)s/4 hours"
 msgstr[0] ""
 msgstr[1] ""
 
-#: ../roundup/date.py:868
+#: ../roundup/date.py:872
 #, python-format
 msgid "%s ago"
 msgstr ""
 
-#: ../roundup/date.py:870
+#: ../roundup/date.py:874
 #, python-format
 msgid "in %s"
 msgstr ""
 
+#: ../roundup/hyperdb.py:87
+#, python-format
+msgid "property %s: %s"
+msgstr ""
+
+#: ../roundup/hyperdb.py:107
+#, python-format
+msgid "property %s: %r is an invalid date (%s)"
+msgstr ""
+
+#: ../roundup/hyperdb.py:124
+#, python-format
+msgid "property %s: %r is an invalid date interval (%s)"
+msgstr ""
+
+#: ../roundup/hyperdb.py:219
+#, python-format
+msgid "property %s: %r is not currently an element"
+msgstr ""
+
+#: ../roundup/hyperdb.py:263
+#, python-format
+msgid "property %s: %r is not a number"
+msgstr ""
+
+#: ../roundup/hyperdb.py:276
+#, python-format
+msgid "\"%s\" not a node designator"
+msgstr ""
+
+#: ../roundup/hyperdb.py:949 ../roundup/hyperdb.py:957
+#: ../roundup/hyperdb.py:949:957
+#, python-format
+msgid "Not a property name: %s"
+msgstr ""
+
+#: ../roundup/hyperdb.py:1240
+#, python-format
+msgid "property %s: %r is not a %s."
+msgstr ""
+
+#: ../roundup/hyperdb.py:1243
+#, python-format
+msgid "you may only enter ID values for property %s"
+msgstr ""
+
+#: ../roundup/hyperdb.py:1273
+#, python-format
+msgid "%r is not a property of %s"
+msgstr ""
+
 #: ../roundup/init.py:134
 #, python-format
 msgid ""
@@ -1331,13 +1411,45 @@
 "\tcontains old-style template - ignored"
 msgstr ""
 
-#: ../roundup/mailgw.py:583
+#: ../roundup/mailgw.py:199 ../roundup/mailgw.py:211
+#: ../roundup/mailgw.py:199:211
+#, python-format
+msgid "Message signed with unknown key: %s"
+msgstr ""
+
+#: ../roundup/mailgw.py:202
+#, python-format
+msgid "Message signed with an expired key: %s"
+msgstr ""
+
+#: ../roundup/mailgw.py:205
+#, python-format
+msgid "Message signed with a revoked key: %s"
+msgstr ""
+
+#: ../roundup/mailgw.py:208
+msgid "Invalid PGP signature detected."
+msgstr ""
+
+#: ../roundup/mailgw.py:404
+msgid "Unknown multipart/encrypted version."
+msgstr ""
+
+#: ../roundup/mailgw.py:413
+msgid "Unable to decrypt your message."
+msgstr ""
+
+#: ../roundup/mailgw.py:442
+msgid "No PGP signature found in message."
+msgstr ""
+
+#: ../roundup/mailgw.py:749
 msgid ""
 "\n"
 "Emails to Roundup trackers must include a Subject: line!\n"
 msgstr ""
 
-#: ../roundup/mailgw.py:673
+#: ../roundup/mailgw.py:873
 #, python-format
 msgid ""
 "\n"
@@ -1354,30 +1466,46 @@
 "Subject was: '%(subject)s'\n"
 msgstr ""
 
-#: ../roundup/mailgw.py:704
+#: ../roundup/mailgw.py:911
 #, python-format
 msgid ""
 "\n"
-"The class name you identified in the subject line (\"%(classname)s\") does "
-"not exist in the\n"
-"database.\n"
+"The class name you identified in the subject line (\"%(classname)s\") does\n"
+"not exist in the database.\n"
 "\n"
 "Valid class names are: %(validname)s\n"
 "Subject was: \"%(subject)s\"\n"
 msgstr ""
 
-#: ../roundup/mailgw.py:739
+#: ../roundup/mailgw.py:919
+#, python-format
+msgid ""
+"\n"
+"You did not identify a class name in the subject line and there is no\n"
+"default set for this tracker. The subject must contain a class name or\n"
+"designator to indicate the 'topic' of the message. For example:\n"
+"    Subject: [issue] This is a new issue\n"
+"      - this will create a new issue in the tracker with the title 'This is\n"
+"        a new issue'.\n"
+"    Subject: [issue1234] This is a followup to issue 1234\n"
+"      - this will append the message's contents to the existing issue 1234\n"
+"        in the tracker.\n"
+"\n"
+"Subject was: '%(subject)s'\n"
+msgstr ""
+
+#: ../roundup/mailgw.py:960
 #, python-format
 msgid ""
 "\n"
 "I cannot match your message to a node in the database - you need to either\n"
-"supply a full designator (with number, eg \"[issue123]\" or keep the\n"
+"supply a full designator (with number, eg \"[issue123]\") or keep the\n"
 "previous subject title intact so I can match that.\n"
 "\n"
 "Subject was: \"%(subject)s\"\n"
 msgstr ""
 
-#: ../roundup/mailgw.py:772
+#: ../roundup/mailgw.py:993
 #, python-format
 msgid ""
 "\n"
@@ -1387,7 +1515,7 @@
 "Subject was: \"%(subject)s\"\n"
 msgstr ""
 
-#: ../roundup/mailgw.py:800
+#: ../roundup/mailgw.py:1021
 #, python-format
 msgid ""
 "\n"
@@ -1396,7 +1524,7 @@
 "  %(current_class)s\n"
 msgstr ""
 
-#: ../roundup/mailgw.py:823
+#: ../roundup/mailgw.py:1044
 #, python-format
 msgid ""
 "\n"
@@ -1405,30 +1533,30 @@
 "  %(errors)s\n"
 msgstr ""
 
-#: ../roundup/mailgw.py:853
+#: ../roundup/mailgw.py:1084
 #, python-format
 msgid ""
 "\n"
-"You are not a registered user.\n"
+"You are not a registered user.%(registration_info)s\n"
 "\n"
 "Unknown address: %(from_address)s\n"
 msgstr ""
 
-#: ../roundup/mailgw.py:861
+#: ../roundup/mailgw.py:1092
 msgid "You are not permitted to access this tracker."
 msgstr ""
 
-#: ../roundup/mailgw.py:868
+#: ../roundup/mailgw.py:1099
 #, python-format
 msgid "You are not permitted to edit %(classname)s."
 msgstr ""
 
-#: ../roundup/mailgw.py:872
+#: ../roundup/mailgw.py:1103
 #, python-format
 msgid "You are not permitted to create %(classname)s."
 msgstr ""
 
-#: ../roundup/mailgw.py:919
+#: ../roundup/mailgw.py:1150
 #, python-format
 msgid ""
 "\n"
@@ -1438,27 +1566,34 @@
 "Subject was: \"%(subject)s\"\n"
 msgstr ""
 
-#: ../roundup/mailgw.py:947
+#: ../roundup/mailgw.py:1203
+msgid ""
+"\n"
+"This tracker has been configured to require all email be PGP signed or\n"
+"encrypted."
+msgstr ""
+
+#: ../roundup/mailgw.py:1209
 msgid ""
 "\n"
 "Roundup requires the submission to be plain text. The message parser could\n"
 "not find a text/plain part to use.\n"
 msgstr ""
 
-#: ../roundup/mailgw.py:969
+#: ../roundup/mailgw.py:1226
 msgid "You are not permitted to create files."
 msgstr ""
 
-#: ../roundup/mailgw.py:983
+#: ../roundup/mailgw.py:1240
 #, python-format
 msgid "You are not permitted to add files to %(classname)s."
 msgstr ""
 
-#: ../roundup/mailgw.py:1001
+#: ../roundup/mailgw.py:1258
 msgid "You are not permitted to create messages."
 msgstr ""
 
-#: ../roundup/mailgw.py:1009
+#: ../roundup/mailgw.py:1266
 #, python-format
 msgid ""
 "\n"
@@ -1466,17 +1601,17 @@
 "%(error)s\n"
 msgstr ""
 
-#: ../roundup/mailgw.py:1017
+#: ../roundup/mailgw.py:1274
 #, python-format
 msgid "You are not permitted to add messages to %(classname)s."
 msgstr ""
 
-#: ../roundup/mailgw.py:1044
+#: ../roundup/mailgw.py:1301
 #, python-format
 msgid "You are not permitted to edit property %(prop)s of class %(classname)s."
 msgstr ""
 
-#: ../roundup/mailgw.py:1052
+#: ../roundup/mailgw.py:1309
 #, python-format
 msgid ""
 "\n"
@@ -1484,79 +1619,85 @@
 "   %(message)s\n"
 msgstr ""
 
-#: ../roundup/mailgw.py:1074
+#: ../roundup/mailgw.py:1331
 msgid "not of form [arg=value,value,...;arg=value,value,...]"
 msgstr ""
 
-#: ../roundup/roundupdb.py:146
+#: ../roundup/roundupdb.py:147
 msgid "files"
 msgstr ""
 
-#: ../roundup/roundupdb.py:146
+#: ../roundup/roundupdb.py:147
 msgid "messages"
 msgstr ""
 
-#: ../roundup/roundupdb.py:146
+#: ../roundup/roundupdb.py:147
 msgid "nosy"
 msgstr ""
 
-#: ../roundup/roundupdb.py:146
+#: ../roundup/roundupdb.py:147
 msgid "superseder"
 msgstr ""
 
-#: ../roundup/roundupdb.py:146
+#: ../roundup/roundupdb.py:147
 msgid "title"
 msgstr ""
 
-#: ../roundup/roundupdb.py:147
+#: ../roundup/roundupdb.py:148
 msgid "assignedto"
 msgstr ""
 
-#: ../roundup/roundupdb.py:147
-msgid "priority"
+#: ../roundup/roundupdb.py:148
+msgid "keyword"
 msgstr ""
 
-#: ../roundup/roundupdb.py:147
-msgid "status"
+#: ../roundup/roundupdb.py:148
+msgid "priority"
 msgstr ""
 
-#: ../roundup/roundupdb.py:147
-msgid "topic"
+#: ../roundup/roundupdb.py:148
+msgid "status"
 msgstr ""
 
-#: ../roundup/roundupdb.py:150
+#: ../roundup/roundupdb.py:151
 msgid "activity"
 msgstr ""
 
 #. following properties are common for all hyperdb classes
 #. they are listed here to keep things in one place
-#: ../roundup/roundupdb.py:150
+#: ../roundup/roundupdb.py:151
 msgid "actor"
 msgstr ""
 
-#: ../roundup/roundupdb.py:150
+#: ../roundup/roundupdb.py:151
 msgid "creation"
 msgstr ""
 
-#: ../roundup/roundupdb.py:150
+#: ../roundup/roundupdb.py:151
 msgid "creator"
 msgstr ""
 
-#: ../roundup/roundupdb.py:308
+#: ../roundup/roundupdb.py:309
 #, python-format
 msgid "New submission from %(authname)s%(authaddr)s:"
 msgstr ""
 
-#: ../roundup/roundupdb.py:311
+#: ../roundup/roundupdb.py:312
 #, python-format
 msgid "%(authname)s%(authaddr)s added the comment:"
 msgstr ""
 
-#: ../roundup/roundupdb.py:314
-msgid "System message:"
+#: ../roundup/roundupdb.py:315
+#, python-format
+msgid "Change by %(authname)s%(authaddr)s:"
+msgstr ""
+
+#: ../roundup/roundupdb.py:342
+#, python-format
+msgid "File '%(filename)s' not attached - you can download it from %(link)s."
 msgstr ""
 
-#: ../roundup/roundupdb.py:597
+#: ../roundup/roundupdb.py:615
 #, python-format
 msgid ""
 "\n"
@@ -1675,58 +1816,62 @@
 "\"imaps\""
 msgstr ""
 
-#: ../roundup/scripts/roundup_server.py:157
+#: ../roundup/scripts/roundup_server.py:76
+msgid "WARNING: generating temporary SSL certificate"
+msgstr ""
+
+#: ../roundup/scripts/roundup_server.py:253
 msgid ""
 "<html><head><title>Roundup trackers index</title></head>\n"
 "<body><h1>Roundup trackers index</h1><ol>\n"
 msgstr ""
 
-#: ../roundup/scripts/roundup_server.py:293
+#: ../roundup/scripts/roundup_server.py:389
 #, python-format
 msgid "Error: %s: %s"
 msgstr ""
 
-#: ../roundup/scripts/roundup_server.py:303
+#: ../roundup/scripts/roundup_server.py:399
 msgid "WARNING: ignoring \"-g\" argument, not root"
 msgstr ""
 
-#: ../roundup/scripts/roundup_server.py:309
+#: ../roundup/scripts/roundup_server.py:405
 msgid "Can't change groups - no grp module"
 msgstr ""
 
-#: ../roundup/scripts/roundup_server.py:318
+#: ../roundup/scripts/roundup_server.py:414
 #, python-format
 msgid "Group %(group)s doesn't exist"
 msgstr ""
 
-#: ../roundup/scripts/roundup_server.py:329
+#: ../roundup/scripts/roundup_server.py:425
 msgid "Can't run as root!"
 msgstr ""
 
-#: ../roundup/scripts/roundup_server.py:332
+#: ../roundup/scripts/roundup_server.py:428
 msgid "WARNING: ignoring \"-u\" argument, not root"
 msgstr ""
 
-#: ../roundup/scripts/roundup_server.py:338
+#: ../roundup/scripts/roundup_server.py:434
 msgid "Can't change users - no pwd module"
 msgstr ""
 
-#: ../roundup/scripts/roundup_server.py:347
+#: ../roundup/scripts/roundup_server.py:443
 #, python-format
 msgid "User %(user)s doesn't exist"
 msgstr ""
 
-#: ../roundup/scripts/roundup_server.py:481
+#: ../roundup/scripts/roundup_server.py:592
 #, python-format
 msgid "Multiprocess mode \"%s\" is not available, switching to single-process"
 msgstr ""
 
-#: ../roundup/scripts/roundup_server.py:504
+#: ../roundup/scripts/roundup_server.py:620
 #, python-format
 msgid "Unable to bind to port %s, port already in use."
 msgstr ""
 
-#: ../roundup/scripts/roundup_server.py:572
+#: ../roundup/scripts/roundup_server.py:688
 msgid ""
 " -c <Command>  Windows Service options.\n"
 "               If you want to run the server as a Windows Service, you\n"
@@ -1736,7 +1881,7 @@
 "               specifics."
 msgstr ""
 
-#: ../roundup/scripts/roundup_server.py:579
+#: ../roundup/scripts/roundup_server.py:695
 msgid ""
 " -u <UID>      runs the Roundup web server as this UID\n"
 " -g <GID>      runs the Roundup web server as this GID\n"
@@ -1745,7 +1890,7 @@
 "               specified if -d is used."
 msgstr ""
 
-#: ../roundup/scripts/roundup_server.py:586
+#: ../roundup/scripts/roundup_server.py:702
 #, python-format
 msgid ""
 "%(message)sUsage: roundup-server [options] [name=tracker home]*\n"
@@ -1760,6 +1905,9 @@
 " -l <fname>    log to the file indicated by fname instead of stderr/stdout\n"
 " -N            log client machine names instead of IP addresses (much "
 "slower)\n"
+" -i <fname>    set tracker index template\n"
+" -s            enable SSL\n"
+" -e <fname>    PEM file containing SSL key and certificate\n"
 " -t <mode>     multiprocess mode (default: %(mp_def)s).\n"
 "               Allowed values: %(mp_types)s.\n"
 "%(os_part)s\n"
@@ -1800,20 +1948,20 @@
 "   any url-unsafe characters like spaces, as these confuse IE.\n"
 msgstr ""
 
-#: ../roundup/scripts/roundup_server.py:741
+#: ../roundup/scripts/roundup_server.py:860
 msgid "Instances must be name=home"
 msgstr ""
 
-#: ../roundup/scripts/roundup_server.py:755
+#: ../roundup/scripts/roundup_server.py:874
 #, python-format
 msgid "Configuration saved to %s"
 msgstr ""
 
-#: ../roundup/scripts/roundup_server.py:773
+#: ../roundup/scripts/roundup_server.py:892
 msgid "Sorry, you can't run the server as a daemon on this Operating System"
 msgstr ""
 
-#: ../roundup/scripts/roundup_server.py:788
+#: ../roundup/scripts/roundup_server.py:907
 #, python-format
 msgid "Roundup server started on %(HOST)s:%(PORT)s"
 msgstr ""
@@ -1888,21 +2036,21 @@
 
 #: ../templates/classic/html/_generic.help.html:41
 #: ../templates/classic/html/help.html:21
-#: ../templates/classic/html/issue.index.html:80
+#: ../templates/classic/html/issue.index.html:81
 #: ../templates/minimal/html/_generic.help.html:41
 msgid "&lt;&lt; previous"
 msgstr ""
 
 #: ../templates/classic/html/_generic.help.html:53
 #: ../templates/classic/html/help.html:28
-#: ../templates/classic/html/issue.index.html:88
+#: ../templates/classic/html/issue.index.html:89
 #: ../templates/minimal/html/_generic.help.html:53
 msgid "${start}..${end} out of ${total}"
 msgstr ""
 
 #: ../templates/classic/html/_generic.help.html:57
 #: ../templates/classic/html/help.html:32
-#: ../templates/classic/html/issue.index.html:91
+#: ../templates/classic/html/issue.index.html:92
 #: ../templates/minimal/html/_generic.help.html:57
 msgid "next &gt;&gt;"
 msgstr ""
@@ -1932,6 +2080,7 @@
 #: ../templates/minimal/html/_generic.index.html:19
 #: ../templates/minimal/html/_generic.item.html:17
 #: ../templates/minimal/html/user.index.html:13
+#: ../templates/minimal/html/user.item.html:39
 #: ../templates/minimal/html/user.register.html:17
 msgid "Please login with your username and password."
 msgstr ""
@@ -2034,7 +2183,8 @@
 msgstr ""
 
 #: ../templates/classic/html/issue.index.html:32
-msgid "Topic"
+#: ../templates/classic/html/keyword.item.html:37
+msgid "Keyword"
 msgstr ""
 
 #: ../templates/classic/html/issue.index.html:33
@@ -2055,29 +2205,29 @@
 msgid "Assigned&nbsp;To"
 msgstr ""
 
-#: ../templates/classic/html/issue.index.html:104
+#: ../templates/classic/html/issue.index.html:105
 msgid "Download as CSV"
 msgstr ""
 
-#: ../templates/classic/html/issue.index.html:114
+#: ../templates/classic/html/issue.index.html:115
 msgid "Sort on:"
 msgstr ""
 
-#: ../templates/classic/html/issue.index.html:118
-#: ../templates/classic/html/issue.index.html:139
+#: ../templates/classic/html/issue.index.html:119
+#: ../templates/classic/html/issue.index.html:140
 msgid "- nothing -"
 msgstr ""
 
-#: ../templates/classic/html/issue.index.html:126
-#: ../templates/classic/html/issue.index.html:147
+#: ../templates/classic/html/issue.index.html:127
+#: ../templates/classic/html/issue.index.html:148
 msgid "Descending:"
 msgstr ""
 
-#: ../templates/classic/html/issue.index.html:135
+#: ../templates/classic/html/issue.index.html:136
 msgid "Group on:"
 msgstr ""
 
-#: ../templates/classic/html/issue.index.html:154
+#: ../templates/classic/html/issue.index.html:155
 msgid "Redisplay"
 msgstr ""
 
@@ -2122,7 +2272,9 @@
 msgstr ""
 
 #: ../templates/classic/html/issue.item.html:78
-msgid "Topics"
+#: ../templates/classic/html/page.html:103
+#: ../templates/minimal/html/page.html:102
+msgid "Keywords"
 msgstr ""
 
 #: ../templates/classic/html/issue.item.html:86
@@ -2138,9 +2290,9 @@
 msgstr ""
 
 #: ../templates/classic/html/issue.item.html:114
-#: ../templates/classic/html/user.item.html:152
+#: ../templates/classic/html/user.item.html:153
 #: ../templates/classic/html/user.register.html:69
-#: ../templates/minimal/html/user.item.html:147
+#: ../templates/minimal/html/user.item.html:153
 msgid ""
 "<table class=\"form\"> <tr> <td>Note:&nbsp;</td> <th class=\"required"
 "\">highlighted</th> <td>&nbsp;fields are required.</td> </tr> </table>"
@@ -2236,91 +2388,92 @@
 msgstr ""
 
 #: ../templates/classic/html/issue.search.html:56
-msgid "Topic:"
+msgid "Keyword:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:64
+#: ../templates/classic/html/issue.search.html:58
+#: ../templates/classic/html/issue.search.html:123
+#: ../templates/classic/html/issue.search.html:139
+msgid "not selected"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:67
 msgid "ID:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:72
+#: ../templates/classic/html/issue.search.html:75
 msgid "Creation Date:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:83
+#: ../templates/classic/html/issue.search.html:86
 msgid "Creator:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:85
+#: ../templates/classic/html/issue.search.html:88
 msgid "created by me"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:94
+#: ../templates/classic/html/issue.search.html:97
 msgid "Activity:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:105
+#: ../templates/classic/html/issue.search.html:108
 msgid "Actor:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:107
+#: ../templates/classic/html/issue.search.html:110
 msgid "done by me"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:118
+#: ../templates/classic/html/issue.search.html:121
 msgid "Priority:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:120
-#: ../templates/classic/html/issue.search.html:136
-msgid "not selected"
-msgstr ""
-
-#: ../templates/classic/html/issue.search.html:131
+#: ../templates/classic/html/issue.search.html:134
 msgid "Status:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:134
+#: ../templates/classic/html/issue.search.html:137
 msgid "not resolved"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:149
+#: ../templates/classic/html/issue.search.html:152
 msgid "Assigned to:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:152
+#: ../templates/classic/html/issue.search.html:155
 msgid "assigned to me"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:154
+#: ../templates/classic/html/issue.search.html:157
 msgid "unassigned"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:164
+#: ../templates/classic/html/issue.search.html:167
 msgid "No Sort or group:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:172
+#: ../templates/classic/html/issue.search.html:175
 msgid "Pagesize:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:178
+#: ../templates/classic/html/issue.search.html:181
 msgid "Start With:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:184
+#: ../templates/classic/html/issue.search.html:187
 msgid "Sort Descending:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:191
+#: ../templates/classic/html/issue.search.html:194
 msgid "Group Descending:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:198
+#: ../templates/classic/html/issue.search.html:201
 msgid "Query name**:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:210
+#: ../templates/classic/html/issue.search.html:213
 #: ../templates/classic/html/page.html:43
 #: ../templates/classic/html/page.html:92
 #: ../templates/classic/html/user.help-search.html:69
@@ -2329,11 +2482,11 @@
 msgid "Search"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:215
+#: ../templates/classic/html/issue.search.html:218
 msgid "*: The \"all text\" field will look in message bodies and issue titles"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:218
+#: ../templates/classic/html/issue.search.html:221
 msgid ""
 "**: If you supply a name, the query will be saved off and available as a link "
 "in the sidebar"
@@ -2361,10 +2514,6 @@
 msgid "To create a new keyword, enter it below and click \"Submit New Entry\"."
 msgstr ""
 
-#: ../templates/classic/html/keyword.item.html:37
-msgid "Keyword"
-msgstr ""
-
 #: ../templates/classic/html/msg.index.html:3
 msgid "List of messages - ${tracker}"
 msgstr ""
@@ -2441,11 +2590,6 @@
 msgid "Show issue:"
 msgstr ""
 
-#: ../templates/classic/html/page.html:103
-#: ../templates/minimal/html/page.html:102
-msgid "Keywords"
-msgstr ""
-
 #: ../templates/classic/html/page.html:108
 #: ../templates/minimal/html/page.html:107
 msgid "Edit Existing"
@@ -2593,7 +2737,7 @@
 msgstr ""
 
 #: ../templates/classic/html/query.edit.html:67
-#: ../templates/classic/html/query.edit.html:92
+#: ../templates/classic/html/query.edit.html:94
 msgid "edit"
 msgstr ""
 
@@ -2609,11 +2753,11 @@
 msgid "Delete"
 msgstr ""
 
-#: ../templates/classic/html/query.edit.html:94
+#: ../templates/classic/html/query.edit.html:96
 msgid "[not yours to edit]"
 msgstr ""
 
-#: ../templates/classic/html/query.edit.html:102
+#: ../templates/classic/html/query.edit.html:104
 msgid "Save Selection"
 msgstr ""
 
@@ -2735,26 +2879,26 @@
 msgid "User${id} Editing"
 msgstr ""
 
-#: ../templates/classic/html/user.item.html:79
+#: ../templates/classic/html/user.item.html:80
 #: ../templates/classic/html/user.register.html:33
-#: ../templates/minimal/html/user.item.html:74
+#: ../templates/minimal/html/user.item.html:80
 #: ../templates/minimal/html/user.register.html:41
 msgid "Roles"
 msgstr ""
 
-#: ../templates/classic/html/user.item.html:87
-#: ../templates/minimal/html/user.item.html:82
+#: ../templates/classic/html/user.item.html:88
+#: ../templates/minimal/html/user.item.html:88
 msgid "(to give the user more than one role, enter a comma,separated,list)"
 msgstr ""
 
-#: ../templates/classic/html/user.item.html:108
-#: ../templates/minimal/html/user.item.html:103
+#: ../templates/classic/html/user.item.html:109
+#: ../templates/minimal/html/user.item.html:109
 msgid "(this is a numeric hour offset, the default is ${zone})"
 msgstr ""
 
-#: ../templates/classic/html/user.item.html:129
+#: ../templates/classic/html/user.item.html:130
 #: ../templates/classic/html/user.register.html:53
-#: ../templates/minimal/html/user.item.html:124
+#: ../templates/minimal/html/user.item.html:130
 #: ../templates/minimal/html/user.register.html:53
 msgid "Alternate E-mail addresses<br>One address per line"
 msgstr ""

Modified: tracker/roundup-src/locale/ru.po
==============================================================================
--- tracker/roundup-src/locale/ru.po	(original)
+++ tracker/roundup-src/locale/ru.po	Sun Mar  9 09:26:16 2008
@@ -1,16 +1,16 @@
 # Russian message file for Roundup Issue Tracker
 # alexander smishlajev <alex at tycobka.lv>, 2004
 #
-# $Id: ru.po,v 1.15 2006/12/18 12:10:10 a1s Exp $
+# $Id: ru.po,v 1.16 2007/09/16 07:23:04 a1s Exp $
 #
-# roundup.pot revision 1.22
+# roundup.pot revision 1.23
 #
 msgid ""
 msgstr ""
 "Project-Id-Version: Roundup 1.3.2\n"
 "Report-Msgid-Bugs-To: roundup-devel at lists.sourceforge.net\n"
 "POT-Creation-Date: 2006-04-27 09:02+0300\n"
-"PO-Revision-Date: 2006-12-18 13:38+0200\n"
+"PO-Revision-Date: 2007-09-16 10:20+0200\n"
 "Last-Translator: alexander smishlajev <alex at tycobka.lv>\n"
 "Language-Team: Russian\n"
 "MIME-Version: 1.0\n"
@@ -19,21 +19,21 @@
 "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
 "X-Poedit-Language: Russian\n"
 
-#: ../roundup/admin.py:85
-#: ../roundup/admin.py:981
-#: ../roundup/admin.py:1030
-#: ../roundup/admin.py:1052
+#: ../roundup/admin.py:86
+#: ../roundup/admin.py:989
+#: ../roundup/admin.py:1040
+#: ../roundup/admin.py:1063
 #, python-format
 msgid "no such class \"%(classname)s\""
 msgstr "ëÌÁÓÓ \"%(classname)s\" ÎÅ ÓÕÝÅÓÔ×ÕÅÔ"
 
-#: ../roundup/admin.py:95
-#: ../roundup/admin.py:99
+#: ../roundup/admin.py:96
+#: ../roundup/admin.py:100
 #, python-format
 msgid "argument \"%(arg)s\" not propname=value"
 msgstr "ÁÒÇÕÍÅÎÔ \"%(arg)s\" ÄÏÌÖÅÎ ÉÍÅÔØ ×ÉÄ ÉÍÑ=ÚÎÁÞÅÎÉÅ"
 
-#: ../roundup/admin.py:112
+#: ../roundup/admin.py:113
 #, python-format
 msgid ""
 "Problem: %(message)s\n"
@@ -42,7 +42,7 @@
 "ïÛÉÂËÁ: %(message)s\n"
 "\n"
 
-#: ../roundup/admin.py:113
+#: ../roundup/admin.py:114
 #, python-format
 msgid ""
 "%(message)sUsage: roundup-admin [options] [<command> <arguments>]\n"
@@ -89,11 +89,11 @@
 " roundup-admin help <command>             -- ÓÐÒÁ×ËÁ ÐÏ ËÏÍÁÎÄÅ\n"
 " roundup-admin help all                   -- ×ÓÅ ÓÐÒÁ×ÏÞÎÙÅ ÓÏÏÂÝÅÎÉÑ\n"
 
-#: ../roundup/admin.py:140
+#: ../roundup/admin.py:141
 msgid "Commands:"
 msgstr "ëÏÍÁÎÄÙ:"
 
-#: ../roundup/admin.py:147
+#: ../roundup/admin.py:148
 msgid ""
 "Commands may be abbreviated as long as the abbreviation\n"
 "matches only one command, e.g. l == li == lis == list."
@@ -107,7 +107,7 @@
 # ÎÏ ÍÎÅ ÜÔÏ ÓÏ×ÓÅÍ ÎÅ ÎÒÁ×ÉÔÓÑ.
 #
 # ÞÔÏ ÌÕÞÛÅ ÎÁÐÉÓÁÔØ ×ÍÅÓÔÏ "××ÅÓÔÉ Ó ÔÅÒÍÉÎÁÌÁ"?
-#: ../roundup/admin.py:177
+#: ../roundup/admin.py:178
 msgid ""
 "\n"
 "All commands (except help) require a tracker specifier. This is just\n"
@@ -236,12 +236,12 @@
 "\n"
 "óÐÒÁ×ËÁ ÐÏ ËÏÍÁÎÄÁÍ:\n"
 
-#: ../roundup/admin.py:240
+#: ../roundup/admin.py:241
 #, python-format
 msgid "%s:"
 msgstr ""
 
-#: ../roundup/admin.py:245
+#: ../roundup/admin.py:246
 msgid ""
 "Usage: help topic\n"
 "        Give help about topic.\n"
@@ -261,22 +261,22 @@
 "        all       -- ×ÓÅ ÓÐÒÁ×ËÉ\n"
 "        "
 
-#: ../roundup/admin.py:268
+#: ../roundup/admin.py:269
 #, python-format
 msgid "Sorry, no help for \"%(topic)s\""
 msgstr "é×ÉÎÉÔÅ, ÓÐÒÁ×ËÁ \"%(topic)s\" ÎÅ ÓÕÝÅÓÔ×ÕÅÔ."
 
-#: ../roundup/admin.py:340
-#: ../roundup/admin.py:396
+#: ../roundup/admin.py:346
+#: ../roundup/admin.py:402
 msgid "Templates:"
 msgstr "ûÁÂÌÏÎÙ:"
 
-#: ../roundup/admin.py:343
-#: ../roundup/admin.py:407
+#: ../roundup/admin.py:349
+#: ../roundup/admin.py:413
 msgid "Back ends:"
 msgstr "óÅÒ×ÅÒÙ:"
 
-#: ../roundup/admin.py:346
+#: ../roundup/admin.py:352
 msgid ""
 "Usage: install [template [backend [key=val[,key=val]]]]\n"
 "        Install a new Roundup tracker.\n"
@@ -327,31 +327,31 @@
 "        óÍ.ÔÁËÖÅ \"help initopts\".\n"
 "        "
 
-#: ../roundup/admin.py:369
-#: ../roundup/admin.py:466
-#: ../roundup/admin.py:527
-#: ../roundup/admin.py:606
-#: ../roundup/admin.py:656
-#: ../roundup/admin.py:714
-#: ../roundup/admin.py:735
-#: ../roundup/admin.py:763
-#: ../roundup/admin.py:834
-#: ../roundup/admin.py:901
-#: ../roundup/admin.py:972
-#: ../roundup/admin.py:1020
-#: ../roundup/admin.py:1042
-#: ../roundup/admin.py:1072
-#: ../roundup/admin.py:1171
-#: ../roundup/admin.py:1243
+#: ../roundup/admin.py:375
+#: ../roundup/admin.py:472
+#: ../roundup/admin.py:533
+#: ../roundup/admin.py:612
+#: ../roundup/admin.py:663
+#: ../roundup/admin.py:721
+#: ../roundup/admin.py:742
+#: ../roundup/admin.py:770
+#: ../roundup/admin.py:842
+#: ../roundup/admin.py:909
+#: ../roundup/admin.py:980
+#: ../roundup/admin.py:1030
+#: ../roundup/admin.py:1053
+#: ../roundup/admin.py:1084
+#: ../roundup/admin.py:1180
+#: ../roundup/admin.py:1253
 msgid "Not enough arguments supplied"
 msgstr "îÅÄÏÓÔÁÔÏÞÎÏ ÁÒÇÕÍÅÎÔÏ×"
 
-#: ../roundup/admin.py:375
+#: ../roundup/admin.py:381
 #, python-format
 msgid "Instance home parent directory \"%(parent)s\" does not exist"
 msgstr "ëÁÔÁÌÏÇ \"%(parent)s\" ÎÅ ÓÕÝÅÓÔ×ÕÅÔ"
 
-#: ../roundup/admin.py:383
+#: ../roundup/admin.py:389
 #, python-format
 msgid ""
 "WARNING: There appears to be a tracker in \"%(tracker_home)s\"!\n"
@@ -362,20 +362,20 @@
 "ðÏ×ÔÏÒÎÁÑ ÕÓÔÁÎÏ×ËÁ ÕÎÉÞÔÏÖÉÔ ×ÓÅ ×ÁÛÉ ÄÁÎÎÙÅ!\n"
 "õÄÁÌÉÔØ ÓÕÝÅÓÔ×ÕÀÝÉÊ ÔÒÅËÅÒ? Y/N: "
 
-#: ../roundup/admin.py:398
+#: ../roundup/admin.py:404
 msgid "Select template [classic]: "
 msgstr "÷ÙÂÅÒÉÔÅ ÛÁÂÌÏÎ [classic]: "
 
-#: ../roundup/admin.py:409
+#: ../roundup/admin.py:415
 msgid "Select backend [anydbm]: "
 msgstr "÷ÙÂÅÒÉÔÅ ÓÅÒ×ÅÒ [anydbm]: "
 
-#: ../roundup/admin.py:419
+#: ../roundup/admin.py:425
 #, python-format
 msgid "Error in configuration settings: \"%s\""
 msgstr "ïÛÉÂËÁ × ÐÁÒÁÍÅÔÒÁÈ ËÏÎÆÉÇÕÒÁÃÉÉ: \"%s\""
 
-#: ../roundup/admin.py:428
+#: ../roundup/admin.py:434
 #, python-format
 msgid ""
 "\n"
@@ -388,12 +388,12 @@
 " ôÅÐÅÒØ ×ÁÍ ÎÕÖÎÏ ÉÓÐÒÁ×ÉÔØ ËÏÎÆÉÇÕÒÁÃÉÏÎÎÙÊ ÆÁÊÌ ÔÒÅËÅÒÁ:\n"
 "   %(config_file)s"
 
-#: ../roundup/admin.py:438
+#: ../roundup/admin.py:444
 msgid " ... at a minimum, you must set following options:"
 msgstr " ... ËÁË ÍÉÎÉÍÕÍ, ×Ù ÄÏÌÖÎÙ ÕÓÔÁÎÏ×ÉÔØ ÎÁÓÔÒÏÊËÉ:"
 
 # õËÁÚÁÎÏ ÁÎÇÌÉÊÓËÏÅ ÎÁÚ×ÁÎÉÅ ÄÏËÕÍÅÎÔÁ
-#: ../roundup/admin.py:443
+#: ../roundup/admin.py:449
 #, python-format
 msgid ""
 "\n"
@@ -419,7 +419,7 @@
 " ðÏÓÌÅ ÜÔÏÇÏ ×Ù ÄÏÌÖÎÙ ×ÙÐÏÌÎÉÔØ ËÏÍÁÎÄÕ \"roundup-admin initialise\".\n"
 "---------------------------------------------------------------------------\n"
 
-#: ../roundup/admin.py:461
+#: ../roundup/admin.py:467
 msgid ""
 "Usage: genconfig <filename>\n"
 "        Generate a new tracker config file (ini style) with default values\n"
@@ -433,7 +433,7 @@
 
 #  password
 #. password
-#: ../roundup/admin.py:471
+#: ../roundup/admin.py:477
 msgid ""
 "Usage: initialise [adminpw]\n"
 "        Initialise a new Roundup tracker.\n"
@@ -451,23 +451,23 @@
 "        éÎÉÃÉÁÌÉÚÁÃÉÑ ÔÒÅËÅÒÁ ÄÅÌÁÅÔÓÑ ÆÕÎËÃÉÅÊ dbinit.init()\n"
 "        "
 
-#: ../roundup/admin.py:485
+#: ../roundup/admin.py:491
 msgid "Admin Password: "
 msgstr "ðÁÒÏÌØ ÁÄÍÉÎÉÓÔÒÁÔÏÒÁ: "
 
-#: ../roundup/admin.py:486
+#: ../roundup/admin.py:492
 msgid "       Confirm: "
 msgstr "              åÝÅ ÒÁÚ: "
 
-#: ../roundup/admin.py:490
+#: ../roundup/admin.py:496
 msgid "Instance home does not exist"
 msgstr "äÏÍÁÛÎÉÊ ËÁÔÁÌÏÇ ÔÒÅËÅÒÁ ÎÅ ÓÕÝÅÓÔ×ÕÅÔ"
 
-#: ../roundup/admin.py:494
+#: ../roundup/admin.py:500
 msgid "Instance has not been installed"
 msgstr "ôÒÅËÅÒ ÎÅ ÕÓÔÁÎÏ×ÌÅÎ"
 
-#: ../roundup/admin.py:499
+#: ../roundup/admin.py:505
 msgid ""
 "WARNING: The database is already initialised!\n"
 "If you re-initialise it, you will lose all the data!\n"
@@ -477,7 +477,7 @@
 "ðÏ×ÔÏÒÎÁÑ ÉÎÉÃÉÁÌÉÚÁÃÉÑ ÕÎÉÞÔÏÖÉÔ ×ÓÅ ×ÁÛÉ ÄÁÎÎÙÅ!\n"
 "õÄÁÌÉÔØ ÓÕÝÅÓÔ×ÕÀÝÕÀ ÂÁÚÕ? Y/N: "
 
-#: ../roundup/admin.py:520
+#: ../roundup/admin.py:526
 msgid ""
 "Usage: get property designator[,designator]*\n"
 "        Get the given property of one or more designator(s).\n"
@@ -494,26 +494,26 @@
 "        ÐÅÒÅÞÉÓÌÅÎÎÙÈ × ÓÐÉÓËÅ ÏÐÉÓÁÔÅÌÅÊ.\n"
 "        "
 
-#: ../roundup/admin.py:560
-#: ../roundup/admin.py:575
+#: ../roundup/admin.py:566
+#: ../roundup/admin.py:581
 #, python-format
 msgid "property %s is not of type Multilink or Link so -d flag does not apply."
 msgstr "ëÌÀÞ '-d' ÎÅÐÒÉÍÅÎÉÍ, ÐÏÔÏÍÕ ÞÔÏ ÔÉÐ ÁÔÒÉÂÕÔÁ %s - ÎÅ Link É ÎÅ Multilink"
 
-#: ../roundup/admin.py:583
-#: ../roundup/admin.py:983
-#: ../roundup/admin.py:1032
-#: ../roundup/admin.py:1054
+#: ../roundup/admin.py:589
+#: ../roundup/admin.py:991
+#: ../roundup/admin.py:1042
+#: ../roundup/admin.py:1065
 #, python-format
 msgid "no such %(classname)s node \"%(nodeid)s\""
 msgstr "÷ ËÌÁÓÓÅ %(classname)s ÎÅÔ ÏÂßÅËÔÁ \"%(nodeid)s\""
 
-#: ../roundup/admin.py:585
+#: ../roundup/admin.py:591
 #, python-format
 msgid "no such %(classname)s property \"%(propname)s\""
 msgstr "õ ËÌÁÓÓÁ %(classname)s ÎÅÔ ÁÔÒÉÂÕÔÁ \"%(propname)s\""
 
-#: ../roundup/admin.py:594
+#: ../roundup/admin.py:600
 msgid ""
 "Usage: set items property=value property=value ...\n"
 "        Set the given properties of one or more items(s).\n"
@@ -541,7 +541,7 @@
 "        ÁÔÒÉÂÕÔ.  (îÁÐÒÉÍÅÒ, \"1,2,3\".)\n"
 "        "
 
-#: ../roundup/admin.py:648
+#: ../roundup/admin.py:655
 msgid ""
 "Usage: find classname propname=value ...\n"
 "        Find the nodes of the given class with a given link property value.\n"
@@ -559,15 +559,15 @@
 "        ËÏÔÏÒÙÊ ÓÓÙÌÁÅÔÓÑ ÁÔÒÉÂÕÔ, ÉÌÉ ËÌÀÞÏÍ ÜÔÏÇÏ ÏÂßÅËÔÁ.\n"
 "        "
 
-#: ../roundup/admin.py:701
-#: ../roundup/admin.py:854
-#: ../roundup/admin.py:866
-#: ../roundup/admin.py:920
+#: ../roundup/admin.py:708
+#: ../roundup/admin.py:862
+#: ../roundup/admin.py:874
+#: ../roundup/admin.py:928
 #, python-format
 msgid "%(classname)s has no property \"%(propname)s\""
 msgstr "ëÌÁÓÓ %(classname)s ÎÅ ÉÍÅÅÔ ÁÔÒÉÂÕÔÁ \"%(propname)s\""
 
-#: ../roundup/admin.py:708
+#: ../roundup/admin.py:715
 msgid ""
 "Usage: specification classname\n"
 "        Show the properties for a classname.\n"
@@ -581,17 +581,17 @@
 "        ÷ÙÄÁÅÔ ÓÐÉÓÏË ÁÔÒÉÂÕÔÏ× ÕËÁÚÁÎÎÏÇÏ ËÌÁÓÓÁ.\n"
 "        "
 
-#: ../roundup/admin.py:723
+#: ../roundup/admin.py:730
 #, python-format
 msgid "%(key)s: %(value)s (key property)"
 msgstr "%(key)s: %(value)s (ËÌÀÞÅ×ÏÊ ÁÔÒÉÂÕÔ)"
 
-#: ../roundup/admin.py:725
+#: ../roundup/admin.py:732
 #, python-format
 msgid "%(key)s: %(value)s"
 msgstr ""
 
-#: ../roundup/admin.py:728
+#: ../roundup/admin.py:735
 msgid ""
 "Usage: display designator[,designator]*\n"
 "        Show the property values for the given node(s).\n"
@@ -607,12 +607,12 @@
 "        ÚÁÄÁÎÎÙÈ ÏÐÉÓÁÔÅÌÑÍÉ.\n"
 "        "
 
-#: ../roundup/admin.py:752
+#: ../roundup/admin.py:759
 #, python-format
 msgid "%(key)s: %(value)r"
 msgstr ""
 
-#: ../roundup/admin.py:755
+#: ../roundup/admin.py:762
 msgid ""
 "Usage: create classname property=value ...\n"
 "        Create a new entry of a given class.\n"
@@ -629,31 +629,31 @@
 "        ÜÔÏÇÏ ÏÂßÅËÔÁ ÕËÁÚÁÎÎÙÍÉ ÚÎÁÞÅÎÉÑÍÉ.\n"
 "        "
 
-#: ../roundup/admin.py:782
+#: ../roundup/admin.py:789
 #, python-format
 msgid "%(propname)s (Password): "
 msgstr " %(propname)s (ÐÁÒÏÌØ): "
 
-#: ../roundup/admin.py:784
+#: ../roundup/admin.py:791
 #, python-format
 msgid "   %(propname)s (Again): "
 msgstr "%(propname)s (ÅÝÅ ÒÁÚ): "
 
-#: ../roundup/admin.py:786
+#: ../roundup/admin.py:793
 msgid "Sorry, try again..."
 msgstr "ðÁÒÏÌÉ ÎÅ ÓÏ×ÐÁÌÉ.  ðÏÐÒÏÂÕÊÔÅ ÅÝÅ ÒÁÚ."
 
-#: ../roundup/admin.py:790
+#: ../roundup/admin.py:797
 #, python-format
 msgid "%(propname)s (%(proptype)s): "
 msgstr ""
 
-#: ../roundup/admin.py:808
+#: ../roundup/admin.py:815
 #, python-format
 msgid "you must provide the \"%(propname)s\" property."
 msgstr "áÔÒÉÂÕÔ \"%(propname)s\" ÄÏÌÖÅÎ ÂÙÔØ ÚÁÐÏÌÎÅÎ."
 
-#: ../roundup/admin.py:819
+#: ../roundup/admin.py:827
 msgid ""
 "Usage: list classname [property]\n"
 "        List the instances of a class.\n"
@@ -682,16 +682,16 @@
 "        ×ÙÄÁÅÔ ÓÐÉÓÏË ÚÎÁÞÅÎÉÊ ÜÔÏÇÏ ÁÔÒÉÂÕÔÁ.\n"
 "        "
 
-#: ../roundup/admin.py:832
+#: ../roundup/admin.py:840
 msgid "Too many arguments supplied"
 msgstr "ðÏÄÁÎÏ ÓÌÉÛËÏÍ ÍÎÏÇÏ ÐÁÒÁÍÅÔÒÏ×"
 
-#: ../roundup/admin.py:868
+#: ../roundup/admin.py:876
 #, python-format
 msgid "%(nodeid)4s: %(value)s"
 msgstr ""
 
-#: ../roundup/admin.py:872
+#: ../roundup/admin.py:880
 msgid ""
 "Usage: table classname [property[,property]*]\n"
 "        List the instances of a class in tabular form.\n"
@@ -751,12 +751,12 @@
 "        ÏÂÒÅÚÁÅÔ ÚÎÁÞÅÎÉÑ ÓÔÏÌÂÃÁ \"Name\" ÄÏ ÞÅÔÙÒÅÈ ÓÉÍ×ÏÌÏ×.\n"
 "        "
 
-#: ../roundup/admin.py:916
+#: ../roundup/admin.py:924
 #, python-format
 msgid "\"%(spec)s\" not name:width"
 msgstr "úÎÁÞÅÎÉÅ \"%(spec)s\" ÄÏÌÖÎÏ ÂÙÔØ ÚÁÄÁÎÏ ËÁË ÉÍÑ:ÛÉÒÉÎÁ"
 
-#: ../roundup/admin.py:966
+#: ../roundup/admin.py:974
 msgid ""
 "Usage: history designator\n"
 "        Show the history entries of a designator.\n"
@@ -771,7 +771,7 @@
 "        ÚÁÄÁÎÎÏÇÏ ÏÐÉÓÁÔÅÌÅÍ.\n"
 "        "
 
-#: ../roundup/admin.py:987
+#: ../roundup/admin.py:995
 msgid ""
 "Usage: commit\n"
 "        Commit changes made to the database during an interactive session.\n"
@@ -795,7 +795,7 @@
 "        Á×ÔÏÍÁÔÉÞÅÓËÉ, ÅÓÌÉ ÐÒÉ ×ÙÐÏÌÎÅÎÉÉ ËÏÍÁÎÄÙ ÎÅ ÐÒÏÉÚÏÛÌÏ ÏÛÉÂËÉ.\n"
 "        "
 
-#: ../roundup/admin.py:1001
+#: ../roundup/admin.py:1010
 msgid ""
 "Usage: rollback\n"
 "        Undo all changes that are pending commit to the database.\n"
@@ -816,7 +816,7 @@
 "        ÂÙÌÏ × ÍÏÍÅÎÔ ÐÏÓÌÅÄÎÅÊ ÚÁÐÉÓÉ.\n"
 "        "
 
-#: ../roundup/admin.py:1013
+#: ../roundup/admin.py:1023
 msgid ""
 "Usage: retire designator[,designator]*\n"
 "        Retire the node specified by designator.\n"
@@ -834,7 +834,7 @@
 "        ÉÓÐÏÌØÚÏ×ÁÎÙ × ÄÒÕÇÉÈ ÏÂßÅËÔÁÈ.\n"
 "        "
 
-#: ../roundup/admin.py:1036
+#: ../roundup/admin.py:1047
 msgid ""
 "Usage: restore designator[,designator]*\n"
 "        Restore the retired node specified by designator.\n"
@@ -850,7 +850,7 @@
 "        "
 
 #. grab the directory to export to
-#: ../roundup/admin.py:1058
+#: ../roundup/admin.py:1070
 msgid ""
 "Usage: export [[-]class[,class]] export_dir\n"
 "        Export the database to colon-separated-value files.\n"
@@ -885,7 +885,7 @@
 "        exporttables.\n"
 "        "
 
-#: ../roundup/admin.py:1136
+#: ../roundup/admin.py:1145
 msgid ""
 "Usage: exporttables [[-]class[,class]] export_dir\n"
 "        Export the database to colon-separated-value files, excluding the\n"
@@ -920,7 +920,7 @@
 "        ÐÏÌÎÏÓÔØÀ, ÉÓÐÏÌØÚÕÊÔÅ ËÏÍÁÎÄÕ export.\n"
 "        "
 
-#: ../roundup/admin.py:1151
+#: ../roundup/admin.py:1160
 msgid ""
 "Usage: import import_dir\n"
 "        Import a database from the directory containing CSV files,\n"
@@ -964,7 +964,7 @@
 "        ÉÚ ÓÕÝÅÓÔ×ÕÀÝÅÊ ÂÁÚÙ ×ÓÅ ÏÂßÅËÔÙ).\n"
 "        "
 
-#: ../roundup/admin.py:1225
+#: ../roundup/admin.py:1235
 msgid ""
 "Usage: pack period | date\n"
 "\n"
@@ -1003,11 +1003,11 @@
 "\n"
 "        "
 
-#: ../roundup/admin.py:1253
+#: ../roundup/admin.py:1263
 msgid "Invalid format"
 msgstr "îÅÐÒÁ×ÉÌØÎÙÊ ÆÏÒÍÁÔ"
 
-#: ../roundup/admin.py:1263
+#: ../roundup/admin.py:1274
 msgid ""
 "Usage: reindex [classname|designator]*\n"
 "        Re-generate a tracker's search indexes.\n"
@@ -1023,12 +1023,12 @@
 "        ÄÁÎÎÙÈ.  ïÂÙÞÎÏ ÐÏÓÔÒÏÅÎÉÅ ÉÎÄÅËÓÏ× ÐÒÏÉÓÈÏÄÉÔ Á×ÔÏÍÁÔÉÞÅÓËÉ.\n"
 "        "
 
-#: ../roundup/admin.py:1277
+#: ../roundup/admin.py:1288
 #, python-format
 msgid "no such item \"%(designator)s\""
 msgstr "ÏÂßÅËÔ \"%(designator)s\" ÎÅ ÓÕÝÅÓÔ×ÕÅÔ"
 
-#: ../roundup/admin.py:1287
+#: ../roundup/admin.py:1298
 msgid ""
 "Usage: security [Role name]\n"
 "        Display the Permissions available to one or all Roles.\n"
@@ -1039,78 +1039,78 @@
 "        ÒÏÌÑÍ.\n"
 "        "
 
-#: ../roundup/admin.py:1295
+#: ../roundup/admin.py:1306
 #, python-format
 msgid "No such Role \"%(role)s\""
 msgstr "òÏÌØ \"%(role)s\" ÎÅ ÓÕÝÅÓÔ×ÕÅÔ"
 
-#: ../roundup/admin.py:1301
+#: ../roundup/admin.py:1312
 #, python-format
 msgid "New Web users get the Roles \"%(role)s\""
 msgstr "îÏ×ÙÅ ÐÏÌØÚÏ×ÁÔÅÌÉ web ÐÏÌÕÞÁÀÔ ÒÏÌÉ \"%(role)s\""
 
-#: ../roundup/admin.py:1303
+#: ../roundup/admin.py:1314
 #, python-format
 msgid "New Web users get the Role \"%(role)s\""
 msgstr "îÏ×ÙÅ ÐÏÌØÚÏ×ÁÔÅÌÉ web ÐÏÌÕÞÁÀÔ ÒÏÌØ \"%(role)s\""
 
-#: ../roundup/admin.py:1306
+#: ../roundup/admin.py:1317
 #, python-format
 msgid "New Email users get the Roles \"%(role)s\""
 msgstr "îÏ×ÙÅ ÐÏÌØÚÏ×ÁÔÅÌÉ email ÐÏÌÕÞÁÀÔ ÒÏÌÉ \"%(role)s\""
 
-#: ../roundup/admin.py:1308
+#: ../roundup/admin.py:1319
 #, python-format
 msgid "New Email users get the Role \"%(role)s\""
 msgstr "îÏ×ÙÅ ÐÏÌØÚÏ×ÁÔÅÌÉ email ÐÏÌÕÞÁÀÔ ÒÏÌØ \"%(role)s\""
 
-#: ../roundup/admin.py:1311
+#: ../roundup/admin.py:1322
 #, python-format
 msgid "Role \"%(name)s\":"
 msgstr "òÏÌØ \"%(name)s\":"
 
-#: ../roundup/admin.py:1316
+#: ../roundup/admin.py:1327
 #, python-format
 msgid " %(description)s (%(name)s for \"%(klass)s\": %(properties)s only)"
 msgstr " %(description)s (%(name)s ÄÌÑ ËÌÁÓÓÁ \"%(klass)s\": ÔÏÌØËÏ Ó×ÏÊÓÔ×Á %(properties)s)"
 
-#: ../roundup/admin.py:1319
+#: ../roundup/admin.py:1330
 #, python-format
 msgid " %(description)s (%(name)s for \"%(klass)s\" only)"
 msgstr " %(description)s (%(name)s ÔÏÌØËÏ ÄÌÑ ËÌÁÓÓÁ \"%(klass)s\")"
 
-#: ../roundup/admin.py:1322
+#: ../roundup/admin.py:1333
 #, python-format
 msgid " %(description)s (%(name)s)"
 msgstr ""
 
-#: ../roundup/admin.py:1351
+#: ../roundup/admin.py:1362
 #, python-format
 msgid "Unknown command \"%(command)s\" (\"help commands\" for a list)"
 msgstr "ëÏÍÁÎÄÁ \"%(command)s\" ÎÅÉÚ×ÅÓÔÎÁ. (\"help commands\" ×ÙÄÁÅÔ ÓÐÉÓÏË ËÏÍÁÎÄ)"
 
-#: ../roundup/admin.py:1357
+#: ../roundup/admin.py:1368
 #, python-format
 msgid "Multiple commands match \"%(command)s\": %(list)s"
 msgstr "\"%(command)s\" ÓÏÏÔ×ÅÔÓÔ×ÕÅÔ ÎÅÓËÏÌØËÉÍ ËÏÍÁÎÄÁÍ: %(list)s"
 
-#: ../roundup/admin.py:1364
+#: ../roundup/admin.py:1375
 msgid "Enter tracker home: "
 msgstr "äÏÍÁÛÎÉÊ ËÁÔÁÌÏÇ ÔÒÅËÅÒÁ: "
 
-#: ../roundup/admin.py:1371
-#: ../roundup/admin.py:1377
-#: ../roundup/admin.py:1397
+#: ../roundup/admin.py:1382
+#: ../roundup/admin.py:1388
+#: ../roundup/admin.py:1408
 #, python-format
 msgid "Error: %(message)s"
 msgstr "ïÛÉÂËÁ: %(message)s"
 
-#: ../roundup/admin.py:1385
+#: ../roundup/admin.py:1396
 #, python-format
 msgid "Error: Couldn't open tracker: %(message)s"
 msgstr "ïÛÉÂËÁ: ôÒÅËÅÒ ÎÅ ÏÔËÒÙ×ÁÅÔÓÑ: %(message)s"
 
-#: ../roundup/admin.py:1410
+#: ../roundup/admin.py:1421
 #, python-format
 msgid ""
 "Roundup %s ready for input.\n"
@@ -1119,48 +1119,48 @@
 "Roundup %s Ë ×ÁÛÉÍ ÕÓÌÕÇÁÍ.\n"
 "÷×ÅÄÉÔÅ \"help\" ÄÌÑ ÓÐÒÁ×ËÉ."
 
-#: ../roundup/admin.py:1415
+#: ../roundup/admin.py:1426
 msgid "Note: command history and editing not available"
 msgstr "ðÒÉÍÅÞÁÎÉÅ: ÒÁÂÏÔÁÅÔ ÒÅÄÁËÔÏÒ É ÉÓÔÏÒÉÑ ËÏÍÁÎÄ"
 
-#: ../roundup/admin.py:1419
+#: ../roundup/admin.py:1430
 msgid "roundup> "
 msgstr ""
 
-#: ../roundup/admin.py:1421
+#: ../roundup/admin.py:1432
 msgid "exit..."
 msgstr "ÐÒÉÈÏÄÉÔÅ Ë ÎÁÍ ÅÝÅ..."
 
-#: ../roundup/admin.py:1431
+#: ../roundup/admin.py:1442
 msgid "There are unsaved changes. Commit them (y/N)? "
 msgstr "ïÊ, ÔÕÔ ÎÅÓÏÈÒÁÎÅÎÎÙÅ ÉÚÍÅÎÅÎÉÑ. úÁÐÉÓÁÔØ × ÂÁÚÕ ÄÁÎÎÙÈ (y/N)? "
 
-#: ../roundup/backends/back_anydbm.py:2000
+#: ../roundup/backends/back_anydbm.py:2004
 #, python-format
 msgid "WARNING: invalid date tuple %r"
 msgstr "÷îéíáîéå! îÅ×ÅÒÎÁÑ ÄÁÔÁ: %r"
 
-#: ../roundup/backends/rdbms_common.py:1442
+#: ../roundup/backends/rdbms_common.py:1445
 msgid "create"
 msgstr "ÓÏÚÄÁÎÉÅ"
 
-#: ../roundup/backends/rdbms_common.py:1608
+#: ../roundup/backends/rdbms_common.py:1611
 msgid "unlink"
 msgstr "ÏÔ×ÑÚËÁ"
 
-#: ../roundup/backends/rdbms_common.py:1612
+#: ../roundup/backends/rdbms_common.py:1615
 msgid "link"
 msgstr "ÐÒÉ×ÑÚËÁ"
 
-#: ../roundup/backends/rdbms_common.py:1732
+#: ../roundup/backends/rdbms_common.py:1737
 msgid "set"
 msgstr "ÕÓÔÁÎÏ×ËÁ"
 
-#: ../roundup/backends/rdbms_common.py:1756
+#: ../roundup/backends/rdbms_common.py:1761
 msgid "retired"
 msgstr "ÚÁÐÒÅÝÅÎÉÅ"
 
-#: ../roundup/backends/rdbms_common.py:1786
+#: ../roundup/backends/rdbms_common.py:1791
 msgid "restored"
 msgstr "×ÏÓÓÔÁÎÏ×ÌÅÎÉÅ"
 
@@ -1191,73 +1191,73 @@
 msgid "%(classname)s %(itemid)s has been retired"
 msgstr "%(classname)s %(itemid)s ÕÄÁÌÅÎ"
 
-#: ../roundup/cgi/actions.py:174
-#: ../roundup/cgi/actions.py:202
+#: ../roundup/cgi/actions.py:169
+#: ../roundup/cgi/actions.py:197
 msgid "You do not have permission to edit queries"
 msgstr "õ ×ÁÓ ÎÅÔ ÒÁÚÒÅÛÅÎÉÑ ÎÁ ÒÅÄÁËÔÉÒÏ×ÁÎÉÅ ÚÁÐÒÏÓÏ×"
 
-#: ../roundup/cgi/actions.py:180
-#: ../roundup/cgi/actions.py:209
+#: ../roundup/cgi/actions.py:175
+#: ../roundup/cgi/actions.py:204
 msgid "You do not have permission to store queries"
 msgstr "õ ×ÁÓ ÎÅÔ ÒÁÚÒÅÛÅÎÉÑ ÎÁ ÓÏÈÒÁÎÅÎÉÅ ÚÁÐÒÏÓÏ×"
 
-#: ../roundup/cgi/actions.py:298
+#: ../roundup/cgi/actions.py:310
 #, python-format
 msgid "Not enough values on line %(line)s"
 msgstr "÷ ÓÔÒÏËÅ %(line)s ÎÅ È×ÁÔÁÅÔ ÚÎÁÞÅÎÉÊ"
 
-#: ../roundup/cgi/actions.py:345
+#: ../roundup/cgi/actions.py:357
 msgid "Items edited OK"
 msgstr "ïÂßÅËÔÙ ÉÚÍÅÎÅÎÙ ÕÓÐÅÛÎÏ"
 
-#: ../roundup/cgi/actions.py:405
+#: ../roundup/cgi/actions.py:416
 #, python-format
 msgid "%(class)s %(id)s %(properties)s edited ok"
 msgstr "éÚÍÅÎÅÎÙ ÁÔÒÉÂÕÔÙ %(properties)s ÏÂßÅËÔÁ %(class)s %(id)s"
 
-#: ../roundup/cgi/actions.py:408
+#: ../roundup/cgi/actions.py:419
 #, python-format
 msgid "%(class)s %(id)s - nothing changed"
 msgstr "%(class)s %(id)s - ÎÅÔ ÉÚÍÅÎÅÎÉÊ"
 
-#: ../roundup/cgi/actions.py:420
+#: ../roundup/cgi/actions.py:431
 #, python-format
 msgid "%(class)s %(id)s created"
 msgstr "%(class)s %(id)s ÓÏÚÄÁÎ"
 
-#: ../roundup/cgi/actions.py:452
+#: ../roundup/cgi/actions.py:463
 #, python-format
 msgid "You do not have permission to edit %(class)s"
 msgstr "õ ×ÁÓ ÎÅÔ ÒÁÚÒÅÛÅÎÉÑ ÒÅÄÁËÔÉÒÏ×ÁÔØ %(class)s"
 
-#: ../roundup/cgi/actions.py:464
+#: ../roundup/cgi/actions.py:475
 #, python-format
 msgid "You do not have permission to create %(class)s"
 msgstr "õ ×ÁÓ ÎÅÔ ÒÁÚÒÅÛÅÎÉÑ ÓÏÚÄÁ×ÁÔØ %(class)s"
 
-#: ../roundup/cgi/actions.py:488
+#: ../roundup/cgi/actions.py:499
 msgid "You do not have permission to edit user roles"
 msgstr "õ ×ÁÓ ÎÅÔ ÒÁÚÒÅÛÅÎÉÑ ÎÁ ÉÚÍÅÎÅÎÉÅ ÒÏÌÅÊ ÐÏÌØÚÏ×ÁÔÅÌÅÊ"
 
-#: ../roundup/cgi/actions.py:538
+#: ../roundup/cgi/actions.py:549
 #, python-format
 msgid "Edit Error: someone else has edited this %s (%s). View <a target=\"new\" href=\"%s%s\">their changes</a> in a new window."
 msgstr "ïÛÉÂËÁ ÒÅÄÁËÔÉÒÏ×ÁÎÉÑ: %s (%s) ÉÚÍÅÎÉÌ ÄÒÕÇÏÊ ÐÏÌØÚÏ×ÁÔÅÌØ. <a target=\"new\" href=\"%s%s\">ðÒÏÓÍÏÔÒÅÔØ ÜÔÉ ÉÚÍÅÎÅÎÉÑ</a> × ÄÒÕÇÏÍ ÏËÎÅ."
 
-#: ../roundup/cgi/actions.py:566
+#: ../roundup/cgi/actions.py:577
 #, python-format
 msgid "Edit Error: %s"
 msgstr "ïÛÉÂËÁ ÒÅÄÁËÔÉÒÏ×ÁÎÉÑ: %s"
 
-#: ../roundup/cgi/actions.py:597
 #: ../roundup/cgi/actions.py:608
-#: ../roundup/cgi/actions.py:779
-#: ../roundup/cgi/actions.py:798
+#: ../roundup/cgi/actions.py:619
+#: ../roundup/cgi/actions.py:790
+#: ../roundup/cgi/actions.py:809
 #, python-format
 msgid "Error: %s"
 msgstr "ïÛÉÂËÁ: %s"
 
-#: ../roundup/cgi/actions.py:634
+#: ../roundup/cgi/actions.py:645
 msgid ""
 "Invalid One Time Key!\n"
 "(a Mozilla bug may cause this message to show up erroneously, please check your email)"
@@ -1265,50 +1265,50 @@
 "ëÌÀÞ ÐÏÄÔ×ÅÒÖÄÅÎÉÑ ÎÅÐÒÁ×ÉÌÅÎ!\n"
 "(éÚ-ÚÁ ÏÛÉÂËÉ × ÂÒÁÕÚÅÒÅ Mozilla ÜÔÏ ÓÏÏÂÝÅÎÉÅ ÍÏÖÅÔ ÂÙÔØ ÎÅ×ÅÒÎÙÍ. ðÒÏ×ÅÒØÔÅ ×ÁÛÕ ÐÏÞÔÕ, ÐÏÖÁÌÕÊÓÔÁ.)"
 
-#: ../roundup/cgi/actions.py:676
+#: ../roundup/cgi/actions.py:687
 #, python-format
 msgid "Password reset and email sent to %s"
 msgstr "ðÁÒÏÌØ ÓÂÒÏÛÅÎ.  ðÏ ÁÄÒÅÓÕ %s ÏÔÐÒÁ×ÌÅÎÏ ÐÉÓØÍÏ."
 
-#: ../roundup/cgi/actions.py:685
+#: ../roundup/cgi/actions.py:696
 msgid "Unknown username"
 msgstr "îÅÉÚ×ÅÓÔÎÏÅ ÉÍÑ ÐÏÌØÚÏ×ÁÔÅÌÑ"
 
-#: ../roundup/cgi/actions.py:693
+#: ../roundup/cgi/actions.py:704
 msgid "Unknown email address"
 msgstr "îÅÉÚ×ÅÓÔÎÙÊ ÁÄÒÅÓ email"
 
-#: ../roundup/cgi/actions.py:698
+#: ../roundup/cgi/actions.py:709
 msgid "You need to specify a username or address"
 msgstr "÷Ù ÄÏÌÖÎÙ ÕËÁÚÁÔØ ÉÍÑ ÐÏÌØÚÏ×ÁÔÅÌÑ ÉÌÉ ÁÄÒÅÓ email"
 
-#: ../roundup/cgi/actions.py:723
+#: ../roundup/cgi/actions.py:734
 #, python-format
 msgid "Email sent to %s"
 msgstr "ðÉÓØÍÏ ÏÔÐÒÁ×ÌÅÎÏ ÎÁ %s"
 
-#: ../roundup/cgi/actions.py:742
+#: ../roundup/cgi/actions.py:753
 msgid "You are now registered, welcome!"
 msgstr "÷Ù ÚÁÒÅÇÉÓÔÒÉÒÏ×ÁÎÙ.  äÏÂÒÏ ÐÏÖÁÌÏ×ÁÔØ!"
 
-#: ../roundup/cgi/actions.py:787
+#: ../roundup/cgi/actions.py:798
 msgid "It is not permitted to supply roles at registration."
 msgstr "îÅÌØÚÑ ÕËÁÚÙ×ÁÔØ ÒÏÌÉ ÐÒÉ ÒÅÇÉÓÔÒÁÃÉÉ"
 
-#: ../roundup/cgi/actions.py:879
+#: ../roundup/cgi/actions.py:890
 msgid "You are logged out"
 msgstr "óÅÁÎÓ ÒÁÂÏÔÙ ÚÁ×ÅÒÛÅÎ"
 
-#: ../roundup/cgi/actions.py:896
+#: ../roundup/cgi/actions.py:907
 msgid "Username required"
 msgstr "îÅ ÕËÁÚÁÎÏ ÉÍÑ ÐÏÌØÚÏ×ÁÔÅÌÑ"
 
-#: ../roundup/cgi/actions.py:931
-#: ../roundup/cgi/actions.py:935
+#: ../roundup/cgi/actions.py:942
+#: ../roundup/cgi/actions.py:946
 msgid "Invalid login"
 msgstr "îÅÐÒÁ×ÉÌØÎÙÊ ÐÁÒÏÌØ ÉÌÉ ÉÍÑ ÐÏÌØÚÏ×ÁÔÅÌÑ."
 
-#: ../roundup/cgi/actions.py:941
+#: ../roundup/cgi/actions.py:952
 msgid "You do not have permission to login"
 msgstr "õ ×ÁÓ ÎÅÔ ÒÁÚÒÅÛÅÎÉÑ ÎÁ ÒÁÂÏÔÕ Ó ÓÉÓÔÅÍÏÊ"
 
@@ -1403,29 +1403,29 @@
 "áÄÍÉÎÉÓÔÒÁÔÏÒÕ ÔÒÅËÅÒÁ ÏÔÏÓÌÁÎÏ ÓÏÏÂÝÅÎÉÅ Ï ÏÛÉÂËÅ.</p>\n"
 "</body></html>"
 
-#: ../roundup/cgi/client.py:326
+#: ../roundup/cgi/client.py:339
 msgid "Form Error: "
 msgstr "ïÛÉÂËÁ ÆÏÒÍÙ: "
 
-#: ../roundup/cgi/client.py:381
+#: ../roundup/cgi/client.py:394
 #, python-format
 msgid "Unrecognized charset: %r"
 msgstr "ëÏÄÉÒÏ×ËÁ %r ÎÅ ÒÁÓÐÏÚÎÁÎÁ"
 
-#: ../roundup/cgi/client.py:509
+#: ../roundup/cgi/client.py:522
 msgid "Anonymous users are not allowed to use the web interface"
 msgstr "áÎÏÎÉÍÎÙÍ ÐÏÌØÚÏ×ÁÔÅÌÑÍ ÎÅ ÒÁÚÒÅÛÅÎÏ ÐÏÌØÚÏ×ÁÔØÓÑ ×ÅÂ-ÉÎÔÅÒÆÅÊÓÏÍ."
 
-#: ../roundup/cgi/client.py:664
+#: ../roundup/cgi/client.py:677
 msgid "You are not allowed to view this file."
 msgstr "õ ×ÁÓ ÎÅÔ ÒÁÚÒÅÛÅÎÉÑ ÎÁ ÐÒÏÓÍÏÔÒ ÜÔÏÇÏ ÆÁÊÌÁ."
 
-#: ../roundup/cgi/client.py:758
+#: ../roundup/cgi/client.py:770
 #, python-format
 msgid "%(starttag)sTime elapsed: %(seconds)fs%(endtag)s\n"
 msgstr "%(starttag)súÁÔÒÁÞÅÎÎÏÅ ×ÒÅÍÑ: %(seconds)fs%(endtag)s\n"
 
-#: ../roundup/cgi/client.py:762
+#: ../roundup/cgi/client.py:774
 #, python-format
 msgid "%(starttag)sCache hits: %(cache_hits)d, misses %(cache_misses)d. Loading items: %(get_items)f secs. Filtering: %(filtering)f secs.%(endtag)s\n"
 msgstr "%(starttag)sëÅÛÉÒÏ×ÁÎÎÙÅ ÜÌÅÍÅÎÔÙ: %(cache_hits)d, ×ÙÞÉÓÌÅÎÎÙÅ: %(cache_misses)d. úÁÇÒÕÚËÁ ÏÂßÅËÔÏ×: %(get_items)f ÓÅË. æÉÌØÔÒÁÃÉÑ: %(filtering)f ÓÅË.%(endtag)s\n"
@@ -1479,158 +1479,159 @@
 msgid "File is empty"
 msgstr "æÁÊÌ ÐÕÓÔ"
 
-#: ../roundup/cgi/templating.py:73
+#: ../roundup/cgi/templating.py:77
 #, python-format
 msgid "You are not allowed to %(action)s items of class %(class)s"
 msgstr "õ ×ÁÓ ÎÅÔ ÒÁÚÒÅÛÅÎÉÑ %(action)s ÄÌÑ ËÌÁÓÓÁ %(class)s"
 
-#: ../roundup/cgi/templating.py:645
+#: ../roundup/cgi/templating.py:657
 msgid "(list)"
 msgstr "(ÓÐÉÓÏË)"
 
-#: ../roundup/cgi/templating.py:714
+#: ../roundup/cgi/templating.py:726
 msgid "Submit New Entry"
 msgstr "äÏÂÁ×ÉÔØ"
 
 # ../roundup/cgi/templating.py:673 :792 :1166 :1187 :1231 :1253 :1287 :1326
 # :1377 :1394 :1470 :1490 :1503 :1520 :1530 :1580 :1755
-#: ../roundup/cgi/templating.py:728
-#: ../roundup/cgi/templating.py:862
-#: ../roundup/cgi/templating.py:1269
-#: ../roundup/cgi/templating.py:1298
-#: ../roundup/cgi/templating.py:1318
-#: ../roundup/cgi/templating.py:1364
-#: ../roundup/cgi/templating.py:1387
-#: ../roundup/cgi/templating.py:1423
-#: ../roundup/cgi/templating.py:1460
-#: ../roundup/cgi/templating.py:1513
-#: ../roundup/cgi/templating.py:1530
-#: ../roundup/cgi/templating.py:1614
-#: ../roundup/cgi/templating.py:1634
-#: ../roundup/cgi/templating.py:1652
-#: ../roundup/cgi/templating.py:1684
-#: ../roundup/cgi/templating.py:1694
-#: ../roundup/cgi/templating.py:1746
-#: ../roundup/cgi/templating.py:1935
+#: ../roundup/cgi/templating.py:740
+#: ../roundup/cgi/templating.py:873
+#: ../roundup/cgi/templating.py:1294
+#: ../roundup/cgi/templating.py:1323
+#: ../roundup/cgi/templating.py:1343
+#: ../roundup/cgi/templating.py:1356
+#: ../roundup/cgi/templating.py:1407
+#: ../roundup/cgi/templating.py:1430
+#: ../roundup/cgi/templating.py:1466
+#: ../roundup/cgi/templating.py:1503
+#: ../roundup/cgi/templating.py:1556
+#: ../roundup/cgi/templating.py:1573
+#: ../roundup/cgi/templating.py:1657
+#: ../roundup/cgi/templating.py:1677
+#: ../roundup/cgi/templating.py:1695
+#: ../roundup/cgi/templating.py:1727
+#: ../roundup/cgi/templating.py:1737
+#: ../roundup/cgi/templating.py:1789
+#: ../roundup/cgi/templating.py:1978
 msgid "[hidden]"
 msgstr "[ÎÅÄÏÓÔÕÐÎÏ]"
 
-#: ../roundup/cgi/templating.py:729
+#: ../roundup/cgi/templating.py:741
 msgid "New node - no history"
 msgstr "îÏ×ÁÑ ËÁÒÔÏÞËÁ - ÎÅÔ ÉÓÔÏÒÉÉ"
 
-#: ../roundup/cgi/templating.py:844
+#: ../roundup/cgi/templating.py:855
 msgid "Submit Changes"
 msgstr "éÚÍÅÎÉÔØ"
 
-#: ../roundup/cgi/templating.py:926
+#: ../roundup/cgi/templating.py:937
 msgid "<em>The indicated property no longer exists</em>"
 msgstr "<em>õËÁÚÁÎÎÙÊ ÁÔÒÉÂÕÔ ÕÖÅ ÎÅ ÓÕÝÅÓÔ×ÕÅÔ.</em>"
 
-#: ../roundup/cgi/templating.py:927
+#: ../roundup/cgi/templating.py:938
 #, python-format
 msgid "<em>%s: %s</em>\n"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:940
+#: ../roundup/cgi/templating.py:951
 #, python-format
 msgid "The linked class %(classname)s no longer exists"
 msgstr "ó×ÑÚÑÎÎÙÊ ËÌÁÓÓ %(classname)s ÕÖÅ ÎÅ ÓÕÝÅÓÔ×ÕÅÔ"
 
 # :823
-#: ../roundup/cgi/templating.py:973
-#: ../roundup/cgi/templating.py:997
+#: ../roundup/cgi/templating.py:984
+#: ../roundup/cgi/templating.py:1008
 msgid "<strike>The linked node no longer exists</strike>"
 msgstr "<strike>ó×ÑÚÁÎÎÙÊ ÏÂßÅËÔ ÕÖÅ ÎÅ ÓÕÝÅÓÔ×ÕÅÔ</strike>"
 
-#: ../roundup/cgi/templating.py:1050
+#: ../roundup/cgi/templating.py:1061
 #, python-format
 msgid "%s: (no value)"
 msgstr "%s: (ÎÅÔ ÚÎÁÞÅÎÉÑ)"
 
-#: ../roundup/cgi/templating.py:1062
+#: ../roundup/cgi/templating.py:1073
 msgid "<strong><em>This event is not handled by the history display!</em></strong>"
 msgstr "<strong><em>îÅÉÚ×ÅÓÔÎÙÊ ÔÉÐ ÓÏÂÙÔÉÑ!</em></strong>"
 
-#: ../roundup/cgi/templating.py:1074
+#: ../roundup/cgi/templating.py:1085
 msgid "<tr><td colspan=4><strong>Note:</strong></td></tr>"
 msgstr "<tr><td colspan=4><strong>ðÒÉÍÅÞÁÎÉÅ:</strong></td></tr>"
 
-#: ../roundup/cgi/templating.py:1083
+#: ../roundup/cgi/templating.py:1094
 msgid "History"
 msgstr "éÓÔÏÒÉÑ"
 
-#: ../roundup/cgi/templating.py:1085
+#: ../roundup/cgi/templating.py:1096
 msgid "<th>Date</th>"
 msgstr "<th>äÁÔÁ</th>"
 
-#: ../roundup/cgi/templating.py:1086
+#: ../roundup/cgi/templating.py:1097
 msgid "<th>User</th>"
 msgstr "<th>ðÏÌØÚÏ×ÁÔÅÌØ</th>"
 
-#: ../roundup/cgi/templating.py:1087
+#: ../roundup/cgi/templating.py:1098
 msgid "<th>Action</th>"
 msgstr "<th>äÅÊÓÔ×ÉÅ</th>"
 
-#: ../roundup/cgi/templating.py:1088
+#: ../roundup/cgi/templating.py:1099
 msgid "<th>Args</th>"
 msgstr "<th>ðÁÒÁÍÅÔÒÙ</th>"
 
-#: ../roundup/cgi/templating.py:1130
+#: ../roundup/cgi/templating.py:1141
 #, python-format
 msgid "Copy of %(class)s %(id)s"
 msgstr "ëÏÐÉÑ: %(class)s %(id)s"
 
-#: ../roundup/cgi/templating.py:1391
+#: ../roundup/cgi/templating.py:1434
 msgid "*encrypted*"
 msgstr "*ÚÁÛÉÆÒÏ×ÁÎ*"
 
-#: ../roundup/cgi/templating.py:1464
-#: ../roundup/cgi/templating.py:1485
-#: ../roundup/cgi/templating.py:1491
-#: ../roundup/cgi/templating.py:1039
+#: ../roundup/cgi/templating.py:1507
+#: ../roundup/cgi/templating.py:1528
+#: ../roundup/cgi/templating.py:1534
+#: ../roundup/cgi/templating.py:1050
 msgid "No"
 msgstr "îÅÔ"
 
-#: ../roundup/cgi/templating.py:1464
-#: ../roundup/cgi/templating.py:1483
-#: ../roundup/cgi/templating.py:1488
-#: ../roundup/cgi/templating.py:1039
+#: ../roundup/cgi/templating.py:1507
+#: ../roundup/cgi/templating.py:1526
+#: ../roundup/cgi/templating.py:1531
+#: ../roundup/cgi/templating.py:1050
 msgid "Yes"
 msgstr "äÁ"
 
-#: ../roundup/cgi/templating.py:1577
+#: ../roundup/cgi/templating.py:1620
 msgid "default value for DateHTMLProperty must be either DateHTMLProperty or string date representation."
 msgstr "ÚÎÁÞÅÎÉÅ ÐÏ ÕÍÏÌÞÁÎÉÀ ÄÌÑ DateHTMLProperty ÄÏÌÖÎÏ ÂÙÔØ ÏÂßÅËÔÏÍ DateHTMLProperty ÉÌÉ ÓÔÒÏËÏ×ÙÍ ÐÒÅÄÓÔÁ×ÌÅÎÉÅÍ ÄÁÔÙ."
 
-#: ../roundup/cgi/templating.py:1737
+#: ../roundup/cgi/templating.py:1780
 #, python-format
 msgid "Attempt to look up %(attr)s on a missing value"
 msgstr "ðÏÐÙÔËÁ ÐÏÌÕÞÉÔØ ÁÔÒÉÂÕÔ \"%(attr)s\" ÎÅÓÕÝÅÓÔ×ÕÀÝÅÇÏ ÏÂßÅËÔÁ"
 
-#: ../roundup/cgi/templating.py:1810
+#: ../roundup/cgi/templating.py:1853
 #, python-format
 msgid "<option %svalue=\"-1\">- no selection -</option>"
 msgstr "<option %svalue=\"-1\">- ÎÅ ÕËÁÚÁÎÏ -</option>"
 
-#: ../roundup/date.py:301
+#: ../roundup/date.py:300
 msgid "Not a date spec: \"yyyy-mm-dd\", \"mm-dd\", \"HH:MM\", \"HH:MM:SS\" or \"yyyy-mm-dd.HH:MM:SS.SSS\""
 msgstr "äÁÔÁ ÄÏÌÖÎÁ ÂÙÔØ × ÆÏÒÍÁÔÅ \"yyyy-mm-dd\", \"mm-dd\", \"HH:MM\", \"HH:MM:SS\" ÉÌÉ \"yyyy-mm-dd.HH:MM:SS.SSS\""
 
-#: ../roundup/date.py:363
+#: ../roundup/date.py:359
 #, python-format
 msgid "%r not a date / time spec \"yyyy-mm-dd\", \"mm-dd\", \"HH:MM\", \"HH:MM:SS\" or \"yyyy-mm-dd.HH:MM:SS.SSS\""
 msgstr "îÅ×ÅÒÎÏÅ ÚÎÁÞÅÎÉÅ ÄÁÔÙ/×ÒÅÍÅÎÉ: %r.  äÁÔÁ ÄÏÌÖÎÁ ÂÙÔØ × ÆÏÒÍÁÔÅ \"yyyy-mm-dd\", \"mm-dd\", \"HH:MM\", \"HH:MM:SS\" ÉÌÉ \"yyyy-mm-dd.HH:MM:SS.SSS\""
 
-#: ../roundup/date.py:662
+#: ../roundup/date.py:666
 msgid "Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [date spec]"
 msgstr "éÎÔÅÒ×ÁÌ ÄÏÌÖÅÎ ÂÙÔØ × ÆÏÒÍÁÔÅ [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [ÄÁÔÁ]"
 
-#: ../roundup/date.py:681
+#: ../roundup/date.py:685
 msgid "Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]"
 msgstr "éÎÔÅÒ×ÁÌ ÄÏÌÖÅÎ ÂÙÔØ × ÆÏÒÍÁÔÅ [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]"
 
-#: ../roundup/date.py:818
+#: ../roundup/date.py:822
 #, python-format
 msgid "%(number)s year"
 msgid_plural "%(number)s years"
@@ -1638,7 +1639,7 @@
 msgstr[1] "%(number)s ÇÏÄÁ"
 msgstr[2] "%(number)s ÌÅÔ"
 
-#: ../roundup/date.py:822
+#: ../roundup/date.py:826
 #, python-format
 msgid "%(number)s month"
 msgid_plural "%(number)s months"
@@ -1646,7 +1647,7 @@
 msgstr[1] "%(number)s ÍÅÓÑÃÁ"
 msgstr[2] "%(number)s ÍÅÓÑÃÅ×"
 
-#: ../roundup/date.py:826
+#: ../roundup/date.py:830
 #, python-format
 msgid "%(number)s week"
 msgid_plural "%(number)s weeks"
@@ -1654,7 +1655,7 @@
 msgstr[1] "%(number)s ÎÅÄÅÌÉ"
 msgstr[2] "%(number)s ÎÅÄÅÌØ"
 
-#: ../roundup/date.py:830
+#: ../roundup/date.py:834
 #, python-format
 msgid "%(number)s day"
 msgid_plural "%(number)s days"
@@ -1662,15 +1663,15 @@
 msgstr[1] "%(number)s ÄÎÑ"
 msgstr[2] "%(number)s ÄÎÅÊ"
 
-#: ../roundup/date.py:834
+#: ../roundup/date.py:838
 msgid "tomorrow"
 msgstr "ÚÁ×ÔÒÁ"
 
-#: ../roundup/date.py:836
+#: ../roundup/date.py:840
 msgid "yesterday"
 msgstr "×ÞÅÒÁ"
 
-#: ../roundup/date.py:839
+#: ../roundup/date.py:843
 #, python-format
 msgid "%(number)s hour"
 msgid_plural "%(number)s hours"
@@ -1678,16 +1679,16 @@
 msgstr[1] "%(number)s ÞÁÓÁ"
 msgstr[2] "%(number)s ÞÁÓÏ×"
 
-#: ../roundup/date.py:843
+#: ../roundup/date.py:847
 msgid "an hour"
 msgstr "ÞÁÓ"
 
-#: ../roundup/date.py:845
+#: ../roundup/date.py:849
 msgid "1 1/2 hours"
 msgstr "ÐÏÌÔÏÒÁ ÞÁÓÁ"
 
 # third form ain't used
-#: ../roundup/date.py:847
+#: ../roundup/date.py:851
 #, python-format
 msgid "1 %(number)s/4 hours"
 msgid_plural "1 %(number)s/4 hours"
@@ -1695,21 +1696,21 @@
 msgstr[1] "ÞÁÓ É %(number)s ÞÅÔ×ÅÒÔÉ"
 msgstr[2] "ÞÁÓ É %(number)s ÞÅÔ×ÅÒÔÅÊ"
 
-#: ../roundup/date.py:851
+#: ../roundup/date.py:855
 msgid "in a moment"
 msgstr "ÓÅÊÞÁÓ"
 
-#: ../roundup/date.py:853
+#: ../roundup/date.py:857
 msgid "just now"
 msgstr "ÔÏÌØËÏ ÞÔÏ"
 
 # ÉÓÐÏÌØÚÕÅÔÓÑ × ×ÙÒÁÖÅÎÉÑÈ "ÞÅÒÅÚ ÍÉÎÕÔÕ" ÉÌÉ "ÍÉÎÕÔÕ ÎÁÚÁÄ"
-#: ../roundup/date.py:856
+#: ../roundup/date.py:860
 msgid "1 minute"
 msgstr "ÍÉÎÕÔÕ"
 
 # ÉÓÐÏÌØÚÕÅÔÓÑ × ×ÙÒÁÖÅÎÉÑÈ "ÞÅÒÅÚ 2 ÍÉÎÕÔÙ" ÉÌÉ "2 ÍÉÎÕÔÙ ÎÁÚÁÄ"
-#: ../roundup/date.py:859
+#: ../roundup/date.py:863
 #, python-format
 msgid "%(number)s minute"
 msgid_plural "%(number)s minutes"
@@ -1717,11 +1718,11 @@
 msgstr[1] "%(number)s ÍÉÎÕÔÙ"
 msgstr[2] "%(number)s ÍÉÎÕÔ"
 
-#: ../roundup/date.py:862
+#: ../roundup/date.py:866
 msgid "1/2 an hour"
 msgstr "ÐÏÌÞÁÓÁ"
 
-#: ../roundup/date.py:864
+#: ../roundup/date.py:868
 #, python-format
 msgid "%(number)s/4 hour"
 msgid_plural "%(number)s/4 hours"
@@ -1729,12 +1730,12 @@
 msgstr[1] "%(number)s ÞÅÔ×ÅÒÔÉ ÞÁÓÁ"
 msgstr[2] "%(number)s ÞÅÔ×ÅÒÔÅÊ ÞÁÓÁ"
 
-#: ../roundup/date.py:868
+#: ../roundup/date.py:872
 #, python-format
 msgid "%s ago"
 msgstr "%s ÎÁÚÁÄ"
 
-#: ../roundup/date.py:870
+#: ../roundup/date.py:874
 #, python-format
 msgid "in %s"
 msgstr "ÞÅÒÅÚ %s"
@@ -1748,7 +1749,7 @@
 "÷îéíáîéå! ëÁÔÁÌÏÇ '%s'\n"
 "\tÓÏÄÅÒÖÉÔ ÛÁÂÌÏÎ ÓÔÁÒÏÇÏ ÏÂÒÁÚÃÁ - ÐÒÏÐÕÝÅÎ"
 
-#: ../roundup/mailgw.py:583
+#: ../roundup/mailgw.py:584
 msgid ""
 "\n"
 "Emails to Roundup trackers must include a Subject: line!\n"
@@ -1756,7 +1757,7 @@
 "\n"
 "÷ ÐÉÓØÍÁÈ ÄÌÑ ÔÒÅËÅÒÁ Roundup ÄÏÌÖÎÁ ÂÙÔØ ÕËÁÚÁÎÁ ÔÅÍÁ ÓÏÏÂÝÅÎÉÑ (Subject).\n"
 
-#: ../roundup/mailgw.py:673
+#: ../roundup/mailgw.py:708
 #, python-format
 msgid ""
 "\n"
@@ -1785,12 +1786,12 @@
 "        1234, ËÏÔÏÒÁÑ ÕÖÅ ÓÕÝÅÓÔ×ÕÅÔ × ÔÒÅËÅÒÅ.\n"
 "ôÅÍÁ ×ÁÛÅÇÏ ÐÉÓØÍÁ: \"%(subject)s\"\n"
 
-#: ../roundup/mailgw.py:704
+#: ../roundup/mailgw.py:746
 #, python-format
 msgid ""
 "\n"
-"The class name you identified in the subject line (\"%(classname)s\") does not exist in the\n"
-"database.\n"
+"The class name you identified in the subject line (\"%(classname)s\") does\n"
+"not exist in the database.\n"
 "\n"
 "Valid class names are: %(validname)s\n"
 "Subject was: \"%(subject)s\"\n"
@@ -1802,12 +1803,42 @@
 "éÍÅÎÁ ÓÕÝÅÓÔ×ÕÀÝÉÈ ËÌÁÓÓÏ×: %(validname)s\n"
 "ôÅÍÁ ×ÁÛÅÇÏ ÐÉÓØÍÁ: \"%(subject)s\"\n"
 
-#: ../roundup/mailgw.py:739
+#: ../roundup/mailgw.py:754
+#, python-format
+msgid ""
+"\n"
+"You did not identify a class name in the subject line and there is no\n"
+"default set for this tracker. The subject must contain a class name or\n"
+"designator to indicate the 'topic' of the message. For example:\n"
+"    Subject: [issue] This is a new issue\n"
+"      - this will create a new issue in the tracker with the title 'This is\n"
+"        a new issue'.\n"
+"    Subject: [issue1234] This is a followup to issue 1234\n"
+"      - this will append the message's contents to the existing issue 1234\n"
+"        in the tracker.\n"
+"\n"
+"Subject was: '%(subject)s'\n"
+msgstr ""
+"\n"
+"÷Ù ÎÅ ÕËÁÚÁÌÉ × ÔÅÍÅ ÐÉÓØÍÁ ÉÍÅÎÉ ËÌÁÓÓÁ, É ÚÎÁÞÅÎÉÅ ÐÏ ÕÍÏÌÞÁÎÉÀ\n"
+"ÎÅ ÕÓÔÁÎÏ×ÌÅÎÏ ÄÌÑ ÜÔÏÇÏ ÔÒÅËÅÒÁ.  ÷ ÐÏÌÅ \"Subject:\" × Ë×ÁÄÒÁÔÎÙÈ\n"
+"ÓËÏÂËÁÈ ÄÏÌÖÎÅÎ ÂÙÔØ ÕËÁÚÁÎ ËÌÁÓÓ ÉÌÉ ÏÐÉÓÁÔÅÌØ ÏÂßÅËÔÁ, Ë ËÏÔÏÒÏÍÕ\n"
+"ÏÔÎÏÓÉÔÓÑ ÜÔÏ ÓÏÏÂÝÅÎÉÅ.  îÁÐÒÉÍÅÒ:\n"
+"    Subject: [issue] üÔÏ ÎÏ×ÁÑ ÚÁÄÁÞÁ\n"
+"      - ÔÁËÏÅ ÐÉÓØÍÏ ÓÏÚÄÁÓÔ × ÔÒÅËÅÒÅ ÎÏ×ÕÀ ÚÁÄÁÞÕ (ÏÂßÅËÔ ËÌÁÓÓÁ issue)\n"
+"        Ó ÚÁÇÏÌÏ×ËÏÍ \"üÔÏ ÎÏ×ÁÑ ÚÁÄÁÞÁ\".\n"
+"    Subject: [issue1234] üÔÏ ÚÁÍÅÞÁÎÉÅ Ë ÚÁÄÁÞÅ 1234\n"
+"      - ÓÏÄÅÒÖÉÍÏÅ ÜÔÏÇÏ ÐÉÓØÍÁ ÂÕÄÅÔ ÄÏÂÁ×ÌÅÎÏ Ë ÓÐÉÓËÕ ÓÏÏÂÝÅÎÉÊ ÚÁÄÁÞÉ\n"
+"        1234, ËÏÔÏÒÁÑ ÕÖÅ ÓÕÝÅÓÔ×ÕÅÔ × ÔÒÅËÅÒÅ.\n"
+"\n"
+"ôÅÍÁ ×ÁÛÅÇÏ ÐÉÓØÍÁ: \"%(subject)s\"\n"
+
+#: ../roundup/mailgw.py:795
 #, python-format
 msgid ""
 "\n"
 "I cannot match your message to a node in the database - you need to either\n"
-"supply a full designator (with number, eg \"[issue123]\" or keep the\n"
+"supply a full designator (with number, eg \"[issue123]\") or keep the\n"
 "previous subject title intact so I can match that.\n"
 "\n"
 "Subject was: \"%(subject)s\"\n"
@@ -1821,7 +1852,7 @@
 "\n"
 "ôÅÍÁ ×ÁÛÅÇÏ ÐÉÓØÍÁ: \"%(subject)s\"\n"
 
-#: ../roundup/mailgw.py:772
+#: ../roundup/mailgw.py:828
 #, python-format
 msgid ""
 "\n"
@@ -1835,7 +1866,7 @@
 "\n"
 "ôÅÍÁ ×ÁÛÅÇÏ ÐÉÓØÍÁ: \"%(subject)s\"\n"
 
-#: ../roundup/mailgw.py:800
+#: ../roundup/mailgw.py:856
 #, python-format
 msgid ""
 "\n"
@@ -1849,7 +1880,7 @@
 "Ï ÎÅÐÒÁ×ÉÌØÎÏ ÏÐÉÓÁÎÎÏÍ ËÌÁÓÓÅ:\n"
 "  %(current_class)s\n"
 
-#: ../roundup/mailgw.py:823
+#: ../roundup/mailgw.py:879
 #, python-format
 msgid ""
 "\n"
@@ -1863,34 +1894,34 @@
 "Ï ÎÅÐÒÁ×ÉÌØÎÏ ÏÐÉÓÁÎÎÙÈ ÁÔÒÉÂÕÔÁÈ:\n"
 "  %(errors)s\n"
 
-#: ../roundup/mailgw.py:853
+#: ../roundup/mailgw.py:919
 #, python-format
 msgid ""
 "\n"
-"You are not a registered user.\n"
+"You are not a registered user.%(registration_info)s\n"
 "\n"
 "Unknown address: %(from_address)s\n"
 msgstr ""
 "\n"
-"äÏÓÔÕÐ ÒÁÚÒÅÛÅÎ ÔÏÌØËÏ ÚÁÒÅÇÉÓÔÒÉÒÏ×ÁÎÎÙÍ ÐÏÌØÚÏ×ÁÔÅÌÑÍ.\n"
+"äÏÓÔÕÐ ÒÁÚÒÅÛÅÎ ÔÏÌØËÏ ÚÁÒÅÇÉÓÔÒÉÒÏ×ÁÎÎÙÍ ÐÏÌØÚÏ×ÁÔÅÌÑÍ.%(registration_info)s\n"
 "\n"
 "îÅÉÚ×ÅÓÔÎÙÊ ÁÄÒÅÓ: %(from_address)s\n"
 
-#: ../roundup/mailgw.py:861
+#: ../roundup/mailgw.py:927
 msgid "You are not permitted to access this tracker."
 msgstr "õ ×ÁÓ ÎÅÔ ÒÁÚÒÅÛÅÎÉÑ ÎÁ ÄÏÓÔÕÐ Ë ÜÔÏÍÕ ÔÒÅËÅÒÕ."
 
-#: ../roundup/mailgw.py:868
+#: ../roundup/mailgw.py:934
 #, python-format
 msgid "You are not permitted to edit %(classname)s."
 msgstr "õ ×ÁÓ ÎÅÔ ÒÁÚÒÅÛÅÎÉÑ ÒÅÄÁËÔÉÒÏ×ÁÔØ %(classname)s"
 
-#: ../roundup/mailgw.py:872
+#: ../roundup/mailgw.py:938
 #, python-format
 msgid "You are not permitted to create %(classname)s."
 msgstr "õ ×ÁÓ ÎÅÔ ÒÁÚÒÅÛÅÎÉÑ ÓÏÚÄÁ×ÁÔØ ÏÂßÅËÔÙ %(classname)s"
 
-#: ../roundup/mailgw.py:919
+#: ../roundup/mailgw.py:985
 #, python-format
 msgid ""
 "\n"
@@ -1905,7 +1936,7 @@
 "\n"
 "ôÅÍÁ ÐÉÓØÍÁ: \"%(subject)s\"\n"
 
-#: ../roundup/mailgw.py:947
+#: ../roundup/mailgw.py:1013
 msgid ""
 "\n"
 "Roundup requires the submission to be plain text. The message parser could\n"
@@ -1915,20 +1946,20 @@
 "óÏÏÂÝÅÎÉÑ ÄÌÑ Roundup ÄÏÌÖÎÙ ÂÙÔØ × ÔÅËÓÔÏ×ÏÍ ÆÏÒÍÁÔÅ.\n"
 "÷ ×ÁÛÅÍ ÓÏÏÂÝÅÎÉÉ ÎÅ ÎÁÊÄÅÎÁ ÞÁÓÔØ ÆÏÒÍÁÔÁ text/plain.\n"
 
-#: ../roundup/mailgw.py:969
+#: ../roundup/mailgw.py:1030
 msgid "You are not permitted to create files."
 msgstr "õ ×ÁÓ ÎÅÔ ÒÁÚÒÅÛÅÎÉÑ ÎÁ ÓÏÚÄÁÎÉÅ ÆÁÊÌÏ×."
 
-#: ../roundup/mailgw.py:983
+#: ../roundup/mailgw.py:1044
 #, python-format
 msgid "You are not permitted to add files to %(classname)s."
 msgstr "õ ×ÁÓ ÎÅÔ ÒÁÚÒÅÛÅÎÉÑ ÄÏÂÁ×ÌÑÔØ ÆÁÊÌÙ ÄÌÑ ËÌÁÓÓÁ %(classname)s."
 
-#: ../roundup/mailgw.py:1001
+#: ../roundup/mailgw.py:1062
 msgid "You are not permitted to create messages."
 msgstr "õ ×ÁÓ ÎÅÔ ÒÁÚÒÅÛÅÎÉÑ ÎÁ ÓÏÚÄÁÎÉÅ ÓÏÏÂÝÅÎÉÊ"
 
-#: ../roundup/mailgw.py:1009
+#: ../roundup/mailgw.py:1070
 #, python-format
 msgid ""
 "\n"
@@ -1939,17 +1970,17 @@
 "óÏÏÂÝÅÎÉÅ ÏÔÂÒÏÛÅÎÏ ÄÅÔÅËÔÏÒÏÍ.\n"
 "%(error)s\n"
 
-#: ../roundup/mailgw.py:1017
+#: ../roundup/mailgw.py:1078
 #, python-format
 msgid "You are not permitted to add messages to %(classname)s."
 msgstr "õ ×ÁÓ ÎÅÔ ÒÁÚÒÅÛÅÎÉÑ ÄÏÂÁ×ÌÑÔØ ÓÏÏÂÝÅÎÉÑ ÄÌÑ ËÌÁÓÓÁ %(classname)s."
 
-#: ../roundup/mailgw.py:1044
+#: ../roundup/mailgw.py:1105
 #, python-format
 msgid "You are not permitted to edit property %(prop)s of class %(classname)s."
 msgstr "õ ×ÁÓ ÎÅÔ ÒÁÚÒÅÛÅÎÉÑ ÉÚÍÅÎÑÔØ ÁÔÒÉÂÕÔ %(prop)s ËÌÁÓÓÁ %(classname)s"
 
-#: ../roundup/mailgw.py:1052
+#: ../roundup/mailgw.py:1113
 #, python-format
 msgid ""
 "\n"
@@ -1960,79 +1991,85 @@
 "ðÒÉ ÏÂÒÁÂÏÔËÅ ×ÁÛÅÇÏ ÓÏÏÂÝÅÎÉÑ ÐÒÏÉÚÏÛÌÁ ÏÛÉÂËÁ:\n"
 "   %(message)s\n"
 
-#: ../roundup/mailgw.py:1074
+#: ../roundup/mailgw.py:1135
 msgid "not of form [arg=value,value,...;arg=value,value,...]"
 msgstr "ÁÒÇÕÍÅÎÔÙ ÄÏÌÖÎÙ ÂÙÔØ × ÆÏÒÍÁÔÅ [ÉÍÑ=ÚÎÁÞÅÎÉÅ,ÚÎÁÞÅÎÉÅ,...;ÉÍÑ=ÚÎÁÞÅÎÉÅ,ÚÎÁÞÅÎÉÅ,...]"
 
-#: ../roundup/roundupdb.py:146
+#: ../roundup/roundupdb.py:147
 msgid "files"
 msgstr "ÆÁÊÌÙ"
 
-#: ../roundup/roundupdb.py:146
+#: ../roundup/roundupdb.py:147
 msgid "messages"
 msgstr "ÓÏÏÂÝÅÎÉÑ"
 
-#: ../roundup/roundupdb.py:146
+#: ../roundup/roundupdb.py:147
 msgid "nosy"
 msgstr "ÉÚ×ÅÝÅÎÉÑ"
 
-#: ../roundup/roundupdb.py:146
+#: ../roundup/roundupdb.py:147
 msgid "superseder"
 msgstr "ÚÁÍÅÝÅÎÉÅ"
 
-#: ../roundup/roundupdb.py:146
+#: ../roundup/roundupdb.py:147
 msgid "title"
 msgstr "ÚÁÇÌÁ×ÉÅ"
 
-#: ../roundup/roundupdb.py:147
+#: ../roundup/roundupdb.py:148
 msgid "assignedto"
 msgstr "ÉÓÐÏÌÎÉÔÅÌØ"
 
-#: ../roundup/roundupdb.py:147
+#: ../roundup/roundupdb.py:148
+msgid "keyword"
+msgstr "ËÌÀÞÅ×ÏÅ ÓÌÏ×Ï"
+
+#: ../roundup/roundupdb.py:148
 msgid "priority"
 msgstr "ÐÒÉÏÒÉÔÅÔ"
 
-#: ../roundup/roundupdb.py:147
+#: ../roundup/roundupdb.py:148
 msgid "status"
 msgstr "ÓÔÁÔÕÓ"
 
-#: ../roundup/roundupdb.py:147
-msgid "topic"
-msgstr "ÔÅÍÁ"
-
-#: ../roundup/roundupdb.py:150
+#: ../roundup/roundupdb.py:151
 msgid "activity"
 msgstr "ÄÅÊÓÔ×ÉÅ"
 
 #. following properties are common for all hyperdb classes
 #. they are listed here to keep things in one place
-#: ../roundup/roundupdb.py:150
+#: ../roundup/roundupdb.py:151
 msgid "actor"
 msgstr "×ÙÐÏÌÎÉÌ"
 
-#: ../roundup/roundupdb.py:150
+#: ../roundup/roundupdb.py:151
 msgid "creation"
 msgstr "ÄÁÔÁ ÓÏÚÄÁÎÉÑ"
 
-#: ../roundup/roundupdb.py:150
+#: ../roundup/roundupdb.py:151
 msgid "creator"
 msgstr "Á×ÔÏÒ"
 
-#: ../roundup/roundupdb.py:308
+#: ../roundup/roundupdb.py:309
 #, python-format
 msgid "New submission from %(authname)s%(authaddr)s:"
 msgstr "îÏ×ÏÅ ÐÏÓÔÕÐÌÅÎÉÅ ÏÔ %(authname)s%(authaddr)s:"
 
-#: ../roundup/roundupdb.py:311
+#: ../roundup/roundupdb.py:312
 #, python-format
 msgid "%(authname)s%(authaddr)s added the comment:"
 msgstr "%(authname)s%(authaddr)s ÄÏÂÁ×ÉÌ ÚÁÍÅÞÁÎÉÅ:"
 
-#: ../roundup/roundupdb.py:314
-msgid "System message:"
-msgstr "óÏÏÂÝÅÎÉÅ ÓÉÓÔÅÍÙ:"
+#: ../roundup/roundupdb.py:315
+#, python-format
+msgid "Change by %(authname)s%(authaddr)s:"
+msgstr "éÚÍÅÎÅÎÉÅ %(authname)s%(authaddr)s:"
+
+#: ../roundup/roundupdb.py:342
+#, python-format
+msgid "File '%(filename)s' not attached - you can download it from %(link)s."
+msgstr "æÁÊÌ '%(filename)s' ÎÅ ×ÌÏÖÅÎ - ×Ù ÍÏÖÅÔÅ ÓËÁÞÁÔØ ÅÇÏ ÐÏ ÁÄÒÅÓÕ %(link)s."
 
-#: ../roundup/roundupdb.py:597
+#: ../roundup/roundupdb.py:615
 #, python-format
 msgid ""
 "\n"
@@ -2225,7 +2262,11 @@
 msgid "Error: The source must be either \"mailbox\", \"pop\", \"apop\", \"imap\" or \"imaps\""
 msgstr "ïÛÉÂËÁ: ôÉÐ ÐÏÞÔÏ×ÏÇÏ ÑÝÉËÁ ÄÏÌÖÅÎ ÂÙÔØ \"mailbox\", \"pop\", \"apop\", \"imap\" ÉÌÉ \"imaps\""
 
-#: ../roundup/scripts/roundup_server.py:157
+#: ../roundup/scripts/roundup_server.py:76
+msgid "WARNING: generating temporary SSL certificate"
+msgstr "÷îéíáîéå: ÓÏÚÄÁÅÔÓÑ ×ÒÅÍÅÎÎÙÊ ÓÅÒÔÉÆÉËÁÔ ÄÌÑ SSL"
+
+#: ../roundup/scripts/roundup_server.py:253
 msgid ""
 "<html><head><title>Roundup trackers index</title></head>\n"
 "<body><h1>Roundup trackers index</h1><ol>\n"
@@ -2233,52 +2274,52 @@
 "<html><head><title>óÐÉÓÏË ÔÒÅËÅÒÏ× Roundup</title></head>\n"
 "<body><h1>óÐÉÓÏË ÔÒÅËÅÒÏ× Roundup</h1><ol>\n"
 
-#: ../roundup/scripts/roundup_server.py:293
+#: ../roundup/scripts/roundup_server.py:389
 #, python-format
 msgid "Error: %s: %s"
 msgstr "ïÛÉÂËÁ: %s: %s"
 
-#: ../roundup/scripts/roundup_server.py:303
+#: ../roundup/scripts/roundup_server.py:399
 msgid "WARNING: ignoring \"-g\" argument, not root"
 msgstr "÷îéíáîéå: ÐÁÒÁÍÅÔÒ \"-g\" ÎÅ ÉÓÐÏÌØÚÕÅÔÓÑ, ÏÎ ÒÁÚÒÅÛÅÎ ÔÏÌØËÏ ÄÌÑ ÐÏÌØÚÏ×ÁÔÅÌÑ root"
 
-#: ../roundup/scripts/roundup_server.py:309
+#: ../roundup/scripts/roundup_server.py:405
 msgid "Can't change groups - no grp module"
 msgstr "ðÏÄÍÅÎÁ ÇÒÕÐÐÙ ÎÅ×ÏÚÍÏÖÎÁ - ÎÕÖÅÎ ÍÏÄÕÌØ grp"
 
-#: ../roundup/scripts/roundup_server.py:318
+#: ../roundup/scripts/roundup_server.py:414
 #, python-format
 msgid "Group %(group)s doesn't exist"
 msgstr "çÒÕÐÐÁ %(group)s ÎÅ ÓÕÝÅÓÔ×ÕÅÔ"
 
-#: ../roundup/scripts/roundup_server.py:329
+#: ../roundup/scripts/roundup_server.py:425
 msgid "Can't run as root!"
 msgstr "úÁÐÕÓË ÓÅÒ×ÅÒÁ Ó ÐÏÌÎÏÍÏÞÉÑÍÉ ÐÏÌØÚÏ×ÁÔÅÌÑ root ÚÁÐÒÅÝÅÎ!"
 
-#: ../roundup/scripts/roundup_server.py:332
+#: ../roundup/scripts/roundup_server.py:428
 msgid "WARNING: ignoring \"-u\" argument, not root"
 msgstr "÷îéíáîéå: ÐÁÒÁÍÅÔÒ \"-u\" ÎÅ ÉÓÐÏÌØÚÕÅÔÓÑ, ÏÎ ÒÁÚÒÅÛÅÎ ÔÏÌØËÏ ÄÌÑ ÐÏÌØÚÏ×ÁÔÅÌÑ root"
 
-#: ../roundup/scripts/roundup_server.py:338
+#: ../roundup/scripts/roundup_server.py:434
 msgid "Can't change users - no pwd module"
 msgstr "ðÏÄÍÅÎÁ ÐÏÌØÚÏ×ÁÔÅÌÑ ÎÅ×ÏÚÍÏÖÎÁ - ÎÕÖÅÎ ÍÏÄÕÌØ pwd"
 
-#: ../roundup/scripts/roundup_server.py:347
+#: ../roundup/scripts/roundup_server.py:443
 #, python-format
 msgid "User %(user)s doesn't exist"
 msgstr "ðÏÌØÚÏ×ÁÔÅÌØ %(user)s ÎÅ ÓÕÝÅÓÔ×ÕÅÔ"
 
-#: ../roundup/scripts/roundup_server.py:481
+#: ../roundup/scripts/roundup_server.py:592
 #, python-format
 msgid "Multiprocess mode \"%s\" is not available, switching to single-process"
 msgstr "òÅÖÉÍ \"%s\" ÎÅÄÏÓÔÕÐÅÎ, ÐÅÒÅËÌÀÞÁÅÍÓÑ × ÏÄÎÏÚÁÄÁÞÎÙÊ ÒÅÖÉÍ"
 
-#: ../roundup/scripts/roundup_server.py:504
+#: ../roundup/scripts/roundup_server.py:620
 #, python-format
 msgid "Unable to bind to port %s, port already in use."
 msgstr "îÅ×ÏÚÍÏÖÎÏ ÕÓÔÁÎÏ×ÉÔØ ÓÅÒ×ÅÒ ÎÁ ÐÏÒÔÕ %s, ÐÏÒÔ ÕÖÅ ÚÁÎÑÔ."
 
-#: ../roundup/scripts/roundup_server.py:572
+#: ../roundup/scripts/roundup_server.py:688
 msgid ""
 " -c <Command>  Windows Service options.\n"
 "               If you want to run the server as a Windows Service, you\n"
@@ -2295,7 +2336,7 @@
 "               ÆÁÊÌ ÐÒÏÔÏËÏÌÁ.  ëÏÍÁÎÄÁ 'roundup-server -c help'\n"
 "               ×ÙÄÁÅÔ ÓÐÒÁ×ËÕ Ï ËÏÍÁÎÄÎÏÊ ÓÔÒÏËÅ ÓÅÒ×ÉÓÁ Windows."
 
-#: ../roundup/scripts/roundup_server.py:579
+#: ../roundup/scripts/roundup_server.py:695
 msgid ""
 " -u <UID>      runs the Roundup web server as this UID\n"
 " -g <GID>      runs the Roundup web server as this GID\n"
@@ -2309,7 +2350,7 @@
 "               É ÚÁÐÕÓÔÉÔØ ÓÅÒ×ÅÒ × ÆÏÎÏ×ÏÍ ÒÅÖÉÍÅ.  åÓÌÉ ÕËÁÚÁÎÏ \"-d\",\n"
 "               ÆÁÊÌ ÐÒÏÔÏËÏÌÁ *ÏÂÑÚÁÔÅÌØÎÏ* ÄÏÌÖÅÎ ÂÙÔØ ÚÁÄÁÎ ËÌÀÞÏÍ \"-l\""
 
-#: ../roundup/scripts/roundup_server.py:586
+#: ../roundup/scripts/roundup_server.py:702
 #, python-format
 msgid ""
 "%(message)sUsage: roundup-server [options] [name=tracker home]*\n"
@@ -2323,6 +2364,9 @@
 " -p <port>     set the port to listen on (default: %(port)s)\n"
 " -l <fname>    log to the file indicated by fname instead of stderr/stdout\n"
 " -N            log client machine names instead of IP addresses (much slower)\n"
+" -i <fname>    set tracker index template\n"
+" -s            enable SSL\n"
+" -e <fname>    PEM file containing SSL key and certificate\n"
 " -t <mode>     multiprocess mode (default: %(mp_def)s).\n"
 "               Allowed values: %(mp_types)s.\n"
 "%(os_part)s\n"
@@ -2374,6 +2418,9 @@
 " -l <ÆÁÊÌ>     ×ÅÓÔÉ ÐÒÏÔÏËÏÌ × ÕËÁÚÁÎÎÏÍ ÆÁÊÌÅ (×ÍÅÓÔÏ stderr/stdout)\n"
 " -N            ÐÒÏÔÏËÏÌÉÒÏ×ÁÔØ ÉÍÅÎÁ ÍÁÛÉÎ ËÌÉÅÎÔÏ× ×ÍÅÓÔÏ IP-ÁÄÒÅÓÏ×\n"
 "               (ÓÉÌØÎÏ ÚÁÍÅÄÌÑÅÔ ÒÁÂÏÔÕ).\n"
+" -i <fname>    ÕËÁÚÁÔØ ÛÁÂÌÏÎ ÄÌÑ ÓÐÉÓËÁ ÔÒÅËÅÒÏ×\n"
+" -s            ×ËÌÀÞÉÔØ SSL\n"
+" -e <fname>    PEM-ÆÁÊÌ, ÓÏÄÅÒÖÁÝÉÊ ËÌÀÞ É ÓÅÒÔÉÆÉËÁÔ ÄÌÑ SSL\n"
 " -t <ÒÅÖÉÍ>    ÒÅÖÉÍ ÍÎÏÇÏÚÁÄÁÞÎÏÓÔÉ (ÐÏ ÕÍÏÌÞÁÎÉÀ - %(mp_def)s).\n"
 "               äÏÓÔÕÐÎÙÅ ÒÅÖÉÍÙ: %(mp_types)s.\n"
 "%(os_part)s\n"
@@ -2416,20 +2463,20 @@
 "   ÎÅ ÍÏÇÕÔ ÉÓÐÏÌØÚÏ×ÁÔØÓÑ × URL (ÐÒÏÂÅÌÙ, ÒÕÓÓËÉÅ ÂÕË×Ù É ÐÒÏÞ.),\n"
 "   ÐÏÔÏÍÕ ÞÔÏ ÔÁËÉÅ ÉÍÅÎÁ ÓÂÉ×ÁÀÔ Ó ÔÏÌËÕ ÎÅËÏÔÏÒÙÅ ÂÒÁÕÚÅÒÙ ÔÉÐÁ IE.\n"
 
-#: ../roundup/scripts/roundup_server.py:741
+#: ../roundup/scripts/roundup_server.py:860
 msgid "Instances must be name=home"
 msgstr "óÐÉÓÏË ÔÒÅËÅÒÏ× ÄÏÌÖÅÎ ÂÙÔØ × ÆÏÒÍÁÔÅ ÉÍÑ=ËÁÔÁÌÏÇ"
 
-#: ../roundup/scripts/roundup_server.py:755
+#: ../roundup/scripts/roundup_server.py:874
 #, python-format
 msgid "Configuration saved to %s"
 msgstr "ëÏÎÆÉÇÕÒÁÃÉÑ ÚÁÐÉÓÁÎÁ × %s"
 
-#: ../roundup/scripts/roundup_server.py:773
+#: ../roundup/scripts/roundup_server.py:892
 msgid "Sorry, you can't run the server as a daemon on this Operating System"
 msgstr "éÚ×ÉÎÉÔÅ, × ÜÔÏÊ ÏÐÅÒÁÃÉÏÎÎÏÊ ÓÉÓÔÅÍÅ ÒÁÂÏÔÁ × ÆÏÎÏ×ÏÍ ÒÅÖÉÍÅ ÎÅ×ÏÚÍÏÖÎÁ"
 
-#: ../roundup/scripts/roundup_server.py:788
+#: ../roundup/scripts/roundup_server.py:907
 #, python-format
 msgid "Roundup server started on %(HOST)s:%(PORT)s"
 msgstr "óÅÒ×ÅÒ Roundup ÇÏÔÏ× Ë ÒÁÂÏÔÅ ÐÏ ÁÄÒÅÓÕ %(HOST)s:%(PORT)s"
@@ -2551,6 +2598,7 @@
 #: ../templates/minimal/html/_generic.index.html:19
 #: ../templates/minimal/html/_generic.item.html:17
 #: ../templates/minimal/html/user.index.html:13
+#: ../templates/minimal/html/user.item.html:39
 #: ../templates/minimal/html/user.register.html:17
 msgid "Please login with your username and password."
 msgstr "õËÁÖÉÔÅ ÉÍÑ ÐÏÌØÚÏ×ÁÔÅÌÑ É ÐÁÒÏÌØ ÄÌÑ ×ÈÏÄÁ × ÓÉÓÔÅÍÕ."
@@ -2645,8 +2693,9 @@
 msgstr "÷ÙÐÏÌÎÉÌ"
 
 #: ../templates/classic/html/issue.index.html:32
-msgid "Topic"
-msgstr "ôÅÍÁ"
+#: ../templates/classic/html/keyword.item.html:37
+msgid "Keyword"
+msgstr "ëÌÀÞÅ×ÏÅ ÓÌÏ×Ï"
 
 #: ../templates/classic/html/issue.index.html:33
 #: ../templates/classic/html/issue.item.html:44
@@ -2733,8 +2782,10 @@
 msgstr "éÓÐÏÌÎÉÔÅÌØ"
 
 #: ../templates/classic/html/issue.item.html:78
-msgid "Topics"
-msgstr "ôÅÍÙ"
+#: ../templates/classic/html/page.html:103
+#: ../templates/minimal/html/page.html:102
+msgid "Keywords"
+msgstr "ëÌÀÞÅ×ÙÅ&nbsp;ÓÌÏ×Á"
 
 #: ../templates/classic/html/issue.item.html:86
 msgid "Change Note"
@@ -2749,9 +2800,9 @@
 msgstr "óËÏÐÉÒÏ×ÁÔØ"
 
 #: ../templates/classic/html/issue.item.html:114
-#: ../templates/classic/html/user.item.html:152
+#: ../templates/classic/html/user.item.html:153
 #: ../templates/classic/html/user.register.html:69
-#: ../templates/minimal/html/user.item.html:147
+#: ../templates/minimal/html/user.item.html:153
 msgid "<table class=\"form\"> <tr> <td>Note:&nbsp;</td> <th class=\"required\">highlighted</th> <td>&nbsp;fields are required.</td> </tr> </table>"
 msgstr "<table class=\"form\"> <tr> <td>ðÒÉÍÅÞÁÎÉÅ:&nbsp;</td><th class=\"required\">×ÙÄÅÌÅÎÎÙÅ</th><td>&nbsp;ÐÏÌÑ ÄÏÌÖÎÙ ÂÙÔØ ÚÁÐÏÌÎÅÎÙ.</td> </tr> </table>"
 
@@ -2843,91 +2894,92 @@
 msgstr "× ÚÁÇÏÌÏ×ËÅ:"
 
 #: ../templates/classic/html/issue.search.html:56
-msgid "Topic:"
-msgstr "ôÅÍÁ:"
+msgid "Keyword:"
+msgstr "ëÌÀÞÅ×ÏÅ ÓÌÏ×Ï:"
+
+#: ../templates/classic/html/issue.search.html:58
+#: ../templates/classic/html/issue.search.html:123
+#: ../templates/classic/html/issue.search.html:139
+msgid "not selected"
+msgstr "ÎÅ ÕÓÔÁÎÏ×ÌÅÎ"
 
-#: ../templates/classic/html/issue.search.html:64
+#: ../templates/classic/html/issue.search.html:67
 msgid "ID:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:72
+#: ../templates/classic/html/issue.search.html:75
 msgid "Creation Date:"
 msgstr "äÁÔÁ ÓÏÚÄÁÎÉÑ:"
 
-#: ../templates/classic/html/issue.search.html:83
+#: ../templates/classic/html/issue.search.html:86
 msgid "Creator:"
 msgstr "á×ÔÏÒ:"
 
-#: ../templates/classic/html/issue.search.html:85
+#: ../templates/classic/html/issue.search.html:88
 msgid "created by me"
 msgstr "ÓÏÚÄÁÎÏ ÍÎÏÊ"
 
-#: ../templates/classic/html/issue.search.html:94
+#: ../templates/classic/html/issue.search.html:97
 msgid "Activity:"
 msgstr "äÅÊÓÔ×ÉÅ:"
 
-#: ../templates/classic/html/issue.search.html:105
+#: ../templates/classic/html/issue.search.html:108
 msgid "Actor:"
 msgstr "÷ÙÐÏÌÎÉÌ:"
 
-#: ../templates/classic/html/issue.search.html:107
+#: ../templates/classic/html/issue.search.html:110
 msgid "done by me"
 msgstr "×ÙÐÏÌÎÅÎÏ ÍÎÏÊ"
 
-#: ../templates/classic/html/issue.search.html:118
+#: ../templates/classic/html/issue.search.html:121
 msgid "Priority:"
 msgstr "ðÒÉÏÒÉÔÅÔ:"
 
-#: ../templates/classic/html/issue.search.html:120
-#: ../templates/classic/html/issue.search.html:136
-msgid "not selected"
-msgstr "ÎÅ ÕÓÔÁÎÏ×ÌÅÎ"
-
-#: ../templates/classic/html/issue.search.html:131
+#: ../templates/classic/html/issue.search.html:134
 msgid "Status:"
 msgstr "óÔÁÔÕÓ:"
 
-#: ../templates/classic/html/issue.search.html:134
+#: ../templates/classic/html/issue.search.html:137
 msgid "not resolved"
 msgstr "ÎÅ ÚÁËÒÙÔ"
 
-#: ../templates/classic/html/issue.search.html:149
+#: ../templates/classic/html/issue.search.html:152
 msgid "Assigned to:"
 msgstr "éÓÐÏÌÎÉÔÅÌØ:"
 
-#: ../templates/classic/html/issue.search.html:152
+#: ../templates/classic/html/issue.search.html:155
 msgid "assigned to me"
 msgstr "ÐÏÒÕÞÅÎÏ ÍÎÅ"
 
-#: ../templates/classic/html/issue.search.html:154
+#: ../templates/classic/html/issue.search.html:157
 msgid "unassigned"
 msgstr "ÎÅÎÁÚÎÁÞÅÎÏ"
 
-#: ../templates/classic/html/issue.search.html:164
+#: ../templates/classic/html/issue.search.html:167
 msgid "No Sort or group:"
 msgstr "îÅ ÓÏÒÔÉÒÏ×ÁÔØ / ÎÅ ÇÒÕÐÐÉÒÏ×ÁÔØ"
 
-#: ../templates/classic/html/issue.search.html:172
+#: ../templates/classic/html/issue.search.html:175
 msgid "Pagesize:"
 msgstr "òÁÚÍÅÒ ÓÔÒÁÎÉÃÙ:"
 
-#: ../templates/classic/html/issue.search.html:178
+#: ../templates/classic/html/issue.search.html:181
 msgid "Start With:"
 msgstr "îÁÞÁÔØ Ó:"
 
-#: ../templates/classic/html/issue.search.html:184
+#: ../templates/classic/html/issue.search.html:187
 msgid "Sort Descending:"
 msgstr "óÏÒÔÉÒÏ×ÁÔØ ÐÏ ÕÂÙ×ÁÎÉÀ:"
 
-#: ../templates/classic/html/issue.search.html:191
+#: ../templates/classic/html/issue.search.html:194
 msgid "Group Descending:"
 msgstr "çÒÕÐÐÉÒÏ×ÁÔØ ÐÏ ÕÂÙ×ÁÎÉÀ"
 
-#: ../templates/classic/html/issue.search.html:198
+#: ../templates/classic/html/issue.search.html:201
 msgid "Query name**:"
 msgstr "éÍÑ ÚÁÐÒÏÓÁ**:"
 
-#: ../templates/classic/html/issue.search.html:210
+#: ../templates/classic/html/issue.search.html:213
 #: ../templates/classic/html/page.html:43
 #: ../templates/classic/html/page.html:92
 #: ../templates/classic/html/user.help-search.html:69
@@ -2936,11 +2988,11 @@
 msgid "Search"
 msgstr "ðÏÉÓË"
 
-#: ../templates/classic/html/issue.search.html:215
+#: ../templates/classic/html/issue.search.html:218
 msgid "*: The \"all text\" field will look in message bodies and issue titles"
 msgstr "*: ðÏÉÓË ÐÏ ×ÓÅÍÕ ÔÅËÓÔÕ ÉÝÅÔ ××ÅÄÅÎÎÕÀ ÓÔÒÏËÕ × ÚÁÇÏÌÏ×ËÁÈ É × ÔÅÌÅ ÓÏÏÂÝÅÎÉÊ."
 
-#: ../templates/classic/html/issue.search.html:218
+#: ../templates/classic/html/issue.search.html:221
 msgid "**: If you supply a name, the query will be saved off and available as a link in the sidebar"
 msgstr "**: åÓÌÉ ÕËÁÚÁÎÏ ÉÍÑ, ÚÁÐÒÏÓ ÂÕÄÅÔ ÓÏÈÒÁÎÅÎ ÐÏÄ ÜÔÉÍ ÉÍÅÎÅÍ É ÐÏÑ×ÉÔÓÑ × ÓÐÉÓËÅ ÚÁÐÒÏÓÏ× × ÍÅÎÀ."
 
@@ -2964,10 +3016,6 @@
 msgid "To create a new keyword, enter it below and click \"Submit New Entry\"."
 msgstr "þÔÏÂÙ ÓÏÚÄÁÔØ ÎÏ×ÏÅ ËÌÀÞÅ×ÏÅ ÓÌÏ×Ï, ÚÁÐÏÌÎÉÔÅ ÐÏÌÅ ××ÏÄÁ É ÎÁÖÍÉÔÅ ËÎÏÐËÕ \"äÏÂÁ×ÉÔØ\"."
 
-#: ../templates/classic/html/keyword.item.html:37
-msgid "Keyword"
-msgstr "ëÌÀÞÅ×ÏÅ ÓÌÏ×Ï"
-
 #: ../templates/classic/html/msg.index.html:3
 msgid "List of messages - ${tracker}"
 msgstr "óÐÉÓÏË ÓÏÏÂÝÅÎÉÊ - ${tracker}"
@@ -3044,11 +3092,6 @@
 msgid "Show issue:"
 msgstr "ðÏËÁÚÁÔØ:"
 
-#: ../templates/classic/html/page.html:103
-#: ../templates/minimal/html/page.html:102
-msgid "Keywords"
-msgstr "ëÌÀÞÅ×ÙÅ&nbsp;ÓÌÏ×Á"
-
 #: ../templates/classic/html/page.html:108
 #: ../templates/minimal/html/page.html:107
 msgid "Edit Existing"
@@ -3332,26 +3375,26 @@
 msgid "User${id} Editing"
 msgstr "òÅÄÁËÔÉÒÏ×ÁÎÉÅ ËÁÒÔÏÞËÉ ÐÏÌØÚÏ×ÁÔÅÌÑ ${id}"
 
-#: ../templates/classic/html/user.item.html:79
+#: ../templates/classic/html/user.item.html:80
 #: ../templates/classic/html/user.register.html:33
-#: ../templates/minimal/html/user.item.html:74
+#: ../templates/minimal/html/user.item.html:80
 #: ../templates/minimal/html/user.register.html:41
 msgid "Roles"
 msgstr "òÏÌÉ"
 
-#: ../templates/classic/html/user.item.html:87
-#: ../templates/minimal/html/user.item.html:82
+#: ../templates/classic/html/user.item.html:88
+#: ../templates/minimal/html/user.item.html:88
 msgid "(to give the user more than one role, enter a comma,separated,list)"
 msgstr "(ÅÓÌÉ ÒÏÌÅÊ ÎÅÓËÏÌØËÏ, ÐÅÒÅÞÉÓÌÉÔÅ ÉÈ ÞÅÒÅÚ ÚÁÐÑÔÕÀ)"
 
-#: ../templates/classic/html/user.item.html:108
-#: ../templates/minimal/html/user.item.html:103
+#: ../templates/classic/html/user.item.html:109
+#: ../templates/minimal/html/user.item.html:109
 msgid "(this is a numeric hour offset, the default is ${zone})"
 msgstr "(ÞÉÓÌÏ - ÒÁÚÎÉÃÁ ÍÅÖÄÕ ÍÅÓÔÎÙÍ É ÇÒÉÎ×ÉÞÓËÉÍ ×ÒÅÍÅÎÅÍ, ÐÏ ÕÍÏÌÞÁÎÉÀ - ${zone})"
 
-#: ../templates/classic/html/user.item.html:129
+#: ../templates/classic/html/user.item.html:130
 #: ../templates/classic/html/user.register.html:53
-#: ../templates/minimal/html/user.item.html:124
+#: ../templates/minimal/html/user.item.html:130
 #: ../templates/minimal/html/user.register.html:53
 msgid "Alternate E-mail addresses<br>One address per line"
 msgstr "äÏÐÏÌÎÉÔÅÌØÎÙÅ ÁÄÒÅÓÁ email<br />ðÏ ÏÄÎÏÍÕ ÁÄÒÅÓÕ × ÓÔÒÏËÅ"

Modified: tracker/roundup-src/roundup/__init__.py
==============================================================================
--- tracker/roundup-src/roundup/__init__.py	(original)
+++ tracker/roundup-src/roundup/__init__.py	Sun Mar  9 09:26:16 2008
@@ -14,8 +14,8 @@
 # FOR A PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
-# 
-# $Id: __init__.py,v 1.45 2006/12/19 03:03:37 richard Exp $
+#
+# $Id: __init__.py,v 1.49 2007/12/23 01:52:07 richard Exp $
 
 '''Roundup - issue tracking for knowledge workers.
 
@@ -27,14 +27,14 @@
 new issues, (b) find and edit existing issues, and (c) discuss issues with
 other participants. The system will facilitate communication among the
 participants by managing discussions and notifying interested parties when
-issues are edited. 
+issues are edited.
 
 Roundup's structure is that of a cake::
 
   _________________________________________________________________________
  |  E-mail Client   |   Web Browser   |   Detector Scripts   |    Shell    |
  |------------------+-----------------+----------------------+-------------|
- |   E-mail User    |    Web User     |      Detector        |   Command   | 
+ |   E-mail User    |    Web User     |      Detector        |   Command   |
  |-------------------------------------------------------------------------|
  |                         Roundup Database Layer                          |
  |-------------------------------------------------------------------------|
@@ -68,6 +68,6 @@
 '''
 __docformat__ = 'restructuredtext'
 
-__version__ = '1.3.2'
+__version__ = '1.4.2'
 
 # vim: set filetype=python ts=4 sw=4 et si

Modified: tracker/roundup-src/roundup/admin.py
==============================================================================
--- tracker/roundup-src/roundup/admin.py	(original)
+++ tracker/roundup-src/roundup/admin.py	Sun Mar  9 09:26:16 2008
@@ -16,7 +16,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-# $Id: admin.py,v 1.105 2006/08/11 05:13:06 richard Exp $
+# $Id: admin.py,v 1.110 2008/02/07 03:28:33 richard Exp $
 
 '''Administration commands for maintaining Roundup trackers.
 '''
@@ -75,6 +75,7 @@
                 self.help[k[5:]] = getattr(self, k)
         self.tracker_home = ''
         self.db = None
+        self.db_uncommitted = False
 
     def get_class(self, classname):
         '''Get the class - raise an exception if it doesn't exist.
@@ -286,26 +287,31 @@
 
         Look in the following places, where the later rules take precedence:
 
-         1. <prefix>/share/roundup/templates/*
+         1. <roundup.admin.__file__>/../../share/roundup/templates/*
+            this is where they will be if we installed an egg via easy_install
+         2. <prefix>/share/roundup/templates/*
             this should be the standard place to find them when Roundup is
             installed
-         2. <roundup.admin.__file__>/../templates/*
+         3. <roundup.admin.__file__>/../templates/*
             this will be used if Roundup's run in the distro (aka. source)
             directory
-         3. <current working dir>/*
+         4. <current working dir>/*
             this is for when someone unpacks a 3rd-party template
-         4. <current working dir>
+         5. <current working dir>
             this is for someone who "cd"s to the 3rd-party template dir
         '''
         # OK, try <prefix>/share/roundup/templates
+        #     and <egg-directory>/share/roundup/templates
         # -- this module (roundup.admin) will be installed in something
         # like:
-        #    /usr/lib/python2.2/site-packages/roundup/admin.py  (5 dirs up)
-        #    c:\python22\lib\site-packages\roundup\admin.py     (4 dirs up)
-        # we're interested in where the "lib" directory is - ie. the /usr/
-        # part
+        #    /usr/lib/python2.5/site-packages/roundup/admin.py  (5 dirs up)
+        #    c:\python25\lib\site-packages\roundup\admin.py     (4 dirs up)
+        #    /usr/lib/python2.5/site-packages/roundup-1.3.3-py2.5-egg/roundup/admin.py
+        #    (2 dirs up)
+        #
+        # we're interested in where the directory containing "share" is
         templates = {}
-        for N in 4, 5:
+        for N in 2, 4, 5:
             path = __file__
             # move up N elements in the path
             for i in range(N):
@@ -642,6 +648,7 @@
             except (TypeError, IndexError, ValueError), message:
                 import traceback; traceback.print_exc()
                 raise UsageError, message
+        self.db_uncommitted = True
         return 0
 
     def do_find(self, args):
@@ -749,7 +756,7 @@
             keys.sort()
             for key in keys:
                 value = cl.get(nodeid, key)
-                print _('%(key)s: %(value)r')%locals()
+                print _('%(key)s: %(value)s')%locals()
 
     def do_create(self, args):
         ""'''Usage: create classname property=value ...
@@ -813,6 +820,7 @@
             print apply(cl.create, (), props)
         except (TypeError, IndexError, ValueError), message:
             raise UsageError, message
+        self.db_uncommitted = True
         return 0
 
     def do_list(self, args):
@@ -995,6 +1003,7 @@
         they are successful.
         '''
         self.db.commit()
+        self.db_uncommitted = False
         return 0
 
     def do_rollback(self, args):
@@ -1007,6 +1016,7 @@
         immediately after would make no changes to the database.
         '''
         self.db.rollback()
+        self.db_uncommitted = False
         return 0
 
     def do_retire(self, args):
@@ -1030,6 +1040,7 @@
                 raise UsageError, _('no such class "%(classname)s"')%locals()
             except IndexError:
                 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
+        self.db_uncommitted = True
         return 0
 
     def do_restore(self, args):
@@ -1052,6 +1063,7 @@
                 raise UsageError, _('no such class "%(classname)s"')%locals()
             except IndexError:
                 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
+        self.db_uncommitted = True
         return 0
 
     def do_export(self, args, export_files=True):
@@ -1110,10 +1122,7 @@
             # all nodes for this class
             for nodeid in cl.getnodeids():
                 if self.verbose:
-                    sys.stdout.write('Exporting %s - %s\r'%(classname, nodeid))
-                    sys.stdout.flush()
-                if self.verbose:
-                    sys.stdout.write('Exporting %s - %s\r'%(classname, nodeid))
+                    sys.stdout.write('\rExporting %s - %s'%(classname, nodeid))
                     sys.stdout.flush()
                 writer.writerow(cl.export_list(propnames, nodeid))
                 if export_files and hasattr(cl, 'export_files'):
@@ -1198,7 +1207,7 @@
                     continue
 
                 if self.verbose:
-                    sys.stdout.write('Importing %s - %s\r'%(classname, n))
+                    sys.stdout.write('\rImporting %s - %s'%(classname, n))
                     sys.stdout.flush()
 
                 # do the import and figure the current highest nodeid
@@ -1219,6 +1228,7 @@
             print 'setting', classname, maxid+1
             self.db.setid(classname, str(maxid+1))
 
+        self.db_uncommitted = True
         return 0
 
     def do_pack(self, args):
@@ -1257,6 +1267,7 @@
         elif m['date']:
             pack_before = date.Date(value)
         self.db.pack(pack_before)
+        self.db_uncommitted = True
         return 0
 
     def do_reindex(self, args, desre=re.compile('([A-Za-z]+)([0-9]+)')):
@@ -1322,6 +1333,33 @@
                     print _(' %(description)s (%(name)s)')%d
         return 0
 
+
+    def do_migrate(self, args):
+        '''Usage: migrate
+        Update a tracker's database to be compatible with the Roundup
+        codebase.
+
+        You should run the "migrate" command for your tracker once you've
+        installed the latest codebase. 
+
+        Do this before you use the web, command-line or mail interface and
+        before any users access the tracker.
+
+        This command will respond with either "Tracker updated" (if you've
+        not previously run it on an RDBMS backend) or "No migration action
+        required" (if you have run it, or have used another interface to the
+        tracker, or possibly because you are using anydbm).
+
+        It's safe to run this even if it's not required, so just get into
+        the habit.
+        '''
+        if getattr(self.db, 'db_version_updated'):
+            print _('Tracker updated')
+            self.db_uncommitted = True
+        else:
+            print _('No migration action required')
+        return 0
+
     def run_command(self, args):
         '''Run a single command
         '''
@@ -1427,7 +1465,7 @@
             self.run_command(args)
 
         # exit.. check for transactions
-        if self.db and self.db.transactions:
+        if self.db and self.db_uncommitted:
             commit = raw_input(_('There are unsaved changes. Commit them (y/N)? '))
             if commit and commit[0].lower() == 'y':
                 self.db.commit()

Modified: tracker/roundup-src/roundup/backends/__init__.py
==============================================================================
--- tracker/roundup-src/roundup/backends/__init__.py	(original)
+++ tracker/roundup-src/roundup/backends/__init__.py	Sun Mar  9 09:26:16 2008
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-# $Id: __init__.py,v 1.39 2006/10/09 23:49:32 richard Exp $
+# $Id: __init__.py,v 1.40 2007/11/07 20:47:12 richard Exp $
 
 '''Container for the hyperdb storage backend implementations.
 '''
@@ -80,7 +80,7 @@
 
     '''
     l = []
-    for name in 'anydbm', 'mysql', 'sqlite', 'metakit', 'postgresql':
+    for name in 'anydbm', 'mysql', 'sqlite', 'postgresql':
         if have_backend(name):
             l.append(name)
     return l

Modified: tracker/roundup-src/roundup/backends/back_anydbm.py
==============================================================================
--- tracker/roundup-src/roundup/backends/back_anydbm.py	(original)
+++ tracker/roundup-src/roundup/backends/back_anydbm.py	Sun Mar  9 09:26:16 2008
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-#$Id: back_anydbm.py,v 1.202 2006/08/29 04:20:50 richard Exp $
+#$Id: back_anydbm.py,v 1.210 2008/02/07 00:57:59 richard Exp $
 '''This module defines a backend that saves the hyperdatabase in a
 database chosen by anydbm. It is guaranteed to always be available in python
 versions >2.1.1 (the dumbdbm fallback in 2.1.1 and earlier has several
@@ -85,6 +85,7 @@
         Class.set(), Class.retire(), and Class.restore() methods are
         disabled.
         '''
+        FileStorage.__init__(self, config.UMASK)
         self.config, self.journaltag = config, journaltag
         self.dir = config.DATABASE
         self.classes = {}
@@ -214,7 +215,8 @@
         if os.path.exists(path):
             db_type = whichdb.whichdb(path)
             if not db_type:
-                raise hyperdb.DatabaseError, "Couldn't identify database type"
+                raise hyperdb.DatabaseError, \
+                    _("Couldn't identify database type")
         elif os.path.exists(path+'.db'):
             # if the path ends in '.db', it's a dbm database, whether
             # anydbm says it's dbhash or not!
@@ -240,8 +242,8 @@
             dbm = __import__(db_type)
         except ImportError:
             raise hyperdb.DatabaseError, \
-                "Couldn't open database - the required module '%s'"\
-                " is not available"%db_type
+                _("Couldn't open database - the required module '%s'"\
+                " is not available")%db_type
         if __debug__:
             logging.getLogger('hyperdb').debug("opendb %r.open(%r, %r)"%(db_type, path,
                 mode))
@@ -376,6 +378,7 @@
 
         # add the destroy commit action
         self.transactions.append((self.doDestroyNode, (classname, nodeid)))
+        self.transactions.append((FileStorage.destroy, (self, classname, nodeid)))
 
     def serialise(self, classname, node):
         '''Copy the node contents, converting non-marshallable data into
@@ -619,6 +622,11 @@
                 db.close()
             del self.databases
 
+        # clear the transactions list now so the blobfile implementation
+        # doesn't think there's still pending file commits when it tries
+        # to access the file data
+        self.transactions = []
+
         # reindex the nodes that request it
         for classname, nodeid in filter(None, reindex.keys()):
             self.getclass(classname).index(nodeid)
@@ -717,9 +725,6 @@
         if db.has_key(nodeid):
             del db[nodeid]
 
-        # return the classname, nodeid so we reindex this content
-        return (classname, nodeid)
-
     def rollback(self):
         ''' Reverse all actions from the current transaction.
         '''
@@ -792,7 +797,7 @@
             raise KeyError, '"id" is reserved'
 
         if self.db.journaltag is None:
-            raise hyperdb.DatabaseError, 'Database open read-only'
+            raise hyperdb.DatabaseError, _('Database open read-only')
 
         if propvalues.has_key('creation') or propvalues.has_key('activity'):
             raise KeyError, '"creation" and "activity" are reserved'
@@ -840,8 +845,10 @@
                         (self.classname, newid, key))
 
             elif isinstance(prop, hyperdb.Multilink):
-                if type(value) != type([]):
-                    raise TypeError, 'new property "%s" not a list of ids'%key
+                if value is None:
+                    value = []
+                if not hasattr(value, '__iter__'):
+                    raise TypeError, 'new property "%s" not an iterable of ids'%key
 
                 # clean up and validate the list of links
                 link_class = self.properties[key].classname
@@ -1065,7 +1072,7 @@
             raise KeyError, '"id" is reserved'
 
         if self.db.journaltag is None:
-            raise hyperdb.DatabaseError, 'Database open read-only'
+            raise hyperdb.DatabaseError, _('Database open read-only')
 
         node = self.db.getnode(self.classname, nodeid)
         if node.has_key(self.db.RETIRED_FLAG):
@@ -1132,8 +1139,10 @@
                             (self.classname, nodeid, propname))
 
             elif isinstance(prop, hyperdb.Multilink):
-                if type(value) != type([]):
-                    raise TypeError, 'new property "%s" not a list of'\
+                if value is None:
+                    value = []
+                if not hasattr(value, '__iter__'):
+                    raise TypeError, 'new property "%s" not an iterable of'\
                         ' ids'%propname
                 link_class = self.properties[propname].classname
                 l = []
@@ -1260,7 +1269,7 @@
         to modify the "creation" or "activity" properties cause a KeyError.
         '''
         if self.db.journaltag is None:
-            raise hyperdb.DatabaseError, 'Database open read-only'
+            raise hyperdb.DatabaseError, _('Database open read-only')
 
         self.fireAuditors('retire', nodeid, None)
 
@@ -1278,7 +1287,7 @@
         Make node available for all operations like it was before retirement.
         '''
         if self.db.journaltag is None:
-            raise hyperdb.DatabaseError, 'Database open read-only'
+            raise hyperdb.DatabaseError, _('Database open read-only')
 
         node = self.db.getnode(self.classname, nodeid)
         # check if key property was overrided
@@ -1324,7 +1333,7 @@
         support the session storage of the cgi interface.
         '''
         if self.db.journaltag is None:
-            raise hyperdb.DatabaseError, 'Database open read-only'
+            raise hyperdb.DatabaseError, _('Database open read-only')
         self.db.destroynode(self.classname, nodeid)
 
     def history(self, nodeid):
@@ -1517,7 +1526,7 @@
         must_close = False
         if db is None:
             db = self.db.getclassdb(self.classname)
-            must_close = True 
+            must_close = True
         try:
             res = res + db.keys()
 
@@ -1556,7 +1565,7 @@
 
         The filter must match all properties specificed. If the property
         value to match is a list:
-        
+
         1. String properties must match all elements in the list, and
         2. Other properties must match any of the elements in the list.
         """
@@ -1640,7 +1649,7 @@
                 l.append((OTHER, k, [float(val) for val in v]))
 
         filterspec = l
-
+        
         # now, find all the nodes that are active and pass filtering
         matches = []
         cldb = self.db.getclassdb(cn)
@@ -1894,7 +1903,7 @@
             Return the nodeid of the node imported.
         '''
         if self.db.journaltag is None:
-            raise hyperdb.DatabaseError, 'Database open read-only'
+            raise hyperdb.DatabaseError, _('Database open read-only')
         properties = self.getprops()
 
         # make the new node's property map
@@ -1962,8 +1971,7 @@
                         prop = properties[propname]
                         # make sure the params are eval()'able
                         if value is None:
-                            # don't export empties
-                            continue
+                            pass
                         elif isinstance(prop, hyperdb.Date):
                             # this is a hack - some dates are stored as strings
                             if not isinstance(value, type('')):

Modified: tracker/roundup-src/roundup/backends/back_metakit.py
==============================================================================
--- tracker/roundup-src/roundup/backends/back_metakit.py	(original)
+++ tracker/roundup-src/roundup/backends/back_metakit.py	Sun Mar  9 09:26:16 2008
@@ -1,2142 +0,0 @@
-# $Id: back_metakit.py,v 1.113 2006/08/29 04:20:50 richard Exp $
-'''Metakit backend for Roundup, originally by Gordon McMillan.
-
-Known Current Bugs:
-
-- You can't change a class' key properly. This shouldn't be too hard to fix.
-- Some unit tests are overridden.
-
-Notes by Richard:
-
-This backend has some behaviour specific to metakit:
-
-- there's no concept of an explicit "unset" in metakit, so all types
-  have some "unset" value:
-
-  ========= ===== ======================================================
-  Type      Value Action when fetching from mk
-  ========= ===== ======================================================
-  Strings   ''    convert to None
-  Date      0     (seconds since 1970-01-01.00:00:00) convert to None
-  Interval  ''    convert to None
-  Number    0     ambiguious :( - do nothing (see BACKWARDS_COMPATIBLE)
-  Boolean   0     ambiguious :( - do nothing (see BACKWARDS_COMPATABILE)
-  Link      0     convert to None
-  Multilink []    actually, mk can handle this one ;)
-  Password  ''    convert to None
-  ========= ===== ======================================================
-
-  The get/set routines handle these values accordingly by converting
-  to/from None where they can. The Number/Boolean types are not able
-  to handle an "unset" at all, so they default the "unset" to 0.
-- Metakit relies in reference counting to close the database, there is
-  no explicit close call.  This can cause issues if a metakit
-  database is referenced multiple times, one might not actually be
-  closing the db.
-- probably a bunch of stuff that I'm not aware of yet because I haven't
-  fully read through the source. One of these days....
-'''
-__docformat__ = 'restructuredtext'
-# Enable this flag to break backwards compatibility (i.e. can't read old
-# databases) but comply with more roundup features, like adding NULL support.
-BACKWARDS_COMPATIBLE = 1
-
-from roundup import hyperdb, date, password, roundupdb, security
-from roundup.support import reversed
-import logging
-import metakit
-from sessions_dbm import Sessions, OneTimeKeys
-import re, marshal, os, sys, time, calendar, shutil
-from indexer_common import Indexer as CommonIndexer
-import locking
-from roundup.date import Range
-from blobfiles import files_in_dir
-
-# view modes for opening
-# XXX FIXME BPK -> these don't do anything, they are ignored
-#  should we just get rid of them for simplicities sake?
-READ = 0
-READWRITE = 1
-
-def db_exists(config):
-    return os.path.exists(os.path.join(config.TRACKER_HOME, 'db',
-        'tracker.mk4'))
-
-def db_nuke(config):
-    shutil.rmtree(os.path.join(config.TRACKER_HOME, 'db'))
-
-# general metakit error
-class MKBackendError(Exception):
-    pass
-
-_dbs = {}
-
-def Database(config, journaltag=None):
-    ''' Only have a single instance of the Database class for each instance
-    '''
-    db = _dbs.get(config.DATABASE, None)
-    if db is None or db._db is None:
-        db = _Database(config, journaltag)
-        _dbs[config.DATABASE] = db
-    else:
-        db.journaltag = journaltag
-    return db
-
-class _Database(hyperdb.Database, roundupdb.Database):
-    # Metakit has no concept of an explicit NULL
-    BACKEND_MISSING_STRING = ''
-    BACKEND_MISSING_NUMBER = 0
-    BACKEND_MISSING_BOOLEAN = 0
-
-    def __init__(self, config, journaltag=None):
-        self.config = config
-        self.journaltag = journaltag
-        self.classes = {}
-        self.dirty = 0
-        self.lockfile = None
-        self._db = self.__open()
-        self.indexer = Indexer(self)
-        self.security = security.Security(self)
-
-        self.stats = {'cache_hits': 0, 'cache_misses': 0, 'get_items': 0,
-            'filtering': 0}
-
-        os.umask(config.UMASK)
-
-    def post_init(self):
-        if self.indexer.should_reindex():
-            self.reindex()
-
-    def refresh_database(self):
-        # XXX handle refresh
-        self.reindex()
-
-    def reindex(self, classname=None):
-        if classname:
-            classes = [self.getclass(classname)]
-        else:
-            classes = self.classes.values()
-        for klass in classes:
-            for nodeid in klass.list():
-                klass.index(nodeid)
-        self.indexer.save_index()
-
-    def getSessionManager(self):
-        return Sessions(self)
-
-    def getOTKManager(self):
-        return OneTimeKeys(self)
-
-    # --- defined in ping's spec
-    def __getattr__(self, classname):
-        if classname == 'transactions':
-            return self.dirty
-        # fall back on the classes
-        try:
-            return self.getclass(classname)
-        except KeyError, msg:
-            # KeyError's not appropriate here
-            raise AttributeError, str(msg)
-    def getclass(self, classname):
-        try:
-            return self.classes[classname]
-        except KeyError:
-            raise KeyError, 'There is no class called "%s"'%classname
-    def getclasses(self):
-        return self.classes.keys()
-    # --- end of ping's spec
-
-    # --- exposed methods
-    def commit(self, fail_ok=False):
-        ''' Commit the current transactions.
-
-        Save all data changed since the database was opened or since the
-        last commit() or rollback().
-
-        fail_ok indicates that the commit is allowed to fail. This is used
-        in the web interface when committing cleaning of the session
-        database. We don't care if there's a concurrency issue there.
-
-        The only backend this seems to affect is postgres.
-        '''
-        if self.dirty:
-            self._db.commit()
-            for cl in self.classes.values():
-                cl._commit()
-            self.indexer.save_index()
-        self.dirty = 0
-    def rollback(self):
-        '''roll back all changes since the last commit'''
-        if self.dirty:
-            for cl in self.classes.values():
-                cl._rollback()
-            self._db.rollback()
-            self._db = None
-            self._db = metakit.storage(self.dbnm, 1)
-            self.hist = self._db.view('history')
-            self.tables = self._db.view('tables')
-            self.indexer.rollback()
-            self.indexer.datadb = self._db
-        self.dirty = 0
-    def clearCache(self):
-        '''clear the internal cache by committing all pending database changes'''
-        for cl in self.classes.values():
-            cl._commit()
-    def clear(self):
-        '''clear the internal cache but don't commit any changes'''
-        for cl in self.classes.values():
-            cl._clear()
-    def hasnode(self, classname, nodeid):
-        '''does a particular class contain a nodeid?'''
-        return self.getclass(classname).hasnode(nodeid)
-    def pack(self, pack_before):
-        ''' Delete all journal entries except "create" before 'pack_before'.
-        '''
-        mindate = int(calendar.timegm(pack_before.get_tuple()))
-        i = 0
-        while i < len(self.hist):
-            if self.hist[i].date < mindate and self.hist[i].action != _CREATE:
-                self.hist.delete(i)
-            else:
-                i = i + 1
-    def addclass(self, cl):
-        ''' Add a Class to the hyperdatabase.
-        '''
-        cn = cl.classname
-        self.classes[cn] = cl
-        if self.tables.find(name=cn) < 0:
-            self.tables.append(name=cn)
-
-        # add default Edit and View permissions
-        self.security.addPermission(name="Create", klass=cn,
-            description="User is allowed to create "+cn)
-        self.security.addPermission(name="Edit", klass=cn,
-            description="User is allowed to edit "+cn)
-        self.security.addPermission(name="View", klass=cn,
-            description="User is allowed to access "+cn)
-
-    def addjournal(self, tablenm, nodeid, action, params, creator=None,
-                   creation=None):
-        ''' Journal the Action
-        'action' may be:
-
-            'create' or 'set' -- 'params' is a dictionary of property values
-            'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
-            'retire' -- 'params' is None
-        '''
-        tblid = self.tables.find(name=tablenm)
-        if tblid == -1:
-            tblid = self.tables.append(name=tablenm)
-        if creator is None:
-            creator = int(self.getuid())
-        else:
-            try:
-                creator = int(creator)
-            except TypeError:
-                creator = int(self.getclass('user').lookup(creator))
-        if creation is None:
-            creation = int(time.time())
-        elif isinstance(creation, date.Date):
-            creation = int(calendar.timegm(creation.get_tuple()))
-        # tableid:I,nodeid:I,date:I,user:I,action:I,params:B
-        self.hist.append(tableid=tblid,
-                         nodeid=int(nodeid),
-                         date=creation,
-                         action=action,
-                         user=creator,
-                         params=marshal.dumps(params))
-
-    def setjournal(self, tablenm, nodeid, journal):
-        '''Set the journal to the "journal" list.'''
-        tblid = self.tables.find(name=tablenm)
-        if tblid == -1:
-            tblid = self.tables.append(name=tablenm)
-        for nodeid, date, user, action, params in journal:
-            # tableid:I,nodeid:I,date:I,user:I,action:I,params:B
-            self.hist.append(tableid=tblid,
-                             nodeid=int(nodeid),
-                             date=date,
-                             action=action,
-                             user=int(user),
-                             params=marshal.dumps(params))
-
-    def getjournal(self, tablenm, nodeid):
-        ''' get the journal for id
-        '''
-        rslt = []
-        tblid = self.tables.find(name=tablenm)
-        if tblid == -1:
-            return rslt
-        q = self.hist.select(tableid=tblid, nodeid=int(nodeid))
-        if len(q) == 0:
-            raise IndexError, "no history for id %s in %s" % (nodeid, tablenm)
-        i = 0
-        #userclass = self.getclass('user')
-        for row in q:
-            try:
-                params = marshal.loads(row.params)
-            except ValueError:
-                logging.getLogger("hyperdb").error(
-                    "history couldn't unmarshal %r" % row.params)
-                params = {}
-            #usernm = userclass.get(str(row.user), 'username')
-            dt = date.Date(time.gmtime(row.date))
-            #rslt.append((nodeid, dt, usernm, _actionnames[row.action], params))
-            rslt.append((nodeid, dt, str(row.user), _actionnames[row.action],
-                params))
-        return rslt
-
-    def destroyjournal(self, tablenm, nodeid):
-        nodeid = int(nodeid)
-        tblid = self.tables.find(name=tablenm)
-        if tblid == -1:
-            return
-        i = 0
-        hist = self.hist
-        while i < len(hist):
-            if hist[i].tableid == tblid and hist[i].nodeid == nodeid:
-                hist.delete(i)
-            else:
-                i = i + 1
-        self.dirty = 1
-
-    def close(self):
-        ''' Close off the connection.
-        '''
-        # de-reference count the metakit databases,
-        #  as this is the only way they will be closed
-        for cl in self.classes.values():
-            cl.db = None
-        self._db = None
-        if self.lockfile is not None:
-            locking.release_lock(self.lockfile)
-        if _dbs.has_key(self.config.DATABASE):
-            del _dbs[self.config.DATABASE]
-        if self.lockfile is not None:
-            self.lockfile.close()
-            self.lockfile = None
-        self.classes = {}
-
-        # force the indexer to close
-        self.indexer.close()
-        self.indexer = None
-
-    # --- internal
-    def __open(self):
-        ''' Open the metakit database
-        '''
-        # make the database dir if it doesn't exist
-        if not os.path.exists(self.config.DATABASE):
-            os.makedirs(self.config.DATABASE)
-
-        # figure the file names
-        self.dbnm = db = os.path.join(self.config.DATABASE, 'tracker.mk4')
-        lockfilenm = db[:-3]+'lck'
-
-        # get the database lock
-        self.lockfile = locking.acquire_lock(lockfilenm)
-        self.lockfile.write(str(os.getpid()))
-        self.lockfile.flush()
-
-        # see if the schema has changed since last db access
-        self.fastopen = 0
-        if os.path.exists(db):
-            dbtm = os.path.getmtime(db)
-            schemafile = os.path.join(self.config['HOME'], 'schema.py')
-            if not os.path.isfile(schemafile):
-                # try old-style schema
-                schemafile = os.path.join(self.config['HOME'], 'dbinit.py')
-            if os.path.isfile(schemafile) \
-            and (os.path.getmtime(schemafile) < dbtm):
-                # found schema file - it's older than the db
-                self.fastopen = 1
-
-        # open the db
-        db = metakit.storage(db, 1)
-        hist = db.view('history')
-        tables = db.view('tables')
-        if not self.fastopen:
-            # create the database if it's brand new
-            if not hist.structure():
-                hist = db.getas('history[tableid:I,nodeid:I,date:I,user:I,action:I,params:B]')
-            if not tables.structure():
-                tables = db.getas('tables[name:S]')
-            db.commit()
-
-        # we now have an open, initialised database
-        self.tables = tables
-        self.hist = hist
-        return db
-
-    def setid(self, classname, maxid):
-        ''' No-op in metakit
-        '''
-        cls = self.getclass(classname)
-        cls.setid(int(maxid))
-
-    def numfiles(self):
-        '''Get number of files in storage, even across subdirectories.
-        '''
-        files_dir = os.path.join(self.config.DATABASE, 'files')
-        return files_in_dir(files_dir)
-
-_STRINGTYPE = type('')
-_LISTTYPE = type([])
-_CREATE, _SET, _RETIRE, _LINK, _UNLINK, _RESTORE = range(6)
-
-_actionnames = {
-    _CREATE : 'create',
-    _SET : 'set',
-    _RETIRE : 'retire',
-    _RESTORE : 'restore',
-    _LINK : 'link',
-    _UNLINK : 'unlink',
-}
-
-_names_to_actionnames = {
-    'create': _CREATE,
-    'set': _SET,
-    'retire': _RETIRE,
-    'restore': _RESTORE,
-    'link': _LINK,
-    'unlink': _UNLINK,
-}
-
-_marker = []
-
-_ALLOWSETTINGPRIVATEPROPS = 0
-
-class Class(hyperdb.Class):
-    ''' The handle to a particular class of nodes in a hyperdatabase.
-
-        All methods except __repr__ and getnode must be implemented by a
-        concrete backend Class of which this is one.
-    '''
-
-    privateprops = None
-    def __init__(self, db, classname, **properties):
-        if hasattr(db, classname):
-            raise ValueError, "Class %s already exists"%classname
-
-        hyperdb.Class.__init__ (self, db, classname, **properties)
-        self.db = db # why isn't this a weakref as for other backends??
-        self.key = None
-        self.ruprops = self.properties
-        self.privateprops = { 'id' : hyperdb.String(),
-                              'activity' : hyperdb.Date(),
-                              'actor' : hyperdb.Link('user'),
-                              'creation' : hyperdb.Date(),
-                              'creator'  : hyperdb.Link('user') }
-
-        self.idcache = {}
-        self.uncommitted = {}
-        self.comactions = []
-        self.rbactions = []
-
-        view = self.__getview()
-        self.maxid = 1
-        if view:
-            self.maxid = view[-1].id + 1
-
-    def setid(self, maxid):
-        self.maxid = maxid + 1
-
-    def enableJournalling(self):
-        '''Turn journalling on for this class
-        '''
-        self.do_journal = 1
-
-    def disableJournalling(self):
-        '''Turn journalling off for this class
-        '''
-        self.do_journal = 0
-
-    # --- the hyperdb.Class methods
-    def create(self, **propvalues):
-        ''' Create a new node of this class and return its id.
-
-        The keyword arguments in 'propvalues' map property names to values.
-
-        The values of arguments must be acceptable for the types of their
-        corresponding properties or a TypeError is raised.
-
-        If this class has a key property, it must be present and its value
-        must not collide with other key strings or a ValueError is raised.
-
-        Any other properties on this class that are missing from the
-        'propvalues' dictionary are set to None.
-
-        If an id in a link or multilink property does not refer to a valid
-        node, an IndexError is raised.
-        '''
-        if not propvalues:
-            raise ValueError, "Need something to create!"
-        self.fireAuditors('create', None, propvalues)
-        newid = self.create_inner(**propvalues)
-        self.fireReactors('create', newid, None)
-        return newid
-
-    def create_inner(self, **propvalues):
-       ''' Called by create, in-between the audit and react calls.
-       '''
-       rowdict = {}
-       rowdict['id'] = newid = self.maxid
-       self.maxid += 1
-       ndx = self.getview(READWRITE).append(rowdict)
-       propvalues['#ISNEW'] = 1
-       try:
-           self.set_inner(str(newid), **propvalues)
-       except Exception:
-           self.maxid -= 1
-           raise
-       return str(newid)
-
-    def get(self, nodeid, propname, default=_marker, cache=1):
-        '''Get the value of a property on an existing node of this class.
-
-        'nodeid' must be the id of an existing node of this class or an
-        IndexError is raised.  'propname' must be the name of a property
-        of this class or a KeyError is raised.
-
-        'cache' exists for backwards compatibility, and is not used.
-        '''
-        view = self.getview()
-        id = int(nodeid)
-        if cache == 0:
-            oldnode = self.uncommitted.get(id, None)
-            if oldnode and oldnode.has_key(propname):
-                raw = oldnode[propname]
-                converter = _converters.get(raw.__class__, None)
-                if converter:
-                    return converter(raw)
-                return raw
-        ndx = self.idcache.get(id, None)
-
-        if ndx is None:
-            ndx = view.find(id=id)
-            if ndx < 0:
-                raise IndexError, "%s has no node %s" % (self.classname, nodeid)
-            self.idcache[id] = ndx
-        try:
-            raw = getattr(view[ndx], propname)
-        except AttributeError:
-            raise KeyError, propname
-        rutyp = self.ruprops.get(propname, None)
-
-        if rutyp is None:
-            rutyp = self.privateprops[propname]
-
-        converter = _converters.get(rutyp.__class__, None)
-        if converter:
-            raw = converter(raw)
-        return raw
-
-    def set(self, nodeid, **propvalues):
-        '''Modify a property on an existing node of this class.
-
-        'nodeid' must be the id of an existing node of this class or an
-        IndexError is raised.
-
-        Each key in 'propvalues' must be the name of a property of this
-        class or a KeyError is raised.
-
-        All values in 'propvalues' must be acceptable types for their
-        corresponding properties or a TypeError is raised.
-
-        If the value of the key property is set, it must not collide with
-        other key strings or a ValueError is raised.
-
-        If the value of a Link or Multilink property contains an invalid
-        node id, a ValueError is raised.
-        '''
-        self.fireAuditors('set', nodeid, propvalues)
-        propvalues, oldnode = self.set_inner(nodeid, **propvalues)
-        self.fireReactors('set', nodeid, oldnode)
-
-    def set_inner(self, nodeid, **propvalues):
-        '''Called outside of auditors'''
-        isnew = 0
-        if propvalues.has_key('#ISNEW'):
-            isnew = 1
-            del propvalues['#ISNEW']
-
-        if propvalues.has_key('id'):
-            raise KeyError, '"id" is reserved'
-        if self.db.journaltag is None:
-            raise hyperdb.DatabaseError, 'Database open read-only'
-        view = self.getview(READWRITE)
-
-        # node must exist & not be retired
-        id = int(nodeid)
-        ndx = view.find(id=id)
-        if ndx < 0:
-            raise IndexError, "%s has no node %s" % (self.classname, nodeid)
-        row = view[ndx]
-        if row._isdel:
-            raise IndexError, "%s has no node %s" % (self.classname, nodeid)
-        oldnode = self.uncommitted.setdefault(id, {})
-        changes = {}
-
-        for key, value in propvalues.items():
-            # this will raise the KeyError if the property isn't valid
-            # ... we don't use getprops() here because we only care about
-            # the writeable properties.
-            if _ALLOWSETTINGPRIVATEPROPS:
-                prop = self.ruprops.get(key, None)
-                if not prop:
-                    prop = self.privateprops[key]
-            else:
-                prop = self.ruprops[key]
-            converter = _converters.get(prop.__class__, lambda v: v)
-            # if the value's the same as the existing value, no sense in
-            # doing anything
-            oldvalue = converter(getattr(row, key))
-            if  value == oldvalue:
-                del propvalues[key]
-                continue
-
-            # check to make sure we're not duplicating an existing key
-            if key == self.key:
-                iv = self.getindexview(READWRITE)
-                ndx = iv.find(k=value)
-                if ndx == -1:
-                    iv.append(k=value, i=row.id)
-                    if not isnew:
-                        ndx = iv.find(k=oldvalue)
-                        if ndx > -1:
-                            iv.delete(ndx)
-                else:
-                    raise ValueError, 'node with key "%s" exists'%value
-
-            # do stuff based on the prop type
-            if isinstance(prop, hyperdb.Link):
-                link_class = prop.classname
-                # must be a string or None
-                if value is not None and not isinstance(value, type('')):
-                    raise ValueError, 'property "%s" link value be a string'%(
-                        key)
-                # Roundup sets to "unselected" by passing None
-                if value is None:
-                    value = 0
-                # if it isn't a number, it's a key
-                try:
-                    int(value)
-                except ValueError:
-                    try:
-                        value = self.db.getclass(link_class).lookup(value)
-                    except (TypeError, KeyError):
-                        raise IndexError, 'new property "%s": %s not a %s'%(
-                            key, value, prop.classname)
-
-                if (value is not None and
-                        not self.db.getclass(link_class).hasnode(value)):
-                    raise IndexError, '%s has no node %s'%(link_class, value)
-
-                setattr(row, key, int(value))
-                changes[key] = oldvalue
-
-                if self.do_journal and prop.do_journal:
-                    # register the unlink with the old linked node
-                    if oldvalue:
-                        self.db.addjournal(link_class, oldvalue, _UNLINK,
-                            (self.classname, str(row.id), key))
-
-                    # register the link with the newly linked node
-                    if value:
-                        self.db.addjournal(link_class, value, _LINK,
-                            (self.classname, str(row.id), key))
-
-            elif isinstance(prop, hyperdb.Multilink):
-                if value is not None and type(value) != _LISTTYPE:
-                    raise TypeError, 'new property "%s" not a list of ids'%key
-                link_class = prop.classname
-                l = []
-                if value is None:
-                    value = []
-                for entry in value:
-                    if type(entry) != _STRINGTYPE:
-                        raise ValueError, 'new property "%s" link value ' \
-                            'must be a string'%key
-                    # if it isn't a number, it's a key
-                    try:
-                        int(entry)
-                    except ValueError:
-                        try:
-                            entry = self.db.getclass(link_class).lookup(entry)
-                        except (TypeError, KeyError):
-                            raise IndexError, 'new property "%s": %s not a %s'%(
-                                key, entry, prop.classname)
-                    l.append(entry)
-                propvalues[key] = value = l
-
-                # handle removals
-                rmvd = []
-                for id in oldvalue:
-                    if id not in value:
-                        rmvd.append(id)
-                        # register the unlink with the old linked node
-                        if self.do_journal and prop.do_journal:
-                            self.db.addjournal(link_class, id, _UNLINK,
-                                (self.classname, str(row.id), key))
-
-                # handle additions
-                adds = []
-                for id in value:
-                    if id not in oldvalue:
-                        if not self.db.getclass(link_class).hasnode(id):
-                            raise IndexError, '%s has no node %s'%(
-                                link_class, id)
-                        adds.append(id)
-                        # register the link with the newly linked node
-                        if self.do_journal and prop.do_journal:
-                            self.db.addjournal(link_class, id, _LINK,
-                                (self.classname, str(row.id), key))
-
-                # perform the modifications on the actual property value
-                sv = getattr(row, key)
-                i = 0
-                while i < len(sv):
-                    if str(sv[i].fid) in rmvd:
-                        sv.delete(i)
-                    else:
-                        i += 1
-                for id in adds:
-                    sv.append(fid=int(id))
-
-                # figure the journal entry
-                l = []
-                if adds:
-                    l.append(('+', adds))
-                if rmvd:
-                    l.append(('-', rmvd))
-                if l:
-                    changes[key] = tuple(l)
-                #changes[key] = oldvalue
-
-                if not rmvd and not adds:
-                    del propvalues[key]
-
-            elif isinstance(prop, hyperdb.String):
-                if value is not None and type(value) != _STRINGTYPE:
-                    raise TypeError, 'new property "%s" not a string'%key
-                if value is None:
-                    value = ''
-                setattr(row, key, value)
-                changes[key] = oldvalue
-                if hasattr(prop, 'isfilename') and prop.isfilename:
-                    propvalues[key] = os.path.basename(value)
-                if prop.indexme:
-                    self.db.indexer.add_text((self.classname, nodeid, key),
-                        value, 'text/plain')
-
-            elif isinstance(prop, hyperdb.Password):
-                if value is not None and not isinstance(value, password.Password):
-                    raise TypeError, 'new property "%s" not a Password'% key
-                if value is None:
-                    value = ''
-                setattr(row, key, str(value))
-                changes[key] = str(oldvalue)
-                propvalues[key] = str(value)
-
-            elif isinstance(prop, hyperdb.Date):
-                if value is not None and not isinstance(value, date.Date):
-                    raise TypeError, 'new property "%s" not a Date'% key
-                if value is None:
-                    setattr(row, key, 0)
-                else:
-                    setattr(row, key, int(calendar.timegm(value.get_tuple())))
-                if oldvalue is None:
-                    changes[key] = oldvalue
-                else:
-                    changes[key] = str(oldvalue)
-                propvalues[key] = str(value)
-
-            elif isinstance(prop, hyperdb.Interval):
-                if value is not None and not isinstance(value, date.Interval):
-                    raise TypeError, 'new property "%s" not an Interval'% key
-                if value is None:
-                    setattr(row, key, '')
-                else:
-                    # kedder: we should store interval values serialized
-                    setattr(row, key, value.serialise())
-                changes[key] = str(oldvalue)
-                propvalues[key] = str(value)
-
-            elif isinstance(prop, hyperdb.Number):
-                if value is None:
-                    v = 0
-                else:
-                    try:
-                        v = float(value)
-                    except ValueError:
-                        raise TypeError, "%s (%s) is not numeric"%(key, repr(value))
-                    if not BACKWARDS_COMPATIBLE:
-                        if v >=0:
-                            v = v + 1
-                setattr(row, key, v)
-                changes[key] = oldvalue
-                propvalues[key] = value
-
-            elif isinstance(prop, hyperdb.Boolean):
-                if value is None:
-                    bv = 0
-                elif value not in (0,1):
-                    raise TypeError, "%s (%s) is not boolean"%(key, repr(value))
-                else:
-                    bv = value
-                    if not BACKWARDS_COMPATIBLE:
-                        bv += 1
-                setattr(row, key, bv)
-                changes[key] = oldvalue
-                propvalues[key] = value
-
-            oldnode[key] = oldvalue
-
-        # nothing to do?
-        if not isnew and not propvalues:
-            return propvalues, oldnode
-        if not propvalues.has_key('activity'):
-            row.activity = int(time.time())
-        if not propvalues.has_key('actor'):
-            row.actor = int(self.db.getuid())
-        if isnew:
-            if not row.creation:
-                row.creation = int(time.time())
-            if not row.creator:
-                row.creator = int(self.db.getuid())
-
-        self.db.dirty = 1
-
-        if self.do_journal:
-            if isnew:
-                self.db.addjournal(self.classname, nodeid, _CREATE, {})
-            else:
-                self.db.addjournal(self.classname, nodeid, _SET, changes)
-
-        return propvalues, oldnode
-
-    def retire(self, nodeid):
-        '''Retire a node.
-
-        The properties on the node remain available from the get() method,
-        and the node's id is never reused.
-
-        Retired nodes are not returned by the find(), list(), or lookup()
-        methods, and other nodes may reuse the values of their key properties.
-        '''
-        if self.db.journaltag is None:
-            raise hyperdb.DatabaseError, 'Database open read-only'
-        self.fireAuditors('retire', nodeid, None)
-        view = self.getview(READWRITE)
-        ndx = view.find(id=int(nodeid))
-        if ndx < 0:
-            raise KeyError, "nodeid %s not found" % nodeid
-
-        row = view[ndx]
-        oldvalues = self.uncommitted.setdefault(row.id, {})
-        oldval = oldvalues['_isdel'] = row._isdel
-        row._isdel = 1
-
-        if self.do_journal:
-            self.db.addjournal(self.classname, nodeid, _RETIRE, {})
-        if self.key:
-            iv = self.getindexview(READWRITE)
-            ndx = iv.find(k=getattr(row, self.key))
-            # find is broken with multiple attribute lookups
-            # on ordered views
-            #ndx = iv.find(k=getattr(row, self.key),i=row.id)
-            if ndx > -1 and iv[ndx].i == row.id:
-                iv.delete(ndx)
-
-        self.db.dirty = 1
-        self.fireReactors('retire', nodeid, None)
-
-    def restore(self, nodeid):
-        '''Restore a retired node.
-
-        Make node available for all operations like it was before retirement.
-        '''
-        if self.db.journaltag is None:
-            raise hyperdb.DatabaseError, 'Database open read-only'
-
-        # check if key property was overrided
-        key = self.getkey()
-        keyvalue = self.get(nodeid, key)
-
-        try:
-            id = self.lookup(keyvalue)
-        except KeyError:
-            pass
-        else:
-            raise KeyError, "Key property (%s) of retired node clashes with \
-                existing one (%s)" % (key, keyvalue)
-        # Now we can safely restore node
-        self.fireAuditors('restore', nodeid, None)
-        view = self.getview(READWRITE)
-        ndx = view.find(id=int(nodeid))
-        if ndx < 0:
-            raise KeyError, "nodeid %s not found" % nodeid
-
-        row = view[ndx]
-        oldvalues = self.uncommitted.setdefault(row.id, {})
-        oldval = oldvalues['_isdel'] = row._isdel
-        row._isdel = 0
-
-        if self.do_journal:
-            self.db.addjournal(self.classname, nodeid, _RESTORE, {})
-        if self.key:
-            iv = self.getindexview(READWRITE)
-            ndx = iv.find(k=getattr(row, self.key),i=row.id)
-            if ndx > -1:
-                iv.delete(ndx)
-        self.db.dirty = 1
-        self.fireReactors('restore', nodeid, None)
-
-    def is_retired(self, nodeid):
-        '''Return true if the node is retired
-        '''
-        view = self.getview(READWRITE)
-        # node must exist & not be retired
-        id = int(nodeid)
-        ndx = view.find(id=id)
-        if ndx < 0:
-            raise IndexError, "%s has no node %s" % (self.classname, nodeid)
-        row = view[ndx]
-        return row._isdel
-
-    def history(self, nodeid):
-        '''Retrieve the journal of edits on a particular node.
-
-        'nodeid' must be the id of an existing node of this class or an
-        IndexError is raised.
-
-        The returned list contains tuples of the form
-
-            (nodeid, date, tag, action, params)
-
-        'date' is a Timestamp object specifying the time of the change and
-        'tag' is the journaltag specified when the database was opened.
-        '''
-        if not self.do_journal:
-            raise ValueError, 'Journalling is disabled for this class'
-        return self.db.getjournal(self.classname, nodeid)
-
-    def setkey(self, propname):
-        '''Select a String property of this class to be the key property.
-
-        'propname' must be the name of a String property of this class or
-        None, or a TypeError is raised.  The values of the key property on
-        all existing nodes must be unique or a ValueError is raised.
-        '''
-        if self.key:
-            if propname == self.key:
-                return
-            else:
-                # drop the old key table
-                tablename = "_%s.%s"%(self.classname, self.key)
-                self.db._db.getas(tablename)
-
-            #raise ValueError, "%s already indexed on %s"%(self.classname,
-            #    self.key)
-
-        prop = self.properties.get(propname, None)
-        if prop is None:
-            prop = self.privateprops.get(propname, None)
-        if prop is None:
-            raise KeyError, "no property %s" % propname
-        if not isinstance(prop, hyperdb.String):
-            raise TypeError, "%s is not a String" % propname
-
-        # the way he index on properties is by creating a
-        # table named _%(classname)s.%(key)s, if this table
-        # exists then everything is okay.  If this table
-        # doesn't exist, then generate a new table on the
-        # key value.
-
-        # first setkey for this run or key has been changed
-        self.key = propname
-        tablename = "_%s.%s"%(self.classname, self.key)
-
-        iv = self.db._db.view(tablename)
-        if self.db.fastopen and iv.structure():
-            return
-
-        # very first setkey ever or the key has changed
-        self.db.dirty = 1
-        iv = self.db._db.getas('_%s[k:S,i:I]' % tablename)
-        iv = iv.ordered(1)
-        for row in self.getview():
-            iv.append(k=getattr(row, propname), i=row.id)
-        self.db.commit()
-
-    def getkey(self):
-       '''Return the name of the key property for this class or None.'''
-       return self.key
-
-    def lookup(self, keyvalue):
-        '''Locate a particular node by its key property and return its id.
-
-        If this class has no key property, a TypeError is raised.  If the
-        keyvalue matches one of the values for the key property among
-        the nodes in this class, the matching node's id is returned;
-        otherwise a KeyError is raised.
-        '''
-        if not self.key:
-            raise TypeError, 'No key property set for class %s'%self.classname
-
-        if type(keyvalue) is not _STRINGTYPE:
-            raise TypeError, '%r is not a string'%keyvalue
-
-        # XXX FIX ME -> this is a bit convoluted
-        # First we search the index view to get the id
-        # which is a quicker look up.
-        # Then we lookup the row with id=id
-        # if the _isdel property of the row is 0, return the
-        # string version of the id. (Why string version???)
-        #
-        # Otherwise, just lookup the non-indexed key
-        # in the non-index table and check the _isdel property
-        iv = self.getindexview()
-        if iv:
-            # look up the index view for the id,
-            # then instead of looking up the keyvalue, lookup the
-            # quicker id
-            ndx = iv.find(k=keyvalue)
-            if ndx > -1:
-                view = self.getview()
-                ndx = view.find(id=iv[ndx].i)
-                if ndx > -1:
-                    row = view[ndx]
-                    if not row._isdel:
-                        return str(row.id)
-        else:
-            # perform the slower query
-            view = self.getview()
-            ndx = view.find({self.key:keyvalue})
-            if ndx > -1:
-                row = view[ndx]
-                if not row._isdel:
-                    return str(row.id)
-
-        raise KeyError, keyvalue
-
-    def destroy(self, id):
-        '''Destroy a node.
-
-        WARNING: this method should never be used except in extremely rare
-                 situations where there could never be links to the node being
-                 deleted
-
-        WARNING: use retire() instead
-
-        WARNING: the properties of this node will not be available ever again
-
-        WARNING: really, use retire() instead
-
-        Well, I think that's enough warnings. This method exists mostly to
-        support the session storage of the cgi interface.
-
-        The node is completely removed from the hyperdb, including all journal
-        entries. It will no longer be available, and will generally break code
-        if there are any references to the node.
-        '''
-        view = self.getview(READWRITE)
-        ndx = view.find(id=int(id))
-        if ndx > -1:
-            if self.key:
-                keyvalue = getattr(view[ndx], self.key)
-                iv = self.getindexview(READWRITE)
-                if iv:
-                    ivndx = iv.find(k=keyvalue)
-                    if ivndx > -1:
-                        iv.delete(ivndx)
-            view.delete(ndx)
-            self.db.destroyjournal(self.classname, id)
-            self.db.dirty = 1
-
-    def find(self, **propspec):
-        '''Get the ids of nodes in this class which link to the given nodes.
-
-        'propspec' consists of keyword args propname=nodeid or
-                   propname={nodeid:1, }
-        'propname' must be the name of a property in this class, or a
-                   KeyError is raised.  That property must be a Link or
-                   Multilink property, or a TypeError is raised.
-
-        Any node in this class whose 'propname' property links to any of
-        the nodeids will be returned. Examples::
-
-            db.issue.find(messages='1')
-            db.issue.find(messages={'1':1,'3':1}, files={'7':1})
-        '''
-        propspec = propspec.items()
-        for propname, nodeid in propspec:
-            # check the prop is OK
-            prop = self.ruprops[propname]
-            if (not isinstance(prop, hyperdb.Link) and
-                    not isinstance(prop, hyperdb.Multilink)):
-                raise TypeError, "'%s' not a Link/Multilink property"%propname
-
-        vws = []
-        for propname, ids in propspec:
-            if type(ids) is _STRINGTYPE:
-                ids = {int(ids):1}
-            elif ids is None:
-                ids = {0:1}
-            else:
-                d = {}
-                for id in ids.keys():
-                    if id is None:
-                        d[0] = 1
-                    else:
-                        d[int(id)] = 1
-                ids = d
-            prop = self.ruprops[propname]
-            view = self.getview()
-            if isinstance(prop, hyperdb.Multilink):
-                def ff(row, nm=propname, ids=ids):
-                    if not row._isdel:
-                        sv = getattr(row, nm)
-                        for sr in sv:
-                            if ids.has_key(sr.fid):
-                                return 1
-                    return 0
-            else:
-                def ff(row, nm=propname, ids=ids):
-                    return not row._isdel and ids.has_key(getattr(row, nm))
-            ndxview = view.filter(ff)
-            vws.append(ndxview.unique())
-
-        # handle the empty match case
-        if not vws:
-            return []
-
-        ndxview = vws[0]
-        for v in vws[1:]:
-            ndxview = ndxview.union(v)
-        view = self.getview().remapwith(ndxview)
-        rslt = []
-        for row in view:
-            rslt.append(str(row.id))
-        return rslt
-
-
-    def list(self):
-        ''' Return a list of the ids of the active nodes in this class.
-        '''
-        l = []
-        for row in self.getview().select(_isdel=0):
-            l.append(str(row.id))
-        return l
-
-    def getnodeids(self, retired=None):
-        ''' Retrieve all the ids of the nodes for a particular Class.
-
-            Set retired=None to get all nodes. Otherwise it'll get all the
-            retired or non-retired nodes, depending on the flag.
-        '''
-        l = []
-        if retired is False or retired is True:
-            result = self.getview().select(_isdel=retired)
-        else:
-            result = self.getview()
-        for row in result:
-            l.append(str(row.id))
-        return l
-
-    def count(self):
-        return len(self.getview())
-
-    def getprops(self, protected=1):
-        # protected is not in ping's spec
-        allprops = self.ruprops.copy()
-        if protected and self.privateprops is not None:
-            allprops.update(self.privateprops)
-        return allprops
-
-    def addprop(self, **properties):
-        for key in properties.keys():
-            if self.ruprops.has_key(key):
-                raise ValueError, "%s is already a property of %s"%(key,
-                    self.classname)
-        self.ruprops.update(properties)
-        # Class structure has changed
-        self.db.fastopen = 0
-        view = self.__getview()
-        self.db.commit()
-    # ---- end of ping's spec
-
-    def _filter(self, search_matches, filterspec, proptree):
-        '''Return a list of the ids of the active nodes in this class that
-        match the 'filter' spec, sorted by the group spec and then the
-        sort spec
-
-        "filterspec" is {propname: value(s)}
-
-        "sort" and "group" are (dir, prop) where dir is '+', '-' or None
-        and prop is a prop name or None
-
-        "search_matches" is {nodeid: marker} or None
-
-        The filter must match all properties specificed - but if the
-        property value to match is a list, any one of the values in the
-        list may match for that property to match.
-        '''
-        if __debug__:
-            start_t = time.time()
-
-        where = {'_isdel':0}
-        wherehigh = {}
-        mlcriteria = {}
-        regexes = []
-        orcriteria = {}
-        for propname, value in filterspec.items():
-            prop = self.ruprops.get(propname, None)
-            if prop is None:
-                prop = self.privateprops[propname]
-            if isinstance(prop, hyperdb.Multilink):
-                if value in ('-1', ['-1']):
-                    value = []
-                elif type(value) is not _LISTTYPE:
-                    value = [value]
-                # transform keys to ids
-                u = []
-                for item in value:
-                    try:
-                        item = int(item)
-                    except (TypeError, ValueError):
-                        item = int(self.db.getclass(prop.classname).lookup(item))
-                    if item == -1:
-                        item = 0
-                    u.append(item)
-                mlcriteria[propname] = u
-            elif isinstance(prop, hyperdb.Link):
-                if type(value) is not _LISTTYPE:
-                    value = [value]
-                # transform keys to ids
-                u = []
-                for item in value:
-                    if item is None:
-                        item = -1
-                    else:
-                        try:
-                            item = int(item)
-                        except (TypeError, ValueError):
-                            linkcl = self.db.getclass(prop.classname)
-                            item = int(linkcl.lookup(item))
-                    if item == -1:
-                        item = 0
-                    u.append(item)
-                if len(u) == 1:
-                    where[propname] = u[0]
-                else:
-                    orcriteria[propname] = u
-            elif isinstance(prop, hyperdb.String):
-                if type(value) is not type([]):
-                    value = [value]
-                for v in value:
-                    # simple glob searching
-                    v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', v)
-                    v = v.replace('?', '.')
-                    v = v.replace('*', '.*?')
-                    regexes.append((propname, re.compile(v, re.I)))
-            elif propname == 'id':
-                where[propname] = int(value)
-            elif isinstance(prop, hyperdb.Boolean):
-                if type(value) is _STRINGTYPE:
-                    bv = value.lower() in ('yes', 'true', 'on', '1')
-                else:
-                    bv = value
-                where[propname] = bv
-            elif isinstance(prop, hyperdb.Date):
-                try:
-                    # Try to filter on range of dates
-                    date_rng = prop.range_from_raw (value, self.db)
-                    if date_rng.from_value:
-                        t = date_rng.from_value.get_tuple()
-                        where[propname] = int(calendar.timegm(t))
-                    else:
-                        # use minimum possible value to exclude items without
-                        # 'prop' property
-                        where[propname] = 0
-                    if date_rng.to_value:
-                        t = date_rng.to_value.get_tuple()
-                        wherehigh[propname] = int(calendar.timegm(t))
-                    else:
-                        wherehigh[propname] = None
-                except ValueError:
-                    # If range creation fails - ignore that search parameter
-                    pass
-            elif isinstance(prop, hyperdb.Interval):
-                try:
-                    # Try to filter on range of intervals
-                    date_rng = Range(value, date.Interval)
-                    if date_rng.from_value:
-                        #t = date_rng.from_value.get_tuple()
-                        where[propname] = date_rng.from_value.serialise()
-                    else:
-                        # use minimum possible value to exclude items without
-                        # 'prop' property
-                        where[propname] = '-99999999999999'
-                    if date_rng.to_value:
-                        #t = date_rng.to_value.get_tuple()
-                        wherehigh[propname] = date_rng.to_value.serialise()
-                    else:
-                        wherehigh[propname] = None
-                except ValueError:
-                    # If range creation fails - ignore that search parameter
-                    pass
-            elif isinstance(prop, hyperdb.Number):
-                if type(value) is _LISTTYPE:
-                    orcriteria[propname] = [float(v) for v in value]
-                else:
-                    where[propname] = float(value)
-            else:
-                where[propname] = str(value)
-        v = self.getview()
-        if where:
-            where_higherbound = where.copy()
-            where_higherbound.update(wherehigh)
-            v = v.select(where, where_higherbound)
-
-        if mlcriteria:
-            # multilink - if any of the nodeids required by the
-            # filterspec aren't in this node's property, then skip it
-            def ff(row, ml=mlcriteria):
-                for propname, values in ml.items():
-                    sv = getattr(row, propname)
-                    if not values and not sv:
-                        return 1
-                    for id in values:
-                        if sv.find(fid=id) != -1:
-                            return 1
-                return 0
-            iv = v.filter(ff)
-            v = v.remapwith(iv)
-
-        if orcriteria:
-            def ff(row, crit=orcriteria):
-                for propname, allowed in crit.items():
-                    val = getattr(row, propname)
-                    if val not in allowed:
-                        return 0
-                return 1
-
-            iv = v.filter(ff)
-            v = v.remapwith(iv)
-
-        if regexes:
-            def ff(row, r=regexes):
-                for propname, regex in r:
-                    val = str(getattr(row, propname))
-                    if not regex.search(val):
-                        return 0
-                return 1
-
-            iv = v.filter(ff)
-            v = v.remapwith(iv)
-
-        # Handle all the sorting we can inside Metakit. If we encounter
-        # transitive attributes or a Multilink on the way, we sort by
-        # what we have so far and defer the rest to the outer sorting
-        # routine. We mark the attributes for which sorting has been
-        # done with sort_done. Of course the whole thing works only if
-        # we do it backwards.
-        sortspec = []
-        rev = []
-        sa = []
-        if proptree:
-            sa = reversed(proptree.sortattr)
-        for pt in sa:
-            if pt.parent != proptree:
-                break;
-            propname = pt.name
-            dir = pt.sort_direction
-            assert (dir and propname)
-            isreversed = 0
-            if dir == '-':
-                isreversed = 1
-            try:
-                prop = getattr(v, propname)
-            except AttributeError:
-                logging.getLogger("hyperdb").error(
-                    "MK has no property %s" % propname)
-                continue
-            propclass = self.ruprops.get(propname, None)
-            if propclass is None:
-                propclass = self.privateprops.get(propname, None)
-                if propclass is None:
-                    logging.getLogger("hyperdb").error(
-                        "Schema has no property %s" % propname)
-                    continue
-            # Dead code: We dont't find Links here (in sortattr we would
-            # see the order property of the link, but this is not in the
-            # first level of the tree). The code is left in because one
-            # day we might want to properly implement this.  The code is
-            # broken because natural-joining to the Link-class can
-            # produce name-clashes wich result in broken sorting.
-            if isinstance(propclass, hyperdb.Link):
-                linkclass = self.db.getclass(propclass.classname)
-                lv = linkclass.getview()
-                lv = lv.rename('id', propname)
-                v = v.join(lv, prop, 1)
-                prop = getattr(v, linkclass.orderprop())
-            if isreversed:
-                rev.append(prop)
-            sortspec.append(prop)
-            pt.sort_done = True
-        sortspec.reverse()
-        rev.reverse()
-        v = v.sortrev(sortspec, rev)[:] #XXX Metakit bug
-
-        rslt = []
-        for row in v:
-            id = str(row.id)
-            if search_matches is not None:
-                if search_matches.has_key(id):
-                    rslt.append(id)
-            else:
-                rslt.append(id)
-
-        if __debug__:
-            self.db.stats['filtering'] += (time.time() - start_t)
-
-        return rslt
-
-    def hasnode(self, nodeid):
-        '''Determine if the given nodeid actually exists
-        '''
-        return int(nodeid) < self.maxid
-
-    def stringFind(self, **requirements):
-        '''Locate a particular node by matching a set of its String
-        properties in a caseless search.
-
-        If the property is not a String property, a TypeError is raised.
-
-        The return is a list of the id of all nodes that match.
-        '''
-        for propname in requirements.keys():
-            prop = self.properties[propname]
-            if isinstance(not prop, hyperdb.String):
-                raise TypeError, "'%s' not a String property"%propname
-            requirements[propname] = requirements[propname].lower()
-        requirements['_isdel'] = 0
-
-        l = []
-        for row in self.getview().select(requirements):
-            l.append(str(row.id))
-        return l
-
-    def addjournal(self, nodeid, action, params):
-        '''Add a journal to the given nodeid,
-        'action' may be:
-
-            'create' or 'set' -- 'params' is a dictionary of property values
-            'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
-            'retire' -- 'params' is None
-        '''
-        self.db.addjournal(self.classname, nodeid, action, params)
-
-    def index(self, nodeid):
-        ''' Add (or refresh) the node to search indexes '''
-        # find all the String properties that have indexme
-        for prop, propclass in self.getprops().items():
-            if isinstance(propclass, hyperdb.String) and propclass.indexme:
-                # index them under (classname, nodeid, property)
-                self.db.indexer.add_text((self.classname, nodeid, prop),
-                                str(self.get(nodeid, prop)))
-
-    # --- used by Database
-    def _commit(self):
-        ''' called post commit of the DB.
-            interested subclasses may override '''
-        self.uncommitted = {}
-        for action in self.comactions:
-            action()
-        self.comactions = []
-        self.rbactions = []
-        self.idcache = {}
-    def _rollback(self):
-        ''' called pre rollback of the DB.
-            interested subclasses may override '''
-        self.comactions = []
-        for action in self.rbactions:
-            action()
-        self.rbactions = []
-        self.uncommitted = {}
-        self.idcache = {}
-    def _clear(self):
-        view = self.getview(READWRITE)
-        if len(view):
-            view[:] = []
-            self.db.dirty = 1
-        iv = self.getindexview(READWRITE)
-        if iv:
-            iv[:] = []
-    def commitaction(self, action):
-        ''' call this to register a callback called on commit
-            callback is removed on end of transaction '''
-        self.comactions.append(action)
-    def rollbackaction(self, action):
-        ''' call this to register a callback called on rollback
-            callback is removed on end of transaction '''
-        self.rbactions.append(action)
-    # --- internal
-    def __getview(self):
-        ''' Find the interface for a specific Class in the hyperdb.
-
-            This method checks to see whether the schema has changed and
-            re-works the underlying metakit structure if it has.
-        '''
-        db = self.db._db
-        view = db.view(self.classname)
-        mkprops = view.structure()
-
-        # if we have structure in the database, and the structure hasn't
-        # changed
-        # note on view.ordered ->
-        # return a metakit view ordered on the id column
-        # id is always the first column.  This speeds up
-        # look-ups on the id column.
-
-        if mkprops and self.db.fastopen:
-            return view.ordered(1)
-
-        # is the definition the same?
-        for nm, rutyp in self.ruprops.items():
-            for mkprop in mkprops:
-                if mkprop.name == nm:
-                    break
-            else:
-                mkprop = None
-            if mkprop is None:
-                break
-            if _typmap[rutyp.__class__] != mkprop.type:
-                break
-        else:
-            # make sure we have the 'actor' property too
-            for mkprop in mkprops:
-                if mkprop.name == 'actor':
-                    return view.ordered(1)
-
-        # The schema has changed.  We need to create or restructure the mk view
-        # id comes first, so we can use view.ordered(1) so that
-        # MK will order it for us to allow binary-search quick lookups on
-        # the id column
-        self.db.dirty = 1
-        s = ["%s[id:I" % self.classname]
-
-        # these columns will always be added, we can't trample them :)
-        _columns = {"id":"I", "_isdel":"I", "activity":"I", "actor": "I",
-            "creation":"I", "creator":"I"}
-
-        for nm, rutyp in self.ruprops.items():
-            mktyp = _typmap[rutyp.__class__].upper()
-            if nm in _columns and _columns[nm] != mktyp:
-                # oops, two columns with the same name and different properties
-               raise MKBackendError("column %s for table %sis defined with multiple types"%(nm, self.classname))
-            _columns[nm] = mktyp
-            s.append('%s:%s' % (nm, mktyp))
-            if mktyp == 'V':
-                s[-1] += ('[fid:I]')
-
-        # XXX FIX ME -> in some tests, creation:I becomes creation:S is this
-        # okay?  Does this need to be supported?
-        s.append('_isdel:I,activity:I,actor:I,creation:I,creator:I]')
-        view = self.db._db.getas(','.join(s))
-        self.db.commit()
-        return view.ordered(1)
-    def getview(self, RW=0):
-        # XXX FIX ME -> The RW flag doesn't do anything.
-        return self.db._db.view(self.classname).ordered(1)
-    def getindexview(self, RW=0):
-        # XXX FIX ME -> The RW flag doesn't do anything.
-        tablename = "_%s.%s"%(self.classname, self.key)
-        return self.db._db.view("_%s" % tablename).ordered(1)
-
-    #
-    # import / export
-    #
-    def export_list(self, propnames, nodeid):
-        ''' Export a node - generate a list of CSV-able data in the order
-            specified by propnames for the given node.
-        '''
-        properties = self.getprops()
-        l = []
-        for prop in propnames:
-            proptype = properties[prop]
-            value = self.get(nodeid, prop)
-            # "marshal" data where needed
-            if value is None:
-                pass
-            elif isinstance(proptype, hyperdb.Date):
-                value = value.get_tuple()
-            elif isinstance(proptype, hyperdb.Interval):
-                value = value.get_tuple()
-            elif isinstance(proptype, hyperdb.Password):
-                value = str(value)
-            l.append(repr(value))
-
-        # append retired flag
-        l.append(repr(self.is_retired(nodeid)))
-
-        return l
-
-    def import_list(self, propnames, proplist):
-        ''' Import a node - all information including "id" is present and
-            should not be sanity checked. Triggers are not triggered. The
-            journal should be initialised using the "creator" and "creation"
-            information.
-
-            Return the nodeid of the node imported.
-        '''
-        if self.db.journaltag is None:
-            raise hyperdb.DatabaseError, 'Database open read-only'
-        properties = self.getprops()
-
-        d = {}
-        view = self.getview(READWRITE)
-        for i in range(len(propnames)):
-            value = eval(proplist[i])
-            if not value:
-                continue
-
-            propname = propnames[i]
-            if propname == 'id':
-                newid = value = int(value)
-            elif propname == 'is retired':
-                # is the item retired?
-                if int(value):
-                    d['_isdel'] = 1
-                continue
-            elif value is None:
-                d[propname] = None
-                continue
-
-            prop = properties[propname]
-            if isinstance(prop, hyperdb.Date):
-                value = int(calendar.timegm(value))
-            elif isinstance(prop, hyperdb.Interval):
-                value = date.Interval(value).serialise()
-            elif isinstance(prop, hyperdb.Number):
-                value = float(value)
-            elif isinstance(prop, hyperdb.Boolean):
-                value = int(value)
-            elif isinstance(prop, hyperdb.Link) and value:
-                value = int(value)
-            elif isinstance(prop, hyperdb.Multilink):
-                # we handle multilinks separately
-                continue
-            d[propname] = value
-
-        # possibly make a new node
-        if not d.has_key('id'):
-            d['id'] = newid = self.maxid
-            self.maxid += 1
-
-        # save off the node
-        view.append(d)
-
-        # fix up multilinks
-        ndx = view.find(id=newid)
-        row = view[ndx]
-        for i in range(len(propnames)):
-            value = eval(proplist[i])
-            propname = propnames[i]
-            if propname == 'is retired':
-                continue
-            prop = properties[propname]
-            if not isinstance(prop, hyperdb.Multilink):
-                continue
-            sv = getattr(row, propname)
-            for entry in value:
-                sv.append((int(entry),))
-
-        self.db.dirty = 1
-        return newid
-
-    def export_journals(self):
-        '''Export a class's journal - generate a list of lists of
-        CSV-able data:
-
-            nodeid, date, user, action, params
-
-        No heading here - the columns are fixed.
-        '''
-        from roundup.hyperdb import Interval, Date, Password
-        properties = self.getprops()
-        r = []
-        for nodeid in self.getnodeids():
-            for nodeid, date, user, action, params in self.history(nodeid):
-                date = date.get_tuple()
-                if action == 'set':
-                    export_data = {}
-                    for propname, value in params.items():
-                        if not properties.has_key(propname):
-                            # property no longer in the schema
-                            continue
-
-                        prop = properties[propname]
-                        # make sure the params are eval()'able
-                        if value is None:
-                            pass
-                        elif isinstance(prop, Date):
-                            value = value.get_tuple()
-                        elif isinstance(prop, Interval):
-                            value = value.get_tuple()
-                        elif isinstance(prop, Password):
-                            value = str(value)
-                        export_data[propname] = value
-                    params = export_data
-                l = [nodeid, date, user, action, params]
-                r.append(map(repr, l))
-        return r
-
-    def import_journals(self, entries):
-        '''Import a class's journal.
-
-        Uses setjournal() to set the journal for each item.'''
-        properties = self.getprops()
-        d = {}
-        for l in entries:
-            l = map(eval, l)
-            nodeid, jdate, user, action, params = l
-            jdate = int(calendar.timegm(date.Date(jdate).get_tuple()))
-            r = d.setdefault(nodeid, [])
-            if action == 'set':
-                for propname, value in params.items():
-                    prop = properties[propname]
-                    if value is None:
-                        pass
-                    elif isinstance(prop, hyperdb.Date):
-                        value = date.Date(value)
-                    elif isinstance(prop, hyperdb.Interval):
-                        value = date.Interval(value)
-                    elif isinstance(prop, hyperdb.Password):
-                        pwd = password.Password()
-                        pwd.unpack(value)
-                        value = pwd
-                    params[propname] = value
-            action = _names_to_actionnames[action]
-            r.append((nodeid, jdate, user, action, params))
-
-        for nodeid, l in d.items():
-            self.db.setjournal(self.classname, nodeid, l)
-
-def _fetchML(sv):
-    l = []
-    for row in sv:
-        if row.fid:
-            l.append(str(row.fid))
-    return l
-
-def _fetchPW(s):
-    ''' Convert to a password.Password unless the password is '' which is
-        our sentinel for "unset".
-    '''
-    if s == '':
-        return None
-    p = password.Password()
-    p.unpack(s)
-    return p
-
-def _fetchLink(n):
-    ''' Return None if the link is 0 - otherwise strify it.
-    '''
-    return n and str(n) or None
-
-def _fetchDate(n):
-    ''' Convert the timestamp to a date.Date instance - unless it's 0 which
-        is our sentinel for "unset".
-    '''
-    if n == 0:
-        return None
-    return date.Date(time.gmtime(n))
-
-def _fetchInterval(n):
-    ''' Convert to a date.Interval unless the interval is '' which is our
-        sentinel for "unset".
-    '''
-    if n == '':
-        return None
-    return date.Interval(n)
-
-# Converters for boolean and numbers to properly
-# return None values.
-# These are in conjunction with the setters above
-#  look for hyperdb.Boolean and hyperdb.Number
-if BACKWARDS_COMPATIBLE:
-    def getBoolean(bool): return bool
-    def getNumber(number): return number
-else:
-    def getBoolean(bool):
-        if not bool: res = None
-        else: res = bool - 1
-        return res
-
-    def getNumber(number):
-        if number == 0: res = None
-        elif number < 0: res = number
-        else: res = number - 1
-        return res
-
-_converters = {
-    hyperdb.Date   : _fetchDate,
-    hyperdb.Link   : _fetchLink,
-    hyperdb.Multilink : _fetchML,
-    hyperdb.Interval  : _fetchInterval,
-    hyperdb.Password  : _fetchPW,
-    hyperdb.Boolean   : getBoolean,
-    hyperdb.Number    : getNumber,
-    hyperdb.String    : lambda s: s and str(s) or None,
-}
-
-class FileName(hyperdb.String):
-    isfilename = 1
-
-_typmap = {
-    FileName : 'S',
-    hyperdb.String : 'S',
-    hyperdb.Date   : 'I',
-    hyperdb.Link   : 'I',
-    hyperdb.Multilink : 'V',
-    hyperdb.Interval  : 'S',
-    hyperdb.Password  : 'S',
-    hyperdb.Boolean   : 'I',
-    hyperdb.Number    : 'D',
-}
-class FileClass(hyperdb.FileClass, Class):
-    ''' like Class but with a content property
-    '''
-    def __init__(self, db, classname, **properties):
-        '''The newly-created class automatically includes the "content"
-        and "type" properties.
-        '''
-        if not properties.has_key('content'):
-            properties['content'] = hyperdb.String(indexme='yes')
-        if not properties.has_key('type'):
-            properties['type'] = hyperdb.String()
-        Class.__init__(self, db, classname, **properties)
-
-    def gen_filename(self, nodeid):
-        nm = '%s%s' % (self.classname, nodeid)
-        sd = str(int(int(nodeid) / 1000))
-        d = os.path.join(self.db.config.DATABASE, 'files', self.classname, sd)
-        if not os.path.exists(d):
-            os.makedirs(d)
-        return os.path.join(d, nm)
-
-    def export_files(self, dirname, nodeid):
-        ''' Export the "content" property as a file, not csv column
-        '''
-        source = self.gen_filename(nodeid)
-        x, filename = os.path.split(source)
-        x, subdir = os.path.split(x)
-        dest = os.path.join(dirname, self.classname+'-files', subdir, filename)
-        if not os.path.exists(os.path.dirname(dest)):
-            os.makedirs(os.path.dirname(dest))
-        shutil.copyfile(source, dest)
-
-    def import_files(self, dirname, nodeid):
-        ''' Import the "content" property as a file
-        '''
-        dest = self.gen_filename(nodeid)
-        x, filename = os.path.split(dest)
-        x, subdir = os.path.split(x)
-        source = os.path.join(dirname, self.classname+'-files', subdir,
-            filename)
-        if not os.path.exists(os.path.dirname(dest)):
-            os.makedirs(os.path.dirname(dest))
-        shutil.copyfile(source, dest)
-
-        if self.properties['content'].indexme:
-            return
-
-        mime_type = None
-        if self.getprops().has_key('type'):
-            mime_type = self.get(nodeid, 'type')
-        if not mime_type:
-            mime_type = self.default_mime_type
-        self.db.indexer.add_text((self.classname, nodeid, 'content'),
-            self.get(nodeid, 'content'), mime_type)
-
-    def get(self, nodeid, propname, default=_marker, cache=1):
-        if propname == 'content':
-            poss_msg = 'Possibly an access right configuration problem.'
-            fnm = self.gen_filename(nodeid)
-            if not os.path.exists(fnm):
-                fnm = fnm + '.tmp'
-            try:
-                f = open(fnm, 'rb')
-            except IOError, (strerror):
-                # XXX by catching this we donot see an error in the log.
-                return 'ERROR reading file: %s%s\n%s\n%s'%(
-                        self.classname, nodeid, poss_msg, strerror)
-            x = f.read()
-            f.close()
-        else:
-            x = Class.get(self, nodeid, propname, default)
-        return x
-
-    def create(self, **propvalues):
-        if not propvalues:
-            raise ValueError, "Need something to create!"
-        self.fireAuditors('create', None, propvalues)
-
-        content = propvalues['content']
-        del propvalues['content']
-
-        newid = Class.create_inner(self, **propvalues)
-        if not content:
-            return newid
-
-        # figure a filename
-        nm = self.gen_filename(newid)
-
-        # make sure we don't register the rename action more than once
-        if not os.path.exists(nm + '.tmp'):
-            # register commit and rollback actions
-            def commit(fnm=nm):
-                os.rename(fnm + '.tmp', fnm)
-            self.commitaction(commit)
-            def undo(fnm=nm):
-                os.remove(fnm + '.tmp')
-            self.rollbackaction(undo)
-
-        # save the tempfile
-        f = open(nm + '.tmp', 'wb')
-        f.write(content)
-        f.close()
-
-        if not self.properties['content'].indexme:
-            return newid
-
-        mimetype = propvalues.get('type', self.default_mime_type)
-        self.db.indexer.add_text((self.classname, newid, 'content'), content,
-            mimetype)
-        return newid
-
-    def set(self, itemid, **propvalues):
-        if not propvalues:
-            return
-        self.fireAuditors('set', None, propvalues)
-
-        content = propvalues.get('content', None)
-        if content is not None:
-            del propvalues['content']
-
-        propvalues, oldnode = Class.set_inner(self, itemid, **propvalues)
-
-        # figure a filename
-        if content is not None:
-            nm = self.gen_filename(itemid)
-
-            # make sure we don't register the rename action more than once
-            if not os.path.exists(nm + '.tmp'):
-                # register commit and rollback actions
-                def commit(fnm=nm):
-                    if os.path.exists(fnm):
-                        os.remove(fnm)
-                    os.rename(fnm + '.tmp', fnm)
-                self.commitaction(commit)
-                def undo(fnm=nm):
-                    os.remove(fnm + '.tmp')
-                self.rollbackaction(undo)
-
-            f = open(nm + '.tmp', 'wb')
-            f.write(content)
-            f.close()
-
-            if self.properties['content'].indexme:
-                mimetype = propvalues.get('type', self.default_mime_type)
-                self.db.indexer.add_text((self.classname, itemid, 'content'),
-                    content, mimetype)
-
-        self.fireReactors('set', oldnode, propvalues)
-
-    def index(self, nodeid):
-        ''' Add (or refresh) the node to search indexes.
-
-        Use the content-type property for the content property.
-        '''
-        # find all the String properties that have indexme
-        for prop, propclass in self.getprops().items():
-            if prop == 'content' and propclass.indexme:
-                mime_type = self.get(nodeid, 'type', self.default_mime_type)
-                self.db.indexer.add_text((self.classname, nodeid, 'content'),
-                    str(self.get(nodeid, 'content')), mime_type)
-            elif isinstance(propclass, hyperdb.String) and propclass.indexme:
-                # index them under (classname, nodeid, property)
-                try:
-                    value = str(self.get(nodeid, prop))
-                except IndexError:
-                    # node has been destroyed
-                    continue
-                self.db.indexer.add_text((self.classname, nodeid, prop), value)
-
-class IssueClass(Class, roundupdb.IssueClass):
-    ''' The newly-created class automatically includes the "messages",
-        "files", "nosy", and "superseder" properties.  If the 'properties'
-        dictionary attempts to specify any of these properties or a
-        "creation" or "activity" property, a ValueError is raised.
-    '''
-    def __init__(self, db, classname, **properties):
-        if not properties.has_key('title'):
-            properties['title'] = hyperdb.String(indexme='yes')
-        if not properties.has_key('messages'):
-            properties['messages'] = hyperdb.Multilink("msg")
-        if not properties.has_key('files'):
-            properties['files'] = hyperdb.Multilink("file")
-        if not properties.has_key('nosy'):
-            # note: journalling is turned off as it really just wastes
-            # space. this behaviour may be overridden in an instance
-            properties['nosy'] = hyperdb.Multilink("user", do_journal="no")
-        if not properties.has_key('superseder'):
-            properties['superseder'] = hyperdb.Multilink(classname)
-        Class.__init__(self, db, classname, **properties)
-
-CURVERSION = 2
-
-class MetakitIndexer(CommonIndexer):
-    def __init__(self, db):
-        CommonIndexer.__init__(self, db)
-        self.path = os.path.join(db.config.DATABASE, 'index.mk4')
-        self.db = metakit.storage(self.path, 1)
-        self.datadb = db._db
-        self.reindex = 0
-        v = self.db.view('version')
-        if not v.structure():
-            v = self.db.getas('version[vers:I]')
-            self.db.commit()
-            v.append(vers=CURVERSION)
-            self.reindex = 1
-        elif v[0].vers != CURVERSION:
-            v[0].vers = CURVERSION
-            self.reindex = 1
-        if self.reindex:
-            self.db.getas('ids[tblid:I,nodeid:I,propid:I,ignore:I]')
-            self.db.getas('index[word:S,hits[pos:I]]')
-            self.db.commit()
-            self.reindex = 1
-        self.changed = 0
-        self.propcache = {}
-
-    def close(self):
-        '''close the indexing database'''
-        del self.db
-        self.db = None
-
-    def force_reindex(self):
-        '''Force a reindexing of the database.  This essentially
-        empties the tables ids and index and sets a flag so
-        that the databases are reindexed'''
-        v = self.db.view('ids')
-        v[:] = []
-        v = self.db.view('index')
-        v[:] = []
-        self.db.commit()
-        self.reindex = 1
-
-    def should_reindex(self):
-        '''returns True if the indexes need to be rebuilt'''
-        return self.reindex
-
-    def _getprops(self, classname):
-        props = self.propcache.get(classname, None)
-        if props is None:
-            props = self.datadb.view(classname).structure()
-            props = [prop.name for prop in props]
-            self.propcache[classname] = props
-        return props
-
-    def _getpropid(self, classname, propname):
-        return self._getprops(classname).index(propname)
-
-    def _getpropname(self, classname, propid):
-        return self._getprops(classname)[propid]
-
-    def add_text(self, identifier, text, mime_type='text/plain'):
-        if mime_type != 'text/plain':
-            return
-        classname, nodeid, property = identifier
-        tbls = self.datadb.view('tables')
-        tblid = tbls.find(name=classname)
-        if tblid < 0:
-            raise KeyError, "unknown class %r"%classname
-        nodeid = int(nodeid)
-        propid = self._getpropid(classname, property)
-        ids = self.db.view('ids')
-        oldpos = ids.find(tblid=tblid,nodeid=nodeid,propid=propid,ignore=0)
-        if oldpos > -1:
-            ids[oldpos].ignore = 1
-            self.changed = 1
-        pos = ids.append(tblid=tblid,nodeid=nodeid,propid=propid)
-
-        wordlist = re.findall(r'\b\w{2,25}\b', text.upper())
-        words = {}
-        for word in wordlist:
-            if not self.is_stopword(word):
-                words[word] = 1
-        words = words.keys()
-
-        index = self.db.view('index').ordered(1)
-        for word in words:
-            ndx = index.find(word=word)
-            if ndx < 0:
-                index.append(word=word)
-                ndx = index.find(word=word)
-            index[ndx].hits.append(pos=pos)
-            self.changed = 1
-
-    def find(self, wordlist):
-        '''look up all the words in the wordlist.
-        If none are found return an empty dictionary
-        * more rules here
-        '''
-        hits = None
-        index = self.db.view('index').ordered(1)
-        for word in wordlist:
-            word = word.upper()
-            if not 2 < len(word) < 26:
-                continue
-            ndx = index.find(word=word)
-            if ndx < 0:
-                return {}
-            if hits is None:
-                hits = index[ndx].hits
-            else:
-                hits = hits.intersect(index[ndx].hits)
-            if len(hits) == 0:
-                return {}
-        if hits is None:
-            return []
-        rslt = []
-        ids = self.db.view('ids').remapwith(hits)
-        tbls = self.datadb.view('tables')
-        for i in range(len(ids)):
-            hit = ids[i]
-            if not hit.ignore:
-                classname = tbls[hit.tblid].name
-                nodeid = str(hit.nodeid)
-                property = self._getpropname(classname, hit.propid)
-                rslt.append((classname, nodeid, property))
-        return rslt
-
-    def save_index(self):
-        if self.changed:
-            self.db.commit()
-        self.changed = 0
-
-    def rollback(self):
-        if self.changed:
-            self.db.rollback()
-            self.db = metakit.storage(self.path, 1)
-        self.changed = 0
-
-try:
-    from indexer_xapian import Indexer
-except ImportError:
-    Indexer = MetakitIndexer
-
-# vim: set et sts=4 sw=4 :

Modified: tracker/roundup-src/roundup/backends/back_mysql.py
==============================================================================
--- tracker/roundup-src/roundup/backends/back_mysql.py	(original)
+++ tracker/roundup-src/roundup/backends/back_mysql.py	Sun Mar  9 09:26:16 2008
@@ -1,4 +1,4 @@
-#$Id: back_mysql.py,v 1.71 2006/08/29 04:20:50 richard Exp $
+#$Id: back_mysql.py,v 1.74 2007/10/26 01:34:43 richard Exp $
 #
 # Copyright (c) 2003 Martynas Sklyzmantas, Andrey Lebedev <andrey at micro.lt>
 #
@@ -13,7 +13,7 @@
 How to implement AUTO_INCREMENT:
 
 mysql> create table foo (num integer auto_increment primary key, name
-varchar(255)) AUTO_INCREMENT=1 type=InnoDB;
+varchar(255)) AUTO_INCREMENT=1 ENGINE=InnoDB;
 
 ql> insert into foo (name) values ('foo5');
 Query OK, 1 row affected (0.00 sec)
@@ -166,10 +166,10 @@
             if message[0] != ER.NO_SUCH_TABLE:
                 raise DatabaseError, message
             self.init_dbschema()
-            self.sql("CREATE TABLE `schema` (`schema` TEXT) TYPE=%s"%
+            self.sql("CREATE TABLE `schema` (`schema` TEXT) ENGINE=%s"%
                 self.mysql_backend)
             self.sql('''CREATE TABLE ids (name VARCHAR(255),
-                num INTEGER) TYPE=%s'''%self.mysql_backend)
+                num INTEGER) ENGINE=%s'''%self.mysql_backend)
             self.sql('create index ids_name_idx on ids(name)')
             self.create_version_2_tables()
 
@@ -194,23 +194,26 @@
         # OTK store
         self.sql('''CREATE TABLE otks (otk_key VARCHAR(255),
             otk_value TEXT, otk_time FLOAT(20))
-            TYPE=%s'''%self.mysql_backend)
+            ENGINE=%s'''%self.mysql_backend)
         self.sql('CREATE INDEX otks_key_idx ON otks(otk_key)')
 
         # Sessions store
         self.sql('''CREATE TABLE sessions (session_key VARCHAR(255),
             session_time FLOAT(20), session_value TEXT)
-            TYPE=%s'''%self.mysql_backend)
+            ENGINE=%s'''%self.mysql_backend)
         self.sql('''CREATE INDEX sessions_key_idx ON
             sessions(session_key)''')
 
         # full-text indexing store
         self.sql('''CREATE TABLE __textids (_class VARCHAR(255),
             _itemid VARCHAR(255), _prop VARCHAR(255), _textid INT)
-            TYPE=%s'''%self.mysql_backend)
+            ENGINE=%s'''%self.mysql_backend)
         self.sql('''CREATE TABLE __words (_word VARCHAR(30),
-            _textid INT) TYPE=%s'''%self.mysql_backend)
+            _textid INT) ENGINE=%s'''%self.mysql_backend)
         self.sql('CREATE INDEX words_word_ids ON __words(_word)')
+        self.sql('CREATE INDEX words_by_id ON __words (_textid)')
+        self.sql('CREATE UNIQUE INDEX __textids_by_props ON '
+                 '__textids (_class, _itemid, _prop)')
         sql = 'insert into ids (name, num) values (%s,%s)'%(self.arg, self.arg)
         self.sql(sql, ('__textids', 1))
 
@@ -389,7 +392,7 @@
 
         # create the base table
         scols = ','.join(['%s %s'%x for x in cols])
-        sql = 'create table _%s (%s) type=%s'%(spec.classname, scols,
+        sql = 'create table _%s (%s) ENGINE=%s'%(spec.classname, scols,
             self.mysql_backend)
         self.sql(sql)
 
@@ -450,7 +453,7 @@
             for x in 'nodeid date tag action params'.split()])
         sql = '''create table %s__journal (
             nodeid integer, date datetime, tag varchar(255),
-            action varchar(255), params text) type=%s'''%(
+            action varchar(255), params text) ENGINE=%s'''%(
             spec.classname, self.mysql_backend)
         self.sql(sql)
         self.create_journal_table_indexes(spec)
@@ -464,7 +467,7 @@
 
     def create_multilink_table(self, spec, ml):
         sql = '''CREATE TABLE `%s_%s` (linkid VARCHAR(255),
-            nodeid VARCHAR(255)) TYPE=%s'''%(spec.classname, ml,
+            nodeid VARCHAR(255)) ENGINE=%s'''%(spec.classname, ml,
                 self.mysql_backend)
         self.sql(sql)
         self.create_multilink_table_indexes(spec, ml)

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	Sun Mar  9 09:26:16 2008
@@ -1,4 +1,4 @@
-#$Id: back_postgresql.py,v 1.37 2006/11/09 00:55:33 richard Exp $
+#$Id: back_postgresql.py,v 1.43 2007/09/28 15:15:06 jpend Exp $
 #
 # Copyright (c) 2003 Martynas Sklyzmantas, Andrey Lebedev <andrey at micro.lt>
 #
@@ -13,13 +13,16 @@
 try:
     import psycopg
     from psycopg import QuotedString
+    from psycopg import ProgrammingError
 except:
     from psycopg2 import psycopg1 as psycopg
     from psycopg2.extensions import QuotedString
+    from psycopg2.psycopg1 import ProgrammingError
 import logging
 
 from roundup import hyperdb, date
 from roundup.backends import rdbms_common
+from roundup.backends import sessions_rdbms
 
 def connection_dict(config, dbnamestr=None):
     ''' read_default_group is MySQL-specific, ignore it '''
@@ -51,12 +54,12 @@
     '''
     template1 = connection_dict(config)
     template1['database'] = 'template1'
-    
+
     try:
         conn = psycopg.connect(**template1)
     except psycopg.OperationalError, message:
         raise hyperdb.DatabaseError, message
-    
+
     conn.set_isolation_level(0)
     cursor = conn.cursor()
     try:
@@ -70,7 +73,7 @@
 def pg_command(cursor, command):
     '''Execute the postgresql command, which may be blocked by some other
     user connecting to the database, and return a true value if it succeeds.
-    
+
     If there is a concurrent update, retry the command.
     '''
     try:
@@ -79,7 +82,7 @@
         response = str(err).split('\n')[0]
         if response.find('FATAL') != -1:
             raise RuntimeError, response
-        elif response.find('ERROR') != -1:
+        else:
             msgs = [
                 'is being accessed by other users',
                 'could not serialize access due to concurrent update',
@@ -104,12 +107,28 @@
     except:
         return 0
 
+class Sessions(sessions_rdbms.Sessions):
+    def set(self, *args, **kwargs):
+        try:
+            sessions_rdbms.Sessions.set(self, *args, **kwargs)
+        except ProgrammingError, err:
+            response = str(err).split('\n')[0]
+            if -1 != response.find('ERROR') and \
+               -1 != response.find('could not serialize access due to concurrent update'):
+                # another client just updated, and we're running on
+                # serializable isolation.
+                # see http://www.postgresql.org/docs/7.4/interactive/transaction-iso.html
+                self.db.rollback()
+
 class Database(rdbms_common.Database):
     arg = '%s'
 
     # used by some code to switch styles of query
     implements_intersect = 1
 
+    def getSessionManager(self):
+        return Sessions(self)
+
     def sql_open_connection(self):
         db = connection_dict(self.config, 'database')
         logging.getLogger('hyperdb').info('open database %r'%db['database'])
@@ -266,3 +285,4 @@
 class FileClass(PostgresqlClass, rdbms_common.FileClass):
     pass
 
+# vim: set et sts=4 sw=4 :

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	Sun Mar  9 09:26:16 2008
@@ -1,4 +1,4 @@
-# $Id: back_sqlite.py,v 1.50 2006/12/19 03:01:37 richard Exp $
+# $Id: back_sqlite.py,v 1.51 2007/06/21 07:35:50 schlatterbeck Exp $
 '''Implements a backend for SQLite.
 
 See https://pysqlite.sourceforge.net/ for pysqlite info
@@ -144,6 +144,9 @@
         self.sql('CREATE TABLE __words (_word varchar, '
             '_textid integer)')
         self.sql('CREATE INDEX words_word_ids ON __words(_word)')
+        self.sql('CREATE INDEX words_by_id ON __words (_textid)')
+        self.sql('CREATE UNIQUE INDEX __textids_by_props ON '
+                 '__textids (_class, _itemid, _prop)')
         sql = 'insert into ids (name, num) values (%s,%s)'%(self.arg, self.arg)
         self.sql(sql, ('__textids', 1))
 

Modified: tracker/roundup-src/roundup/backends/blobfiles.py
==============================================================================
--- tracker/roundup-src/roundup/backends/blobfiles.py	(original)
+++ tracker/roundup-src/roundup/backends/blobfiles.py	Sun Mar  9 09:26:16 2008
@@ -14,8 +14,8 @@
 # FOR A PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
-# 
-#$Id: blobfiles.py,v 1.19 2005/06/08 03:35:18 anthonybaxter Exp $
+#
+#$Id: blobfiles.py,v 1.24 2008/02/07 00:57:59 richard Exp $
 '''This module exports file storage for roundup backends.
 Files are stored into a directory hierarchy.
 '''
@@ -36,22 +36,204 @@
     return num_files
 
 class FileStorage:
-    """Store files in some directory structure"""    
+    """Store files in some directory structure
+
+    Some databases do not permit the storage of arbitrary data (i.e.,
+    file content).  And, some database schema explicitly store file
+    content in the fielsystem.  In particular, if a class defines a
+    'filename' property, it is assumed that the data is stored in the
+    indicated file, outside of whatever database Roundup is otherwise
+    using.
+
+    In these situations, it is difficult to maintain the transactional
+    abstractions used elsewhere in Roundup.  In particular, if a
+    file's content is edited, but then the containing transaction is
+    not committed, we do not want to commit the edit.  Similarly, we
+    would like to guarantee that if a transaction is committed to the
+    database, then the edit has in fact taken place.
+
+    This class provides an approximation of these transactional
+    requirements.
+
+    For classes that do not have a 'filename' property, the file name
+    used to store the file's content is a deterministic function of
+    the classname and nodeid for the file.  The 'filename' function
+    computes this name.  The name will contain directories and
+    subdirectories, but, suppose, for the purposes of what follows,
+    that the filename is 'file'.
+
+    Edit Procotol
+    -------------
+    
+    When a file is created or edited, the following protocol is used:
+
+    1. The new content of the file is placed in 'file.tmp'.
+
+    2. A transaction is recored in 'self.transactions' referencing the
+       'doStoreFile' method of this class.
+
+    3. At some subsequent point, the database 'commit' function is
+       called.  This function first performs a traditional database
+       commit (for example, by issuing a SQL command to commit the
+       current transaction), and, then, runs the transactions recored
+       in 'self.transactions'.
+
+    4. The 'doStoreFile' method renames the 'file.tmp' to 'file'.
+
+    If Step 3 never occurs, but, instead, the database 'rollback'
+    method is called, then that method, after rolling back the
+    database transaction, calls 'rollbackStoreFile', which removes
+    'file.tmp'.
+
+    Race Condition
+    --------------
+
+    If two Roundup instances (say, the mail gateway and a web client,
+    or two web clients running with a multi-process server) attempt
+    edits at the same time, both will write to 'file.tmp', and the
+    results will be indeterminate.
+    
+    Crash Analysis
+    --------------
+    
+    There are several situations that may occur if a crash (whether
+    because the machine crashes, because an unhandled Python exception
+    is raised, or because the Python process is killed) occurs.
+    
+    Complexity ensues because backuping up an RDBMS is generally more
+    complex than simply copying a file.  Instead, some command is run
+    which stores a snapshot of the database in a file.  So, if you
+    back up the database to a file, and then back up the filesystem,
+    it is likely that further database transactions have occurred
+    between the point of database backup and the point of filesystem
+    backup.
+
+    For the purposes, of this analysis, we assume that the filesystem
+    backup occurred after the database backup.  Furthermore, we assume
+    that filesystem backups are atomic; i.e., the at the filesystem is
+    not being modified during the backup.
+
+    1. Neither the 'commit' nor 'rollback' methods on the database are
+       ever called.
+
+       In this case, the '.tmp' file should be ignored as the
+       transaction was not committed.
+
+    2. The 'commit' method is called.  Subsequently, the machine
+       crashes, and is restored from backups.
+
+       The most recent filesystem backup and the most recent database
+       backup are not in general from the same instant in time.
+
+       This problem means that we can never be sure after a crash if
+       the contents of a file are what we intend.  It is always
+       possible that an edit was made to the file that is not
+       reflected in the filesystem.
+
+    3. A crash occurs between the point of the database commit and the
+       call to 'doStoreFile'.
+
+       If only one of 'file' and 'file.tmp' exists, then that
+       version should be used.  However, if both 'file' and 'file.tmp'
+       exist, there is no way to know which version to use.
+
+    Reading the File
+    ----------------
+
+    When determining the content of the file, we use the following
+    algorithm:
+
+    1. If 'self.transactions' reflects an edit of the file, then use
+       'file.tmp'.
+
+       We know that an edit to the file is in process so 'file.tmp' is
+       the right choice.  If 'file.tmp' does not exist, raise an
+       exception; something has removed the content of the file while
+       we are in the process of editing it.
+
+    2. Otherwise, if 'file.tmp' exists, and 'file' does not, use
+       'file.tmp'.
+
+       We know that the file is supposed to exist because there is a
+       reference to it in the database.  Since 'file' does not exist,
+       we assume that Crash 3 occurred during the initial creation of
+       the file.
+
+    3. Otherwise, use 'file'.
+
+       If 'file.tmp' is not present, this is obviously the best we can
+       do.  This is always the right answer unless Crash 2 occurred,
+       in which case the contents of 'file' may be newer than they
+       were at the point of database backup.
+
+       If 'file.tmp' is present, we know that we are not actively
+       editing the file.  The possibilities are:
+
+       a. Crash 1 has occurred.  In this case, using 'file' is the
+          right answer, so we will have chosen correctly.
+
+       b. Crash 3 has occurred.  In this case, 'file.tmp' is the right
+          answer, so we will have chosen incorrectly.  However, 'file'
+          was at least a previously committed value.
+
+    Future Improvements
+    -------------------
+
+    One approach would be to take advantage of databases which do
+    allow the storage of arbitary date.  For example, MySQL provides
+    the HUGE BLOB datatype for storing up to 4GB of data.
+
+    Another approach would be to store a version ('v') in the actual
+    database and name files 'file.v'.  Then, the editing protocol
+    would become:
+
+    1. Generate a new version 'v', guaranteed to be different from all
+       other versions ever used by the database.  (The version need
+       not be in any particular sequence; a UUID would be fine.)
+
+    2. Store the content in 'file.v'.
+
+    3. Update the database to indicate that the version of the node is
+       'v'.
+
+    Now, if the transaction is committed, the database will refer to
+    'file.v', where the content exists.  If the transaction is rolled
+    back, or not committed, 'file.v' will never be referenced.  In the
+    event of a crash, under the assumptions above, there may be
+    'file.v' files that are not referenced by the database, but the
+    database will be consistent, so long as unreferenced 'file.v'
+    files are never removed until after the database has been backed
+    up.
+    """    
+
+    tempext = '.tmp'
+    """The suffix added to files indicating that they are uncommitted."""
+    
+    def __init__(self, umask):
+        self.umask = umask
+
     def subdirFilename(self, classname, nodeid, property=None):
         """Determine what the filename and subdir for nodeid + classname is."""
         if property:
             name = '%s%s.%s'%(classname, nodeid, property)
         else:
-            # roundupdb.FileClass never specified the property name, so don't 
+            # roundupdb.FileClass never specified the property name, so don't
             # include it
             name = '%s%s'%(classname, nodeid)
-        
+
         # have a separate subdir for every thousand messages
         subdir = str(int(nodeid) / 1000)
         return os.path.join(subdir, name)
-    
+
+    def _tempfile(self, filename):
+        """Return a temporary filename.
+
+        'filename' -- The name of the eventual destination file."""
+
+        return filename + self.tempext
+
     def filename(self, classname, nodeid, property=None, create=0):
-        '''Determine what the filename for the given node and optionally 
+        '''Determine what the filename for the given node and optionally
         property is.
 
         Try a variety of different filenames - the file could be in the
@@ -60,14 +242,45 @@
         '''
         filename  = os.path.join(self.dir, 'files', classname,
                                  self.subdirFilename(classname, nodeid, property))
-        if create or os.path.exists(filename):
+        # If the caller is going to create the file, return the
+        # post-commit filename.  It is the callers responsibility to
+        # add self.tempext when actually creating the file.
+        if create:
             return filename
 
-        # try .tmp
-        filename = filename + '.tmp'
+        tempfile = self._tempfile(filename)
+
+        # If an edit to this file is in progress, then return the name
+        # of the temporary file containing the edited content.
+        for method, args in self.transactions:
+            if (method == self.doStoreFile and
+                    args == (classname, nodeid, property)):
+                # There is an edit in progress for this file.
+                if not os.path.exists(tempfile):
+                    raise IOError('content file for %s not found'%tempfile)
+                return tempfile
+
         if os.path.exists(filename):
             return filename
 
+        # Otherwise, if the temporary file exists, then the probable 
+        # explanation is that a crash occurred between the point that
+        # the database entry recording the creation of the file
+        # occured and the point at which the file was renamed from the
+        # temporary name to the final name.
+        if os.path.exists(tempfile):
+            try:
+                # Clean up, by performing the commit now.
+                os.rename(tempfile, filename)
+            except:
+                pass
+            # If two Roundup clients both try to rename the file
+            # at the same time, only one of them will succeed.
+            # So, tolerate such an error -- but no other.
+            if not os.path.exists(filename):
+                raise IOError('content file for %s not found'%filename)
+            return filename
+
         # ok, try flat (very old-style)
         if property:
             filename = os.path.join(self.dir, 'files', '%s%s.%s'%(classname,
@@ -94,13 +307,17 @@
             os.makedirs(os.path.dirname(name))
 
         # save to a temp file
-        name = name + '.tmp'
+        name = self._tempfile(name)
 
         # make sure we don't register the rename action more than once
         if not os.path.exists(name):
             # save off the rename action
             self.transactions.append((self.doStoreFile, (classname, nodeid,
                 property)))
+        # always set umask before writing to make sure we have the proper one
+        # in multi-tracker (i.e. multi-umask) or modpython scenarios
+        # the umask may have changed since last we set it.
+        os.umask(self.umask)
         open(name, 'wb').write(content)
 
     def getfile(self, classname, nodeid, property):
@@ -125,16 +342,16 @@
         '''Store the file as part of a transaction commit.
         '''
         # determine the name of the file to write to
-        name = self.filename(classname, nodeid, property)
+        name = self.filename(classname, nodeid, property, 1)
 
         # the file is currently ".tmp" - move it to its real name to commit
-        if name.endswith('.tmp'):
+        if name.endswith(self.tempext):
             # creation
             dstname = os.path.splitext(name)[0]
         else:
             # edit operation
             dstname = name
-            name = name + '.tmp'
+            name = self._tempfile(name)
 
         # content is being updated (and some platforms, eg. win32, won't
         # let us rename over the top of the old file)
@@ -151,8 +368,25 @@
         '''
         # determine the name of the file to delete
         name = self.filename(classname, nodeid, property)
-        if not name.endswith('.tmp'):
-            name += '.tmp'
+        if not name.endswith(self.tempext):
+            name += self.tempext
         os.remove(name)
 
+    def isStoreFile(self, classname, nodeid):
+        '''See if there is actually any FileStorage for this node.
+           Is there a better way than using self.filename?
+        '''
+        try:
+            fname = self.filename(classname, nodeid)
+            return True
+        except IOError:
+            return False
+
+    def destroy(self, classname, nodeid):
+        '''If there is actually FileStorage for this node
+           remove it from the filesystem
+        '''
+        if self.isStoreFile(classname, nodeid):
+            os.remove(self.filename(classname, nodeid))
+
 # vim: set filetype=python ts=4 sw=4 et si

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	Sun Mar  9 09:26:16 2008
@@ -1,4 +1,4 @@
-#$Id: indexer_xapian.py,v 1.4 2006/02/10 00:16:13 richard Exp $
+#$Id: indexer_xapian.py,v 1.6 2007/10/25 07:02:42 richard Exp $
 ''' This implements the full-text indexer using the Xapian indexer.
 '''
 import re, os
@@ -32,7 +32,7 @@
     def close(self):
         '''close the indexing database'''
         pass
-  
+
     def rollback(self):
         if not self.transaction_active:
             return
@@ -92,7 +92,7 @@
             word = match.group(0)
             if self.is_stopword(word):
                 continue
-            term = stemmer.stem_word(word)
+            term = stemmer(word)
             doc.add_posting(term, match.start(0))
         if docid:
             database.replace_document(docid, doc)
@@ -103,7 +103,7 @@
         '''look up all the words in the wordlist.
         If none are found return an empty dictionary
         * more rules here
-        '''        
+        '''
         if not wordlist:
             return {}
 
@@ -113,7 +113,7 @@
         stemmer = xapian.Stem("english")
         terms = []
         for term in [word.upper() for word in wordlist if 26 > len(word) > 2]:
-            terms.append(stemmer.stem_word(term.upper()))
+            terms.append(stemmer(term.upper()))
         query = xapian.Query(xapian.Query.OP_AND, terms)
 
         enquire.set_query(query)

Modified: tracker/roundup-src/roundup/backends/rdbms_common.py
==============================================================================
--- tracker/roundup-src/roundup/backends/rdbms_common.py	(original)
+++ tracker/roundup-src/roundup/backends/rdbms_common.py	Sun Mar  9 09:26:16 2008
@@ -15,8 +15,8 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-#$Id: rdbms_common.py,v 1.182 2006/10/04 01:12:00 richard Exp $
-''' Relational database (SQL) backend common code.
+#$Id: rdbms_common.py,v 1.195 2008/02/07 05:01:42 richard Exp $
+""" Relational database (SQL) backend common code.
 
 Basics:
 
@@ -29,7 +29,7 @@
 - journals are stored adjunct to the per-class tables
 - table names and columns have "_" prepended so the names can't clash with
   restricted names (like "order")
-- retirement is determined by the __retired__ column being true
+- retirement is determined by the __retired__ column being > 0
 
 Database-specific changes may generally be pushed out to the overridable
 sql_* methods, since everything else should be fairly generic. There's
@@ -42,7 +42,14 @@
 that maps to a table. If that information differs from the hyperdb schema,
 then we update it. We also store in the schema dict a version which
 allows us to upgrade the database schema when necessary. See upgrade_db().
-'''
+
+To force a unqiueness constraint on the key properties we put the item
+id into the __retired__ column duing retirement (so it's 0 for "active"
+items) and place a unqiueness constraint on key + __retired__. This is
+particularly important for the users class where multiple users may
+try to have the same username, with potentially many retired users with
+the same name.
+"""
 __docformat__ = 'restructuredtext'
 
 # standard python modules
@@ -54,6 +61,7 @@
     Multilink, DatabaseError, Boolean, Number, Node
 from roundup.backends import locking
 from roundup.support import reversed
+from roundup.i18n import _
 
 # support
 from blobfiles import FileStorage
@@ -84,8 +92,8 @@
     return int(value)
 
 def connection_dict(config, dbnamestr=None):
-    ''' Used by Postgresql and MySQL to detemine the keyword args for
-    opening the database connection.'''
+    """ Used by Postgresql and MySQL to detemine the keyword args for
+    opening the database connection."""
     d = { }
     if dbnamestr:
         d[dbnamestr] = config.RDBMS_NAME
@@ -97,15 +105,16 @@
     return d
 
 class Database(FileStorage, hyperdb.Database, roundupdb.Database):
-    ''' Wrapper around an SQL database that presents a hyperdb interface.
+    """ Wrapper around an SQL database that presents a hyperdb interface.
 
         - some functionality is specific to the actual SQL database, hence
           the sql_* methods that are NotImplemented
         - we keep a cache of the latest ROW_CACHE_SIZE row fetches.
-    '''
+    """
     def __init__(self, config, journaltag=None):
-        ''' Open the database and load the schema from it.
-        '''
+        """ Open the database and load the schema from it.
+        """
+        FileStorage.__init__(self, config.UMASK)
         self.config, self.journaltag = config, journaltag
         self.dir = config.DATABASE
         self.classes = {}
@@ -139,15 +148,15 @@
         return OneTimeKeys(self)
 
     def open_connection(self):
-        ''' Open a connection to the database, creating it if necessary.
+        """ Open a connection to the database, creating it if necessary.
 
             Must call self.load_dbschema()
-        '''
+        """
         raise NotImplemented
 
     def sql(self, sql, args=None):
-        ''' Execute the sql with the optional args.
-        '''
+        """ Execute the sql with the optional args.
+        """
         if __debug__:
             logging.getLogger('hyperdb').debug('SQL %r %r'%(sql, args))
         if args:
@@ -156,18 +165,18 @@
             self.cursor.execute(sql)
 
     def sql_fetchone(self):
-        ''' Fetch a single row. If there's nothing to fetch, return None.
-        '''
+        """ Fetch a single row. If there's nothing to fetch, return None.
+        """
         return self.cursor.fetchone()
 
     def sql_fetchall(self):
-        ''' Fetch all rows. If there's nothing to fetch, return [].
-        '''
+        """ Fetch all rows. If there's nothing to fetch, return [].
+        """
         return self.cursor.fetchall()
 
     def sql_stringquote(self, value):
-        ''' Quote the string so it's safe to put in the 'sql quotes'
-        '''
+        """ Quote the string so it's safe to put in the 'sql quotes'
+        """
         return re.sub("'", "''", str(value))
 
     def init_dbschema(self):
@@ -177,8 +186,8 @@
         }
 
     def load_dbschema(self):
-        ''' Load the schema definition that the database currently implements
-        '''
+        """ Load the schema definition that the database currently implements
+        """
         self.cursor.execute('select schema from schema')
         schema = self.cursor.fetchone()
         if schema:
@@ -187,18 +196,18 @@
             self.database_schema = {}
 
     def save_dbschema(self):
-        ''' Save the schema definition that the database currently implements
-        '''
+        """ Save the schema definition that the database currently implements
+        """
         s = repr(self.database_schema)
         self.sql('delete from schema')
         self.sql('insert into schema values (%s)'%self.arg, (s,))
 
     def post_init(self):
-        ''' Called once the schema initialisation has finished.
+        """ Called once the schema initialisation has finished.
 
             We should now confirm that the schema defined by our "classes"
             attribute actually matches the schema in the database.
-        '''
+        """
         save = 0
 
         # handle changes in the schema
@@ -237,12 +246,13 @@
 
     # update this number when we need to make changes to the SQL structure
     # of the backen database
-    current_db_version = 4
+    current_db_version = 5
+    db_version_updated = False
     def upgrade_db(self):
-        ''' Update the SQL database to reflect changes in the backend code.
+        """ Update the SQL database to reflect changes in the backend code.
 
             Return boolean whether we need to save the schema.
-        '''
+        """
         version = self.database_schema.get('version', 1)
         if version == self.current_db_version:
             # nothing to do
@@ -270,7 +280,11 @@
         if version < 4:
             self.fix_version_3_tables()
 
+        if version < 5:
+            self.fix_version_4_tables()
+
         self.database_schema['version'] = self.current_db_version
+        self.db_version_updated = True
         return 1
 
     def fix_version_3_tables(self):
@@ -281,11 +295,23 @@
             self.sql('ALTER TABLE %ss ADD %s_value TEXT'%(name, name))
 
     def fix_version_2_tables(self):
-        '''Default (used by sqlite): NOOP'''
+        # Default (used by sqlite): NOOP
         pass
 
+    def fix_version_4_tables(self):
+        # note this is an explicit call now
+        c = self.cursor
+        for cn, klass in self.classes.items():
+            c.execute('select id from _%s where __retired__<>0'%(cn,))
+            for (id,) in c.fetchall():
+                c.execute('update _%s set __retired__=%s where id=%s'%(cn,
+                    self.arg, self.arg), (id, id))
+
+            if klass.key:
+                self.add_class_key_required_unique_constraint(cn, klass.key)
+
     def _convert_journal_tables(self):
-        '''Get current journal table contents, drop the table and re-create'''
+        """Get current journal table contents, drop the table and re-create"""
         c = self.cursor
         cols = ','.join('nodeid date tag action params'.split())
         for klass in self.classes.values():
@@ -307,8 +333,8 @@
                 self.cursor.execute(sql, row)
 
     def _convert_string_properties(self):
-        '''Get current Class tables that contain String properties, and
-        convert the VARCHAR columns to TEXT'''
+        """Get current Class tables that contain String properties, and
+        convert the VARCHAR columns to TEXT"""
         c = self.cursor
         for klass in self.classes.values():
             # slurp and drop
@@ -363,11 +389,11 @@
         hyperdb.Number    : 'REAL',
     }
     def determine_columns(self, properties):
-        ''' Figure the column names and multilink properties from the spec
+        """ Figure the column names and multilink properties from the spec
 
             "properties" is a list of (name, prop) where prop may be an
             instance of a hyperdb "type" _or_ a string repr of that type.
-        '''
+        """
         cols = [
             ('_actor', self.hyperdb_to_sql_datatypes[hyperdb.Link]),
             ('_activity', self.hyperdb_to_sql_datatypes[hyperdb.Date]),
@@ -397,11 +423,11 @@
         return cols, mls
 
     def update_class(self, spec, old_spec, force=0):
-        ''' Determine the differences between the current spec and the
+        """ Determine the differences between the current spec and the
             database version of the spec, and update where necessary.
 
             If 'force' is true, update the database anyway.
-        '''
+        """
         new_has = spec.properties.has_key
         new_spec = spec.schema()
         new_spec[1].sort()
@@ -500,8 +526,8 @@
         return cols, mls
 
     def create_class_table(self, spec):
-        '''Create the class table for the given Class "spec". Creates the
-        indexes too.'''
+        """Create the class table for the given Class "spec". Creates the
+        indexes too."""
         cols, mls = self.determine_all_columns(spec)
 
         # create the base table
@@ -514,8 +540,8 @@
         return cols, mls
 
     def create_class_table_indexes(self, spec):
-        ''' create the class table for the given spec
-        '''
+        """ create the class table for the given spec
+        """
         # create __retired__ index
         index_sql2 = 'create index _%s_retired_idx on _%s(__retired__)'%(
                         spec.classname, spec.classname)
@@ -528,9 +554,18 @@
                         spec.classname, spec.key)
             self.sql(index_sql3)
 
+            # and the unique index for key / retired(id)
+            self.add_class_key_required_unique_constraint(spec.classname,
+                spec.key)
+
         # TODO: create indexes on (selected?) Link property columns, as
         # they're more likely to be used for lookup
 
+    def add_class_key_required_unique_constraint(self, cn, key):
+        sql = '''create unique index _%s_key_retired_idx 
+            on _%s(__retired__, _%s)'''%(cn, cn, key)
+        self.sql(sql)
+
     def drop_class_table_indexes(self, cn, key):
         # drop the old table indexes first
         l = ['_%s_id_idx'%cn, '_%s_retired_idx'%cn]
@@ -545,29 +580,34 @@
             self.sql(index_sql)
 
     def create_class_table_key_index(self, cn, key):
-        ''' create the class table for the given spec
-        '''
+        """ create the class table for the given spec
+        """
         sql = 'create index _%s_%s_idx on _%s(_%s)'%(cn, key, cn, key)
         self.sql(sql)
 
     def drop_class_table_key_index(self, cn, key):
         table_name = '_%s'%cn
         index_name = '_%s_%s_idx'%(cn, key)
-        if not self.sql_index_exists(table_name, index_name):
-            return
-        sql = 'drop index '+index_name
-        self.sql(sql)
+        if self.sql_index_exists(table_name, index_name):
+            sql = 'drop index '+index_name
+            self.sql(sql)
+
+        # and now the retired unique index too
+        index_name = '_%s_key_retired_idx'%cn
+        if self.sql_index_exists(table_name, index_name):
+            sql = 'drop index '+index_name
+            self.sql(sql)
 
     def create_journal_table(self, spec):
-        ''' create the journal table for a class given the spec and
+        """ create the journal table for a class given the spec and
             already-determined cols
-        '''
+        """
         # journal table
         cols = ','.join(['%s varchar'%x
             for x in 'nodeid date tag action params'.split()])
-        sql = '''create table %s__journal (
+        sql = """create table %s__journal (
             nodeid integer, date %s, tag varchar(255),
-            action varchar(255), params text)''' % (spec.classname,
+            action varchar(255), params text)""" % (spec.classname,
             self.hyperdb_to_sql_datatypes[hyperdb.Date])
         self.sql(sql)
         self.create_journal_table_indexes(spec)
@@ -586,9 +626,9 @@
         self.sql(index_sql)
 
     def create_multilink_table(self, spec, ml):
-        ''' Create a multilink table for the "ml" property of the class
+        """ Create a multilink table for the "ml" property of the class
             given by the spec
-        '''
+        """
         # create the table
         sql = 'create table %s_%s (linkid INTEGER, nodeid INTEGER)'%(
             spec.classname, ml)
@@ -619,8 +659,8 @@
             self.sql(index_sql)
 
     def create_class(self, spec):
-        ''' Create a database table according to the given spec.
-        '''
+        """ Create a database table according to the given spec.
+        """
         cols, mls = self.create_class_table(spec)
         self.create_journal_table(spec)
 
@@ -629,14 +669,14 @@
             self.create_multilink_table(spec, ml)
 
     def drop_class(self, cn, spec):
-        ''' Drop the given table from the database.
+        """ Drop the given table from the database.
 
             Drop the journal and multilink tables too.
-        '''
+        """
         properties = spec[1]
         # figure the multilinks
         mls = []
-        for propanme, prop in properties:
+        for propname, prop in properties:
             if isinstance(prop, Multilink):
                 mls.append(propname)
 
@@ -664,15 +704,15 @@
     # Classes
     #
     def __getattr__(self, classname):
-        ''' A convenient way of calling self.getclass(classname).
-        '''
+        """ A convenient way of calling self.getclass(classname).
+        """
         if self.classes.has_key(classname):
             return self.classes[classname]
         raise AttributeError, classname
 
     def addclass(self, cl):
-        ''' Add a Class to the hyperdatabase.
-        '''
+        """ Add a Class to the hyperdatabase.
+        """
         cn = cl.classname
         if self.classes.has_key(cn):
             raise ValueError, cn
@@ -687,28 +727,28 @@
             description="User is allowed to access "+cn)
 
     def getclasses(self):
-        ''' Return a list of the names of all existing classes.
-        '''
+        """ Return a list of the names of all existing classes.
+        """
         l = self.classes.keys()
         l.sort()
         return l
 
     def getclass(self, classname):
-        '''Get the Class object representing a particular class.
+        """Get the Class object representing a particular class.
 
         If 'classname' is not a valid class name, a KeyError is raised.
-        '''
+        """
         try:
             return self.classes[classname]
         except KeyError:
             raise KeyError, 'There is no class called "%s"'%classname
 
     def clear(self):
-        '''Delete all database contents.
+        """Delete all database contents.
 
         Note: I don't commit here, which is different behaviour to the
               "nuke from orbit" behaviour in the dbs.
-        '''
+        """
         logging.getLogger('hyperdb').info('clear')
         for cn in self.classes.keys():
             sql = 'delete from _%s'%cn
@@ -730,8 +770,8 @@
         hyperdb.Multilink : lambda x: x,    # used in journal marshalling
     }
     def addnode(self, classname, nodeid, node):
-        ''' Add the specified node to its class's db.
-        '''
+        """ Add the specified node to its class's db.
+        """
         if __debug__:
             logging.getLogger('hyperdb').debug('addnode %s%s %r'%(classname,
                 nodeid, node))
@@ -805,8 +845,8 @@
                 self.sql(sql, (entry, nodeid))
 
     def setnode(self, classname, nodeid, values, multilink_changes={}):
-        ''' Change the specified node.
-        '''
+        """ Change the specified node.
+        """
         if __debug__:
             logging.getLogger('hyperdb').debug('setnode %s%s %r'
                 % (classname, nodeid, values))
@@ -919,8 +959,8 @@
         hyperdb.Multilink : lambda x: x,    # used in journal marshalling
     }
     def getnode(self, classname, nodeid):
-        ''' Get a node from the database.
-        '''
+        """ Get a node from the database.
+        """
         # see if we have this node cached
         key = (classname, nodeid)
         if self.cache.has_key(key):
@@ -990,9 +1030,9 @@
         return node
 
     def destroynode(self, classname, nodeid):
-        '''Remove a node from the database. Called exclusively by the
+        """Remove a node from the database. Called exclusively by the
            destroy() method on Class.
-        '''
+        """
         logging.getLogger('hyperdb').info('destroynode %s%s'%(classname, nodeid))
 
         # make sure the node exists
@@ -1024,29 +1064,32 @@
         sql = 'delete from %s__journal where nodeid=%s'%(classname, self.arg)
         self.sql(sql, (nodeid,))
 
+        # cleanup any blob filestorage when we commit
+        self.transactions.append((FileStorage.destroy, (self, classname, nodeid)))
+
     def hasnode(self, classname, nodeid):
-        ''' Determine if the database has a given node.
-        '''
+        """ Determine if the database has a given node.
+        """
         sql = 'select count(*) from _%s where id=%s'%(classname, self.arg)
         self.sql(sql, (nodeid,))
         return int(self.cursor.fetchone()[0])
 
     def countnodes(self, classname):
-        ''' Count the number of nodes that exist for a particular Class.
-        '''
+        """ Count the number of nodes that exist for a particular Class.
+        """
         sql = 'select count(*) from _%s'%classname
         self.sql(sql)
         return self.cursor.fetchone()[0]
 
     def addjournal(self, classname, nodeid, action, params, creator=None,
             creation=None):
-        ''' Journal the Action
+        """ Journal the Action
         'action' may be:
 
             'create' or 'set' -- 'params' is a dictionary of property values
             'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
             'retire' -- 'params' is None
-        '''
+        """
         # handle supply of the special journalling parameters (usually
         # supplied on importing an existing database)
         if creator:
@@ -1078,7 +1121,7 @@
             journaltag, action, params)
 
     def setjournal(self, classname, nodeid, journal):
-        '''Set the journal to the "journal" list.'''
+        """Set the journal to the "journal" list."""
         # clear out any existing entries
         self.sql('delete from %s__journal where nodeid=%s'%(classname,
             self.arg), (nodeid,))
@@ -1102,8 +1145,8 @@
                 journaltag, action, params)
 
     def _journal_marshal(self, params, classname):
-        '''Convert the journal params values into safely repr'able and
-        eval'able values.'''
+        """Convert the journal params values into safely repr'able and
+        eval'able values."""
         properties = self.getclass(classname).getprops()
         for param, value in params.items():
             if not value:
@@ -1120,8 +1163,8 @@
                 params[param] = cvt(value)
 
     def getjournal(self, classname, nodeid):
-        ''' get the journal for id
-        '''
+        """ get the journal for id
+        """
         # make sure the node exists
         if not self.hasnode(classname, nodeid):
             raise IndexError, '%s has no node %s'%(classname, nodeid)
@@ -1158,8 +1201,8 @@
 
     def save_journal(self, classname, cols, nodeid, journaldate,
             journaltag, action, params):
-        ''' Save the journal entry to the database
-        '''
+        """ Save the journal entry to the database
+        """
         entry = (nodeid, journaldate, journaltag, action, params)
 
         # do the insert
@@ -1169,8 +1212,8 @@
         self.sql(sql, entry)
 
     def load_journal(self, classname, cols, nodeid):
-        ''' Load the journal from the database
-        '''
+        """ Load the journal from the database
+        """
         # now get the journal entries
         sql = 'select %s from %s__journal where nodeid=%s order by date'%(
             cols, classname, self.arg)
@@ -1178,8 +1221,8 @@
         return self.cursor.fetchall()
 
     def pack(self, pack_before):
-        ''' Delete all journal entries except "create" before 'pack_before'.
-        '''
+        """ Delete all journal entries except "create" before 'pack_before'.
+        """
         date_stamp = self.hyperdb_to_sql_value[Date](pack_before)
 
         # do the delete
@@ -1189,8 +1232,8 @@
             self.sql(sql, (date_stamp,))
 
     def sql_commit(self, fail_ok=False):
-        ''' Actually commit to the database.
-        '''
+        """ Actually commit to the database.
+        """
         logging.getLogger('hyperdb').info('commit')
 
         self.conn.commit()
@@ -1199,7 +1242,7 @@
         self.cursor = self.conn.cursor()
 
     def commit(self, fail_ok=False):
-        ''' Commit the current transactions.
+        """ Commit the current transactions.
 
         Save all data changed since the database was opened or since the
         last commit() or rollback().
@@ -1209,7 +1252,7 @@
         database. We don't care if there's a concurrency issue there.
 
         The only backend this seems to affect is postgres.
-        '''
+        """
         # commit the database
         self.sql_commit(fail_ok)
 
@@ -1227,11 +1270,11 @@
         self.conn.rollback()
 
     def rollback(self):
-        ''' Reverse all actions from the current transaction.
+        """ Reverse all actions from the current transaction.
 
         Undo all the changes made since the database was opened or the last
         commit() or rollback() was performed.
-        '''
+        """
         logging.getLogger('hyperdb').info('rollback')
 
         self.sql_rollback()
@@ -1251,8 +1294,8 @@
         self.conn.close()
 
     def close(self):
-        ''' Close off the connection.
-        '''
+        """ Close off the connection.
+        """
         self.indexer.close()
         self.sql_close()
 
@@ -1260,31 +1303,31 @@
 # The base Class class
 #
 class Class(hyperdb.Class):
-    ''' The handle to a particular class of nodes in a hyperdatabase.
+    """ The handle to a particular class of nodes in a hyperdatabase.
 
         All methods except __repr__ and getnode must be implemented by a
         concrete backend Class.
-    '''
+    """
 
     def schema(self):
-        ''' A dumpable version of the schema that we can store in the
+        """ A dumpable version of the schema that we can store in the
             database
-        '''
+        """
         return (self.key, [(x, repr(y)) for x,y in self.properties.items()])
 
     def enableJournalling(self):
-        '''Turn journalling on for this class
-        '''
+        """Turn journalling on for this class
+        """
         self.do_journal = 1
 
     def disableJournalling(self):
-        '''Turn journalling off for this class
-        '''
+        """Turn journalling off for this class
+        """
         self.do_journal = 0
 
     # Editing nodes:
     def create(self, **propvalues):
-        ''' Create a new node of this class and return its id.
+        """ Create a new node of this class and return its id.
 
         The keyword arguments in 'propvalues' map property names to values.
 
@@ -1299,20 +1342,20 @@
 
         If an id in a link or multilink property does not refer to a valid
         node, an IndexError is raised.
-        '''
+        """
         self.fireAuditors('create', None, propvalues)
         newid = self.create_inner(**propvalues)
         self.fireReactors('create', newid, None)
         return newid
 
     def create_inner(self, **propvalues):
-        ''' Called by create, in-between the audit and react calls.
-        '''
+        """ Called by create, in-between the audit and react calls.
+        """
         if propvalues.has_key('id'):
             raise KeyError, '"id" is reserved'
 
         if self.db.journaltag is None:
-            raise DatabaseError, 'Database open read-only'
+            raise DatabaseError, _('Database open read-only')
 
         if propvalues.has_key('creator') or propvalues.has_key('actor') or \
              propvalues.has_key('creation') or propvalues.has_key('activity'):
@@ -1363,8 +1406,10 @@
                         (self.classname, newid, key))
 
             elif isinstance(prop, Multilink):
-                if type(value) != type([]):
-                    raise TypeError, 'new property "%s" not a list of ids'%key
+                if value is None:
+                    value = []
+                if not hasattr(value, '__iter__'):
+                    raise TypeError, 'new property "%s" not an iterable of ids'%key
 
                 # clean up and validate the list of links
                 link_class = self.properties[key].classname
@@ -1445,14 +1490,14 @@
         return str(newid)
 
     def get(self, nodeid, propname, default=_marker, cache=1):
-        '''Get the value of a property on an existing node of this class.
+        """Get the value of a property on an existing node of this class.
 
         'nodeid' must be the id of an existing node of this class or an
         IndexError is raised.  'propname' must be the name of a property
         of this class or a KeyError is raised.
 
         'cache' exists for backwards compatibility, and is not used.
-        '''
+        """
         if propname == 'id':
             return nodeid
 
@@ -1501,7 +1546,7 @@
         return d[propname]
 
     def set(self, nodeid, **propvalues):
-        '''Modify a property on an existing node of this class.
+        """Modify a property on an existing node of this class.
 
         'nodeid' must be the id of an existing node of this class or an
         IndexError is raised.
@@ -1517,7 +1562,7 @@
 
         If the value of a Link or Multilink property contains an invalid
         node id, a ValueError is raised.
-        '''
+        """
         self.fireAuditors('set', nodeid, propvalues)
         oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid))
         propvalues = self.set_inner(nodeid, **propvalues)
@@ -1525,8 +1570,8 @@
         return propvalues
 
     def set_inner(self, nodeid, **propvalues):
-        ''' Called by set, in-between the audit and react calls.
-        '''
+        """ Called by set, in-between the audit and react calls.
+        """
         if not propvalues:
             return propvalues
 
@@ -1539,7 +1584,7 @@
             raise KeyError, '"id" is reserved'
 
         if self.db.journaltag is None:
-            raise DatabaseError, 'Database open read-only'
+            raise DatabaseError, _('Database open read-only')
 
         node = self.db.getnode(self.classname, nodeid)
         if self.is_retired(nodeid):
@@ -1613,8 +1658,10 @@
                             (self.classname, nodeid, propname))
 
             elif isinstance(prop, Multilink):
-                if type(value) != type([]):
-                    raise TypeError, 'new property "%s" not a list of'\
+                if value is None:
+                    value = []
+                if not hasattr(value, '__iter__'):
+                    raise TypeError, 'new property "%s" not an iterable of'\
                         ' ids'%propname
                 link_class = self.properties[propname].classname
                 l = []
@@ -1734,16 +1781,16 @@
         return propvalues
 
     def retire(self, nodeid):
-        '''Retire a node.
+        """Retire a node.
 
         The properties on the node remain available from the get() method,
         and the node's id is never reused.
 
         Retired nodes are not returned by the find(), list(), or lookup()
         methods, and other nodes may reuse the values of their key properties.
-        '''
+        """
         if self.db.journaltag is None:
-            raise DatabaseError, 'Database open read-only'
+            raise DatabaseError, _('Database open read-only')
 
         self.fireAuditors('retire', nodeid, None)
 
@@ -1751,19 +1798,19 @@
         # conversion (hello, sqlite)
         sql = 'update _%s set __retired__=%s where id=%s'%(self.classname,
             self.db.arg, self.db.arg)
-        self.db.sql(sql, (1, nodeid))
+        self.db.sql(sql, (nodeid, nodeid))
         if self.do_journal:
             self.db.addjournal(self.classname, nodeid, ''"retired", None)
 
         self.fireReactors('retire', nodeid, None)
 
     def restore(self, nodeid):
-        '''Restore a retired node.
+        """Restore a retired node.
 
         Make node available for all operations like it was before retirement.
-        '''
+        """
         if self.db.journaltag is None:
-            raise DatabaseError, 'Database open read-only'
+            raise DatabaseError, _('Database open read-only')
 
         node = self.db.getnode(self.classname, nodeid)
         # check if key property was overrided
@@ -1788,15 +1835,15 @@
         self.fireReactors('restore', nodeid, None)
 
     def is_retired(self, nodeid):
-        '''Return true if the node is rerired
-        '''
+        """Return true if the node is rerired
+        """
         sql = 'select __retired__ from _%s where id=%s'%(self.classname,
             self.db.arg)
         self.db.sql(sql, (nodeid,))
-        return int(self.db.sql_fetchone()[0])
+        return int(self.db.sql_fetchone()[0]) > 0
 
     def destroy(self, nodeid):
-        '''Destroy a node.
+        """Destroy a node.
 
         WARNING: this method should never be used except in extremely rare
                  situations where there could never be links to the node being
@@ -1814,13 +1861,13 @@
         The node is completely removed from the hyperdb, including all journal
         entries. It will no longer be available, and will generally break code
         if there are any references to the node.
-        '''
+        """
         if self.db.journaltag is None:
-            raise DatabaseError, 'Database open read-only'
+            raise DatabaseError, _('Database open read-only')
         self.db.destroynode(self.classname, nodeid)
 
     def history(self, nodeid):
-        '''Retrieve the journal of edits on a particular node.
+        """Retrieve the journal of edits on a particular node.
 
         'nodeid' must be the id of an existing node of this class or an
         IndexError is raised.
@@ -1831,49 +1878,49 @@
 
         'date' is a Timestamp object specifying the time of the change and
         'tag' is the journaltag specified when the database was opened.
-        '''
+        """
         if not self.do_journal:
             raise ValueError, 'Journalling is disabled for this class'
         return self.db.getjournal(self.classname, nodeid)
 
     # Locating nodes:
     def hasnode(self, nodeid):
-        '''Determine if the given nodeid actually exists
-        '''
+        """Determine if the given nodeid actually exists
+        """
         return self.db.hasnode(self.classname, nodeid)
 
     def setkey(self, propname):
-        '''Select a String property of this class to be the key property.
+        """Select a String property of this class to be the key property.
 
         'propname' must be the name of a String property of this class or
         None, or a TypeError is raised.  The values of the key property on
         all existing nodes must be unique or a ValueError is raised.
-        '''
+        """
         prop = self.getprops()[propname]
         if not isinstance(prop, String):
             raise TypeError, 'key properties must be String'
         self.key = propname
 
     def getkey(self):
-        '''Return the name of the key property for this class or None.'''
+        """Return the name of the key property for this class or None."""
         return self.key
 
     def lookup(self, keyvalue):
-        '''Locate a particular node by its key property and return its id.
+        """Locate a particular node by its key property and return its id.
 
         If this class has no key property, a TypeError is raised.  If the
         'keyvalue' matches one of the values for the key property among
         the nodes in this class, the matching node's id is returned;
         otherwise a KeyError is raised.
-        '''
+        """
         if not self.key:
             raise TypeError, 'No key property set for class %s'%self.classname
 
         # use the arg to handle any odd database type conversion (hello,
         # sqlite)
-        sql = "select id from _%s where _%s=%s and __retired__ <> %s"%(
+        sql = "select id from _%s where _%s=%s and __retired__=%s"%(
             self.classname, self.key, self.db.arg, self.db.arg)
-        self.db.sql(sql, (keyvalue, 1))
+        self.db.sql(sql, (keyvalue, 0))
 
         # see if there was a result that's not retired
         row = self.db.sql_fetchone()
@@ -1886,7 +1933,7 @@
         return str(row[0])
 
     def find(self, **propspec):
-        '''Get the ids of nodes in this class which link to the given nodes.
+        """Get the ids of nodes in this class which link to the given nodes.
 
         'propspec' consists of keyword args propname=nodeid or
                    propname={nodeid:1, }
@@ -1899,7 +1946,7 @@
 
             db.issue.find(messages='1')
             db.issue.find(messages={'1':1,'3':1}, files={'7':1})
-        '''
+        """
         # shortcut
         if not propspec:
             return []
@@ -1938,9 +1985,9 @@
                 s += '_%s in (%s)'%(prop, ','.join([a]*len(values)))
                 where.append('(' + s +')')
         if where:
-            allvalues = (1, ) + allvalues
-            sql.append('''select id from _%s where  __retired__ <> %s
-                and %s'''%(self.classname, a, ' and '.join(where)))
+            allvalues = (0, ) + allvalues
+            sql.append("""select id from _%s where  __retired__=%s
+                and %s"""%(self.classname, a, ' and '.join(where)))
 
         # now multilinks
         for prop, values in propspec:
@@ -1948,7 +1995,7 @@
                 continue
             if not values:
                 continue
-            allvalues += (1, )
+            allvalues += (0, )
             if type(values) is type(''):
                 allvalues += (values,)
                 s = a
@@ -1956,8 +2003,8 @@
                 allvalues += tuple(values.keys())
                 s = ','.join([a]*len(values))
             tn = '%s_%s'%(self.classname, prop)
-            sql.append('''select id from _%s, %s where  __retired__ <> %s
-                  and id = %s.nodeid and %s.linkid in (%s)'''%(self.classname,
+            sql.append("""select id from _%s, %s where  __retired__=%s
+                  and id = %s.nodeid and %s.linkid in (%s)"""%(self.classname,
                   tn, a, tn, tn, s))
 
         if not sql:
@@ -1969,13 +2016,13 @@
         return l
 
     def stringFind(self, **requirements):
-        '''Locate a particular node by matching a set of its String
+        """Locate a particular node by matching a set of its String
         properties in a caseless search.
 
         If the property is not a String property, a TypeError is raised.
 
         The return is a list of the id of all nodes that match.
-        '''
+        """
         where = []
         args = []
         for propname in requirements.keys():
@@ -1987,33 +2034,34 @@
 
         # generate the where clause
         s = ' and '.join(['lower(_%s)=%s'%(col, self.db.arg) for col in where])
-        sql = 'select id from _%s where %s and __retired__<>%s'%(
+        sql = 'select id from _%s where %s and __retired__=%s'%(
             self.classname, s, self.db.arg)
-        args.append(1)
+        args.append(0)
         self.db.sql(sql, tuple(args))
         # XXX numeric ids
         l = [str(x[0]) for x in self.db.sql_fetchall()]
         return l
 
     def list(self):
-        ''' Return a list of the ids of the active nodes in this class.
-        '''
+        """ Return a list of the ids of the active nodes in this class.
+        """
         return self.getnodeids(retired=0)
 
     def getnodeids(self, retired=None):
-        ''' Retrieve all the ids of the nodes for a particular Class.
+        """ Retrieve all the ids of the nodes for a particular Class.
 
             Set retired=None to get all nodes. Otherwise it'll get all the
             retired or non-retired nodes, depending on the flag.
-        '''
+        """
         # flip the sense of the 'retired' flag if we don't want all of them
         if retired is not None:
+            args = (0, )
             if retired:
-                args = (0, )
+                compare = '>'
             else:
-                args = (1, )
-            sql = 'select id from _%s where __retired__ <> %s'%(self.classname,
-                self.db.arg)
+                compare = '='
+            sql = 'select id from _%s where __retired__%s%s'%(self.classname,
+                compare, self.db.arg)
         else:
             args = ()
             sql = 'select id from _%s'%self.classname
@@ -2023,11 +2071,11 @@
         return ids
 
     def _subselect(self, classname, multilink_table):
-        '''Create a subselect. This is factored out because some
+        """Create a subselect. This is factored out because some
            databases (hmm only one, so far) doesn't support subselects
            look for "I can't believe it's not a toy RDBMS" in the mysql
            backend.
-        '''
+        """
         return '_%s.id not in (select nodeid from %s)'%(classname,
             multilink_table)
 
@@ -2040,7 +2088,7 @@
     order_by_null_values = None
 
     def filter(self, search_matches, filterspec, sort=[], group=[]):
-        '''Return a list of the ids of the active nodes in this class that
+        """Return a list of the ids of the active nodes in this class that
         match the 'filter' spec, sorted by the group spec and then the
         sort spec
 
@@ -2058,7 +2106,7 @@
 
         1. String properties must match all elements in the list, and
         2. Other properties must match any of the elements in the list.
-        '''
+        """
         # we can't match anything if search_matches is empty
         if search_matches == {}:
             return []
@@ -2267,7 +2315,7 @@
         props = self.getprops()
 
         # don't match retired nodes
-        where.append('_%s.__retired__ <> 1'%icn)
+        where.append('_%s.__retired__=0'%icn)
 
         # add results of full text search
         if search_matches is not None:
@@ -2326,14 +2374,14 @@
         return l
 
     def filter_sql(self, sql):
-        '''Return a list of the ids of the items in this class that match
+        """Return a list of the ids of the items in this class that match
         the SQL provided. The SQL is a complete "select" statement.
 
         The SQL select must include the item id as the first column.
 
         This function DOES NOT filter out retired items, add on a where
-        clause "__retired__ <> 1" if you don't want retired nodes.
-        '''
+        clause "__retired__=0" if you don't want retired nodes.
+        """
         if __debug__:
             start_t = time.time()
 
@@ -2345,20 +2393,20 @@
         return l
 
     def count(self):
-        '''Get the number of nodes in this class.
+        """Get the number of nodes in this class.
 
         If the returned integer is 'numnodes', the ids of all the nodes
         in this class run from 1 to numnodes, and numnodes+1 will be the
         id of the next node to be created in this class.
-        '''
+        """
         return self.db.countnodes(self.classname)
 
     # Manipulating properties:
     def getprops(self, protected=1):
-        '''Return a dictionary mapping property names to property objects.
+        """Return a dictionary mapping property names to property objects.
            If the "protected" flag is true, we include protected properties -
            those which may not be modified.
-        '''
+        """
         d = self.properties.copy()
         if protected:
             d['id'] = String()
@@ -2369,21 +2417,21 @@
         return d
 
     def addprop(self, **properties):
-        '''Add properties to this class.
+        """Add properties to this class.
 
         The keyword arguments in 'properties' must map names to property
         objects, or a TypeError is raised.  None of the keys in 'properties'
         may collide with the names of existing properties, or a ValueError
         is raised before any properties have been added.
-        '''
+        """
         for key in properties.keys():
             if self.properties.has_key(key):
                 raise ValueError, key
         self.properties.update(properties)
 
     def index(self, nodeid):
-        '''Add (or refresh) the node to search indexes
-        '''
+        """Add (or refresh) the node to search indexes
+        """
         # find all the String properties that have indexme
         for prop, propclass in self.getprops().items():
             if isinstance(propclass, String) and propclass.indexme:
@@ -2394,9 +2442,9 @@
     # import / export support
     #
     def export_list(self, propnames, nodeid):
-        ''' Export a node - generate a list of CSV-able data in the order
+        """ Export a node - generate a list of CSV-able data in the order
             specified by propnames for the given node.
-        '''
+        """
         properties = self.getprops()
         l = []
         for prop in propnames:
@@ -2416,15 +2464,15 @@
         return l
 
     def import_list(self, propnames, proplist):
-        ''' Import a node - all information including "id" is present and
+        """ Import a node - all information including "id" is present and
             should not be sanity checked. Triggers are not triggered. The
             journal should be initialised using the "creator" and "created"
             information.
 
             Return the nodeid of the node imported.
-        '''
+        """
         if self.db.journaltag is None:
-            raise DatabaseError, 'Database open read-only'
+            raise DatabaseError, _('Database open read-only')
         properties = self.getprops()
 
         # make the new node's property map
@@ -2493,17 +2541,17 @@
             # conversion (hello, sqlite)
             sql = 'update _%s set __retired__=%s where id=%s'%(self.classname,
                 self.db.arg, self.db.arg)
-            self.db.sql(sql, (1, newid))
+            self.db.sql(sql, (newid, newid))
         return newid
 
     def export_journals(self):
-        '''Export a class's journal - generate a list of lists of
+        """Export a class's journal - generate a list of lists of
         CSV-able data:
 
             nodeid, date, user, action, params
 
         No heading here - the columns are fixed.
-        '''
+        """
         properties = self.getprops()
         r = []
         for nodeid in self.getnodeids():
@@ -2528,14 +2576,17 @@
                             value = str(value)
                         export_data[propname] = value
                     params = export_data
+                elif action == 'create' and params:
+                    # old tracker with data stored in the create!
+                    params = {}
                 l = [nodeid, date, user, action, params]
                 r.append(map(repr, l))
         return r
 
     def import_journals(self, entries):
-        '''Import a class's journal.
+        """Import a class's journal.
 
-        Uses setjournal() to set the journal for each item.'''
+        Uses setjournal() to set the journal for each item."""
         properties = self.getprops()
         d = {}
         for l in entries:
@@ -2556,24 +2607,27 @@
                         pwd.unpack(value)
                         value = pwd
                     params[propname] = value
+            elif action == 'create' and params:
+                # old tracker with data stored in the create!
+                params = {}
             r.append((nodeid, date.Date(jdate), user, action, params))
 
         for nodeid, l in d.items():
             self.db.setjournal(self.classname, nodeid, l)
 
 class FileClass(hyperdb.FileClass, Class):
-    '''This class defines a large chunk of data. To support this, it has a
+    """This class defines a large chunk of data. To support this, it has a
        mandatory String property "content" which is typically saved off
        externally to the hyperdb.
 
        The default MIME type of this data is defined by the
        "default_mime_type" class attribute, which may be overridden by each
        node if the class defines a "type" String property.
-    '''
+    """
     def __init__(self, db, classname, **properties):
-        '''The newly-created class automatically includes the "content"
+        """The newly-created class automatically includes the "content"
         and "type" properties.
-        '''
+        """
         if not properties.has_key('content'):
             properties['content'] = hyperdb.String(indexme='yes')
         if not properties.has_key('type'):
@@ -2581,8 +2635,8 @@
         Class.__init__(self, db, classname, **properties)
 
     def create(self, **propvalues):
-        ''' snaffle the file propvalue and store in a file
-        '''
+        """ snaffle the file propvalue and store in a file
+        """
         # we need to fire the auditors now, or the content property won't
         # be in propvalues for the auditors to play with
         self.fireAuditors('create', None, propvalues)
@@ -2610,10 +2664,10 @@
         return newid
 
     def get(self, nodeid, propname, default=_marker, cache=1):
-        ''' Trap the content propname and get it from the file
+        """ Trap the content propname and get it from the file
 
         'cache' exists for backwards compatibility, and is not used.
-        '''
+        """
         poss_msg = 'Possibly a access right configuration problem.'
         if propname == 'content':
             try:
@@ -2627,21 +2681,9 @@
         else:
             return Class.get(self, nodeid, propname)
 
-    def getprops(self, protected=1):
-        '''In addition to the actual properties on the node, these methods
-        provide the "content" property. If the "protected" flag is true,
-        we include protected properties - those which may not be
-        modified.
-
-        Note that the content prop is indexed separately, hence no indexme.
-        '''
-        d = Class.getprops(self, protected=protected).copy()
-        d['content'] = hyperdb.String(indexme='yes')
-        return d
-
     def set(self, itemid, **propvalues):
-        ''' Snarf the "content" propvalue and update it in a file
-        '''
+        """ Snarf the "content" propvalue and update it in a file
+        """
         self.fireAuditors('set', itemid, propvalues)
         oldvalues = copy.deepcopy(self.db.getnode(self.classname, itemid))
 
@@ -2669,10 +2711,10 @@
         return propvalues
 
     def index(self, nodeid):
-        ''' Add (or refresh) the node to search indexes.
+        """ Add (or refresh) the node to search indexes.
 
         Use the content-type property for the content property.
-        '''
+        """
         # find all the String properties that have indexme
         for prop, propclass in self.getprops().items():
             if prop == 'content' and propclass.indexme:
@@ -2692,12 +2734,12 @@
 class IssueClass(Class, roundupdb.IssueClass):
     # Overridden methods:
     def __init__(self, db, classname, **properties):
-        '''The newly-created class automatically includes the "messages",
+        """The newly-created class automatically includes the "messages",
         "files", "nosy", and "superseder" properties.  If the 'properties'
         dictionary attempts to specify any of these properties or a
         "creation", "creator", "activity" or "actor" property, a ValueError
         is raised.
-        '''
+        """
         if not properties.has_key('title'):
             properties['title'] = hyperdb.String(indexme='yes')
         if not properties.has_key('messages'):

Modified: tracker/roundup-src/roundup/backends/sessions_dbm.py
==============================================================================
--- tracker/roundup-src/roundup/backends/sessions_dbm.py	(original)
+++ tracker/roundup-src/roundup/backends/sessions_dbm.py	Sun Mar  9 09:26:16 2008
@@ -1,4 +1,4 @@
-#$Id: sessions_dbm.py,v 1.7 2006/04/27 04:59:37 richard Exp $
+#$Id: sessions_dbm.py,v 1.9 2007/09/27 06:18:53 jpend Exp $
 """This module defines a very basic store that's used by the CGI interface
 to store session and one-time-key information.
 
@@ -8,6 +8,8 @@
 __docformat__ = 'restructuredtext'
 
 import anydbm, whichdb, os, marshal, time
+from roundup import hyperdb
+from roundup.i18n import _
 
 class BasicDatabase:
     ''' Provide a nice encapsulation of an anydbm store.
@@ -44,7 +46,8 @@
         if os.path.exists(path):
             db_type = whichdb.whichdb(path)
             if not db_type:
-                raise hyperdb.DatabaseError, "Couldn't identify database type"
+                raise hyperdb.DatabaseError, \
+                    _("Couldn't identify database type")
         elif os.path.exists(path+'.db'):
             # if the path ends in '.db', it's a dbm database, whether
             # anydbm says it's dbhash or not!
@@ -155,3 +158,4 @@
 class OneTimeKeys(BasicDatabase):
     name = 'otks'
 
+# vim: set sts ts=4 sw=4 et si :

Modified: tracker/roundup-src/roundup/backends/sessions_rdbms.py
==============================================================================
--- tracker/roundup-src/roundup/backends/sessions_rdbms.py	(original)
+++ tracker/roundup-src/roundup/backends/sessions_rdbms.py	Sun Mar  9 09:26:16 2008
@@ -1,4 +1,4 @@
-#$Id: sessions_rdbms.py,v 1.4 2006/04/27 04:03:11 richard Exp $
+#$Id: sessions_rdbms.py,v 1.7 2007/09/25 19:49:19 jpend Exp $
 """This module defines a very basic store that's used by the CGI interface
 to store session and one-time-key information.
 
@@ -77,7 +77,7 @@
             self.name, self.db.arg), (infoid,))
 
     def updateTimestamp(self, infoid):
-        ''' don't update every hit - once a minute should be OK '''
+        """ don't update every hit - once a minute should be OK """
         now = time.time()
         self.cursor.execute('''update %ss set %s_time=%s where %s_key=%s
             and %s_time < %s'''%(self.name, self.name, self.db.arg,
@@ -97,3 +97,4 @@
 class OneTimeKeys(BasicDatabase):
     name = 'otk'
 
+# vim: set et sts=4 sw=4 :

Modified: tracker/roundup-src/roundup/cgi/TranslationService.py
==============================================================================
--- tracker/roundup-src/roundup/cgi/TranslationService.py	(original)
+++ tracker/roundup-src/roundup/cgi/TranslationService.py	Sun Mar  9 09:26:16 2008
@@ -13,8 +13,8 @@
 #   translate(domain, msgid, mapping, context, target_language, default)
 #
 
-__version__ = "$Revision: 1.3 $"[11:-2]
-__date__ = "$Date: 2006/12/02 23:41:28 $"[7:-2]
+__version__ = "$Revision: 1.4 $"[11:-2]
+__date__ = "$Date: 2007/01/14 22:54:15 $"[7:-2]
 
 from roundup import i18n
 from roundup.cgi.PageTemplates import Expressions, PathIterator, TALES
@@ -35,6 +35,8 @@
         return _msg
 
     def gettext(self, msgid):
+        if not isinstance(msgid, unicode):
+            msgid = unicode(msgid, 'utf8')
         return self.ugettext(msgid).encode(self.OUTPUT_ENCODING)
 
     def ngettext(self, singular, plural, number):

Modified: tracker/roundup-src/roundup/cgi/actions.py
==============================================================================
--- tracker/roundup-src/roundup/cgi/actions.py	(original)
+++ tracker/roundup-src/roundup/cgi/actions.py	Sun Mar  9 09:26:16 2008
@@ -1,4 +1,4 @@
-#$Id: actions.py,v 1.62 2006/08/11 05:41:32 richard Exp $
+#$Id: actions.py,v 1.71 2007/09/20 23:44:58 jpend Exp $
 
 import re, cgi, StringIO, urllib, Cookie, time, random, csv, codecs
 
@@ -148,21 +148,16 @@
         """
         self.fakeFilterVars()
         queryname = self.getQueryName()
-    
+
         # editing existing query name?
-        old_queryname = ''
-        for key in ('@old-queryname', ':old-queryname'):
-            if self.form.has_key(key):
-                old_queryname = self.form[key].value.strip()
+        old_queryname = self.getFromForm('old-queryname')
 
         # handle saving the query params
         if queryname:
             # parse the environment and figure what the query _is_
             req = templating.HTMLRequest(self.client)
 
-            # The [1:] strips off the '?' character, it isn't part of the
-            # query string.
-            url = req.indexargs_url('', {})[1:]
+            url = self.getCurrentURL(req)
 
             key = self.db.query.getkey()
             if key:
@@ -247,12 +242,29 @@
 
             self.form.value.append(cgi.MiniFieldStorage('@filter', key))
 
-    def getQueryName(self):
-        for key in ('@queryname', ':queryname'):
+    def getCurrentURL(self, req):
+        """Get current URL for storing as a query.
+
+        Note: We are removing the first character from the current URL,
+        because the leading '?' is not part of the query string.
+
+        Implementation note:
+        But maybe the template should be part of the stored query:
+        template = self.getFromForm('template')
+        if template:
+            return req.indexargs_url('', {'@template' : template})[1:]
+        """
+        return req.indexargs_url('', {})[1:]
+
+    def getFromForm(self, name):
+        for key in ('@' + name, ':' + name):
             if self.form.has_key(key):
                 return self.form[key].value.strip()
         return ''
 
+    def getQueryName(self):
+        return self.getFromForm('queryname')
+
 class EditCSVAction(Action):
     name = 'edit'
     permissionType = 'Edit'
@@ -355,9 +367,11 @@
         deps = {}
         links = {}
         for cn, nodeid, propname, vlist in all_links:
-            if not all_props.has_key((cn, nodeid)):
+            numeric_id = int (nodeid or 0)
+            if not (numeric_id > 0 or all_props.has_key((cn, nodeid))):
                 # link item to link to doesn't (and won't) exist
                 continue
+
             for value in vlist:
                 if not all_props.has_key(value):
                     # link item to link to doesn't (and won't) exist
@@ -389,36 +403,33 @@
         m = []
         for needed in order:
             props = all_props[needed]
-            if not props:
-                # nothing to do
-                continue
             cn, nodeid = needed
-
-            if nodeid is not None and int(nodeid) > 0:
-                # make changes to the node
-                props = self._changenode(cn, nodeid, props)
-
-                # and some nice feedback for the user
-                if props:
-                    info = ', '.join(map(self._, props.keys()))
-                    m.append(
-                        self._('%(class)s %(id)s %(properties)s edited ok')
-                        % {'class':cn, 'id':nodeid, 'properties':info})
+            if props:
+                if nodeid is not None and int(nodeid) > 0:
+                    # make changes to the node
+                    props = self._changenode(cn, nodeid, props)
+
+                    # and some nice feedback for the user
+                    if props:
+                        info = ', '.join(map(self._, props.keys()))
+                        m.append(
+                            self._('%(class)s %(id)s %(properties)s edited ok')
+                            % {'class':cn, 'id':nodeid, 'properties':info})
+                    else:
+                        m.append(self._('%(class)s %(id)s - nothing changed')
+                            % {'class':cn, 'id':nodeid})
                 else:
-                    m.append(self._('%(class)s %(id)s - nothing changed')
-                        % {'class':cn, 'id':nodeid})
-            else:
-                assert props
+                    assert props
 
-                # make a new node
-                newid = self._createnode(cn, props)
-                if nodeid is None:
-                    self.nodeid = newid
-                nodeid = newid
-
-                # and some nice feedback for the user
-                m.append(self._('%(class)s %(id)s created')
-                    % {'class':cn, 'id':newid})
+                    # make a new node
+                    newid = self._createnode(cn, props)
+                    if nodeid is None:
+                        self.nodeid = newid
+                    nodeid = newid
+
+                    # and some nice feedback for the user
+                    m.append(self._('%(class)s %(id)s created')
+                        % {'class':cn, 'id':newid})
 
             # fill in new ids in links
             if links.has_key(needed):
@@ -760,7 +771,7 @@
         except (ValueError, KeyError), message:
             self.client.error_message.append(str(message))
             return
-        self.finishRego()
+        return self.finishRego()
 
 class RegisterAction(RegoCommon, EditCommon):
     name = 'register'

Modified: tracker/roundup-src/roundup/cgi/client.py
==============================================================================
--- tracker/roundup-src/roundup/cgi/client.py	(original)
+++ tracker/roundup-src/roundup/cgi/client.py	Sun Mar  9 09:26:16 2008
@@ -1,4 +1,4 @@
-# $Id: client.py,v 1.229 2006/11/15 06:27:15 a1s Exp $
+# $Id: client.py,v 1.238 2007/09/22 21:20:57 jpend Exp $
 
 """WWW request handler (also used in the stand-alone server).
 """
@@ -7,6 +7,7 @@
 import base64, binascii, cgi, codecs, mimetypes, os
 import random, re, rfc822, stat, time, urllib, urlparse
 import Cookie, socket, errno
+from Cookie import CookieError, BaseCookie, SimpleCookie
 
 from roundup import roundupdb, date, hyperdb, password
 from roundup.cgi import templating, cgitb, TranslationService
@@ -46,12 +47,47 @@
         return match.group(1)
     return '&lt;%s&gt;'%match.group(2)
 
+
 error_message = ""'''<html><head><title>An error has occurred</title></head>
 <body><h1>An error has occurred</h1>
 <p>A problem was encountered processing your request.
 The tracker maintainers have been notified of the problem.</p>
 </body></html>'''
 
+
+class LiberalCookie(SimpleCookie):
+    ''' Python's SimpleCookie throws an exception if the cookie uses invalid
+        syntax.  Other applications on the same server may have done precisely
+        this, preventing roundup from working through no fault of roundup.
+        Numerous other python apps have run into the same problem:
+
+        trac: http://trac.edgewall.org/ticket/2256
+        mailman: http://bugs.python.org/issue472646
+
+        This particular implementation comes from trac's solution to the
+        problem. Unfortunately it requires some hackery in SimpleCookie's
+        internals to provide a more liberal __set method.
+    '''
+    def load(self, rawdata, ignore_parse_errors=True):
+        if ignore_parse_errors:
+            self.bad_cookies = []
+            self._BaseCookie__set = self._loose_set
+        SimpleCookie.load(self, rawdata)
+        if ignore_parse_errors:
+            self._BaseCookie__set = self._strict_set
+            for key in self.bad_cookies:
+                del self[key]
+
+    _strict_set = BaseCookie._BaseCookie__set
+
+    def _loose_set(self, key, real_value, coded_value):
+        try:
+            self._strict_set(key, real_value, coded_value)
+        except CookieError:
+            self.bad_cookies.append(key)
+            dict.__setitem__(self, key, None)
+
+
 class Client:
     '''Instantiate to handle one CGI request.
 
@@ -181,7 +217,9 @@
         self.charset = self.STORAGE_CHARSET
 
         # parse cookies (used in charset and session lookups)
-        self.cookie = Cookie.SimpleCookie(self.env.get('HTTP_COOKIE', ''))
+        # use our own LiberalCookie to handle bad apps on the same
+        # server that have set cookies that are out of spec
+        self.cookie = LiberalCookie(self.env.get('HTTP_COOKIE', ''))
 
         self.user = None
         self.userid = None
@@ -287,7 +325,12 @@
             self.additional_headers['Expires'] = rfc822.formatdate(date)
 
             # render the content
-            self.write_html(self.renderContext())
+            try:
+                self.write_html(self.renderContext())
+            except IOError:
+                # IOErrors here are due to the client disconnecting before
+                # recieving the reply.
+                pass
 
         except SeriousError, message:
             self.write_html(str(message))
@@ -319,9 +362,17 @@
             self.template = ''
             self.error_message.append(message)
             self.write_html(self.renderContext())
-        except NotFound:
-            # pass through
-            raise
+        except NotFound, e:
+            self.response_code = 404
+            self.template = '404'
+            try:
+                cl = self.db.getclass(self.classname)
+                self.write_html(self.renderContext())
+            except KeyError:
+                # we can't map the URL to a class we know about
+                # reraise the NotFound and let roundup_server
+                # handle it
+                raise NotFound, e
         except FormError, e:
             self.error_message.append(self._('Form Error: ') + str(e))
             self.write_html(self.renderContext())
@@ -734,8 +785,7 @@
         # spit out headers
         self.additional_headers['Content-Type'] = mime_type
         self.additional_headers['Content-Length'] = str(len(content))
-        lmt = rfc822.formatdate(lmt)
-        self.additional_headers['Last-Modified'] = lmt
+        self.additional_headers['Last-Modified'] = rfc822.formatdate(lmt)
 
         ims = None
         # see if there's an if-modified-since...
@@ -868,7 +918,13 @@
         try:
             call(*args, **kwargs)
         except socket.error, err:
-            if err.errno not in self.IGNORE_NET_ERRORS:
+            err_errno = getattr (err, 'errno', None)
+            if err_errno is None:
+                try:
+                    err_errno = err[0]
+                except TypeError:
+                    pass
+            if err_errno not in self.IGNORE_NET_ERRORS:
                 raise
 
     def write(self, content):
@@ -880,8 +936,9 @@
     def write_html(self, content):
         if not self.headers_done:
             # at this point, we are sure about Content-Type
-            self.additional_headers['Content-Type'] = \
-                'text/html; charset=%s' % self.charset
+            if not self.additional_headers.has_key('Content-Type'):
+                self.additional_headers['Content-Type'] = \
+                    'text/html; charset=%s' % self.charset
             self.header()
 
         if self.env['REQUEST_METHOD'] == 'HEAD':

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	Sun Mar  9 09:26:16 2008
@@ -541,7 +541,7 @@
                         cl = self.db.classes[self.classname]
                         if cl.get(nodeid, entry) is not None:
                             required.remove(entry)
-            
+
             # any required values not present?
             if not required:
                 continue

Modified: tracker/roundup-src/roundup/cgi/templating.py
==============================================================================
--- tracker/roundup-src/roundup/cgi/templating.py	(original)
+++ tracker/roundup-src/roundup/cgi/templating.py	Sun Mar  9 09:26:16 2008
@@ -3,7 +3,7 @@
 """Implements the API used in the HTML templating for the web interface.
 """
 
-todo = '''
+todo = """
 - Most methods should have a "default" arg to supply a value
   when none appears in the hyperdb or request.
 - Multilink property additions: change_note and new_upload
@@ -11,11 +11,11 @@
 - NumberHTMLProperty should support numeric operations
 - LinkHTMLProperty should handle comparisons to strings (cf. linked name)
 - HTMLRequest.default(self, sort, group, filter, columns, **filterspec):
-  """Set the request's view arguments to the given values when no
+  '''Set the request's view arguments to the given values when no
      values are found in the CGI environment.
-  """
+  '''
 - have menu() methods accept filtering arguments
-'''
+"""
 
 __docformat__ = 'restructuredtext'
 
@@ -42,6 +42,10 @@
         import StructuredText
     except ImportError:
         StructuredText = None
+try:
+    from docutils.core import publish_parts as ReStructuredText
+except ImportError:
+    ReStructuredText = None
 
 # bring in the templating support
 from roundup.cgi.PageTemplates import PageTemplate, GlobalTranslationService
@@ -75,8 +79,8 @@
             'action': self.action, 'class': self.klass}
 
 def find_template(dir, name, view):
-    ''' Find a template in the nominated dir
-    '''
+    """ Find a template in the nominated dir
+    """
     # find the source
     if view:
         filename = '%s.%s'%(name, view)
@@ -122,8 +126,8 @@
         self.dir = dir
 
     def precompileTemplates(self):
-        ''' Go through a directory and precompile all the templates therein
-        '''
+        """ Go through a directory and precompile all the templates therein
+        """
         for filename in os.listdir(self.dir):
             # skip subdirs
             if os.path.isdir(filename):
@@ -147,7 +151,7 @@
                 self.get(filename, None)
 
     def get(self, name, extension=None):
-        ''' Interface to get a template, possibly loading a compiled template.
+        """ Interface to get a template, possibly loading a compiled template.
 
             "name" and "extension" indicate the template we're after, which in
             most cases will be "name.extension". If "extension" is None, then
@@ -155,7 +159,7 @@
 
             If the file "name.extension" doesn't exist, we look for
             "_generic.extension" as a fallback.
-        '''
+        """
         # default the name to "home"
         if name is None:
             name = 'home'
@@ -290,12 +294,12 @@
     return c
 
 class RoundupPageTemplate(PageTemplate.PageTemplate):
-    '''A Roundup-specific PageTemplate.
+    """A Roundup-specific PageTemplate.
 
     Interrogate the client to set up Roundup-specific template variables
     to be available.  See 'context' function for the list of variables.
 
-    '''
+    """
 
     # 06-jun-2004 [als] i am not sure if this method is used yet
     def getContext(self, client, classname, request):
@@ -327,8 +331,8 @@
         return '<Roundup PageTemplate %r>'%self.id
 
 class HTMLDatabase:
-    ''' Return HTMLClasses for valid class fetches
-    '''
+    """ Return HTMLClasses for valid class fetches
+    """
     def __init__(self, client):
         self._client = client
         self._ = client._
@@ -362,26 +366,35 @@
             m.append(HTMLClass(self._client, item))
         return m
 
-def lookupIds(db, prop, ids, fail_ok=0, num_re=re.compile('^-?\d+$')):
-    ''' "fail_ok" should be specified if we wish to pass through bad values
+num_re = re.compile('^-?\d+$')
+
+def lookupIds(db, prop, ids, fail_ok=0, num_re=num_re, do_lookup=True):
+    """ "fail_ok" should be specified if we wish to pass through bad values
         (most likely form values that we wish to represent back to the user)
-    '''
+        "do_lookup" is there for preventing lookup by key-value (if we
+        know that the value passed *is* an id)
+    """
     cl = db.getclass(prop.classname)
     l = []
     for entry in ids:
-        try:
-            l.append(cl.lookup(entry))
-        except (TypeError, KeyError):
-            # if fail_ok, ignore lookup error
-            # otherwise entry must be existing object id rather than key value
-            if fail_ok or num_re.match(entry):
-                l.append(entry)
+        if do_lookup:
+            try:
+                item = cl.lookup(entry)
+            except (TypeError, KeyError):
+                pass
+            else:
+                l.append(item)
+                continue
+        # if fail_ok, ignore lookup error
+        # otherwise entry must be existing object id rather than key value
+        if fail_ok or num_re.match(entry):
+            l.append(entry)
     return l
 
-def lookupKeys(linkcl, key, ids, num_re=re.compile('^-?\d+$')):
-    ''' Look up the "key" values for "ids" list - though some may already
+def lookupKeys(linkcl, key, ids, num_re=num_re):
+    """ Look up the "key" values for "ids" list - though some may already
     be key values, not ids.
-    '''
+    """
     l = []
     for entry in ids:
         if num_re.match(entry):
@@ -409,7 +422,7 @@
 
 def input_html4(**attrs):
     """Generate an 'input' (html4) element with given attributes"""
-    _set_input_default_args(attrs) 
+    _set_input_default_args(attrs)
     return '<input %s>'%' '.join(['%s="%s"'%(k,cgi.escape(str(v), True))
         for k,v in attrs.items()])
 
@@ -420,7 +433,7 @@
         for k,v in attrs.items()])
 
 class HTMLInputMixin:
-    ''' requires a _client property '''
+    """ requires a _client property """
     def __init__(self):
         html_version = 'html4'
         if hasattr(self._client.instance.config, 'HTML_VERSION'):
@@ -445,25 +458,25 @@
 class HTMLPermissions:
 
     def view_check(self):
-        ''' Raise the Unauthorised exception if the user's not permitted to
+        """ Raise the Unauthorised exception if the user's not permitted to
             view this class.
-        '''
+        """
         if not self.is_view_ok():
             raise Unauthorised("view", self._classname,
                 translator=self._client.translator)
 
     def edit_check(self):
-        ''' Raise the Unauthorised exception if the user's not permitted to
+        """ Raise the Unauthorised exception if the user's not permitted to
             edit items of this class.
-        '''
+        """
         if not self.is_edit_ok():
             raise Unauthorised("edit", self._classname,
                 translator=self._client.translator)
 
 
 class HTMLClass(HTMLInputMixin, HTMLPermissions):
-    ''' Accesses through a class (either through *class* or *db.<classname>*)
-    '''
+    """ Accesses through a class (either through *class* or *db.<classname>*)
+    """
     def __init__(self, client, classname, anonymous=0):
         self._client = client
         self._ = client._
@@ -479,29 +492,28 @@
         HTMLInputMixin.__init__(self)
 
     def is_edit_ok(self):
-        ''' Is the user allowed to Create the current class?
-        '''
+        """ Is the user allowed to Create the current class?
+        """
         return self._db.security.hasPermission('Create', self._client.userid,
             self._classname)
 
     def is_view_ok(self):
-        ''' Is the user allowed to View the current class?
-        '''
+        """ Is the user allowed to View the current class?
+        """
         return self._db.security.hasPermission('View', self._client.userid,
             self._classname)
 
     def is_only_view_ok(self):
-        ''' Is the user only allowed to View (ie. not Create) the current class?
-        '''
+        """ Is the user only allowed to View (ie. not Create) the current class?
+        """
         return self.is_view_ok() and not self.is_edit_ok()
 
     def __repr__(self):
         return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
 
     def __getitem__(self, item):
-        ''' return an HTMLProperty instance
-        '''
-       #print 'HTMLClass.getitem', (self, item)
+        """ return an HTMLProperty instance
+        """
 
         # we don't exist
         if item == 'id':
@@ -543,19 +555,19 @@
         raise KeyError, item
 
     def __getattr__(self, attr):
-        ''' convenience access '''
+        """ convenience access """
         try:
             return self[attr]
         except KeyError:
             raise AttributeError, attr
 
     def designator(self):
-        ''' Return this class' designator (classname) '''
+        """ Return this class' designator (classname) """
         return self._classname
 
-    def getItem(self, itemid, num_re=re.compile('^-?\d+$')):
-        ''' Get an item of this class by its item id.
-        '''
+    def getItem(self, itemid, num_re=num_re):
+        """ Get an item of this class by its item id.
+        """
         # make sure we're looking at an itemid
         if not isinstance(itemid, type(1)) and not num_re.match(itemid):
             itemid = self._klass.lookup(itemid)
@@ -563,8 +575,8 @@
         return HTMLItem(self._client, self.classname, itemid)
 
     def properties(self, sort=1):
-        ''' Return HTMLProperty for all of this class' properties.
-        '''
+        """ Return HTMLProperty for all of this class' properties.
+        """
         l = []
         for name, prop in self._props.items():
             for klass, htmlklass in propclasses:
@@ -580,8 +592,8 @@
         return l
 
     def list(self, sort_on=None):
-        ''' List all items in this class.
-        '''
+        """ List all items in this class.
+        """
         # get the list and sort it nicely
         l = self._klass.list()
         sortfunc = make_sort_function(self._db, self._classname, sort_on)
@@ -597,8 +609,8 @@
         return l
 
     def csv(self):
-        ''' Return the items of this class as a chunk of CSV text.
-        '''
+        """ Return the items of this class as a chunk of CSV text.
+        """
         props = self.propnames()
         s = StringIO.StringIO()
         writer = csv.writer(s)
@@ -617,18 +629,18 @@
         return s.getvalue()
 
     def propnames(self):
-        ''' Return the list of the names of the properties of this class.
-        '''
+        """ Return the list of the names of the properties of this class.
+        """
         idlessprops = self._klass.getprops(protected=0).keys()
         idlessprops.sort()
         return ['id'] + idlessprops
 
     def filter(self, request=None, filterspec={}, sort=[], group=[]):
-        ''' Return a list of items from this class, filtered and sorted
+        """ Return a list of items from this class, filtered and sorted
             by the current requested filterspec/filter/sort/group args
 
             "request" takes precedence over the other three arguments.
-        '''
+        """
         if request is not None:
             filterspec = request.filterspec
             sort = request.sort
@@ -645,7 +657,7 @@
     def classhelp(self, properties=None, label=''"(list)", width='500',
             height='400', property='', form='itemSynopsis',
             pagesize=50, inputtype="checkbox", sort=None, filter=None):
-        '''Pop up a javascript window with class help
+        """Pop up a javascript window with class help
 
         This generates a link to a popup window which displays the
         properties indicated by "properties" of the class named by
@@ -674,7 +686,7 @@
         If the "form" arg is given, it's passed through to the
         javascript help_window function. - it's the name of the form
         the "property" belongs to.
-        '''
+        """
         if properties is None:
             properties = self._klass.getprops(protected=0).keys()
             properties.sort()
@@ -711,15 +723,15 @@
         return '<a class="classhelp" href="%s" onclick="%s">%s</a>' % \
                (help_url, onclick, self._(label))
 
-    def submit(self, label=''"Submit New Entry"):
-        ''' Generate a submit button (and action hidden element)
+    def submit(self, label=''"Submit New Entry", action="new"):
+        """ Generate a submit button (and action hidden element)
 
         Generate nothing if we're not editable.
-        '''
+        """
         if not self.is_edit_ok():
             return ''
 
-        return self.input(type="hidden", name="@action", value="new") + \
+        return self.input(type="hidden", name="@action", value=action) + \
             '\n' + \
             self.input(type="submit", name="submit_button", value=self._(label))
 
@@ -729,8 +741,8 @@
         return self._('New node - no history')
 
     def renderWith(self, name, **kwargs):
-        ''' Render this class with the given template.
-        '''
+        """ Render this class with the given template.
+        """
         # create a new request and override the specified args
         req = HTMLRequest(self._client)
         req.classname = self.classname
@@ -747,8 +759,8 @@
         return pt.render(self._client, self.classname, req, **args)
 
 class _HTMLItem(HTMLInputMixin, HTMLPermissions):
-    ''' Accesses through an *item*
-    '''
+    """ Accesses through an *item*
+    """
     def __init__(self, client, classname, nodeid, anonymous=0):
         self._client = client
         self._db = client.db
@@ -763,22 +775,22 @@
         HTMLInputMixin.__init__(self)
 
     def is_edit_ok(self):
-        ''' Is the user allowed to Edit the current class?
-        '''
+        """ Is the user allowed to Edit the current class?
+        """
         return self._db.security.hasPermission('Edit', self._client.userid,
             self._classname, itemid=self._nodeid)
 
     def is_view_ok(self):
-        ''' Is the user allowed to View the current class?
-        '''
+        """ Is the user allowed to View the current class?
+        """
         if self._db.security.hasPermission('View', self._client.userid,
                 self._classname, itemid=self._nodeid):
             return 1
         return self.is_edit_ok()
 
     def is_only_view_ok(self):
-        ''' Is the user only allowed to View (ie. not Edit) the current class?
-        '''
+        """ Is the user only allowed to View (ie. not Edit) the current class?
+        """
         return self.is_view_ok() and not self.is_edit_ok()
 
     def __repr__(self):
@@ -786,11 +798,10 @@
             self._nodeid)
 
     def __getitem__(self, item):
-        ''' return an HTMLProperty instance
+        """ return an HTMLProperty instance
             this now can handle transitive lookups where item is of the
             form x.y.z
-        '''
-        #print 'HTMLItem.getitem', (self, item)
+        """
         if item == 'id':
             return self._nodeid
 
@@ -827,7 +838,7 @@
         raise KeyError, item
 
     def __getattr__(self, attr):
-        ''' convenience access to properties '''
+        """ convenience access to properties """
         try:
             return self[attr]
         except KeyError:
@@ -841,19 +852,19 @@
         """Is this item retired?"""
         return self._klass.is_retired(self._nodeid)
 
-    def submit(self, label=''"Submit Changes"):
+    def submit(self, label=''"Submit Changes", action="edit"):
         """Generate a submit button.
 
         Also sneak in the lastactivity and action hidden elements.
         """
         return self.input(type="hidden", name="@lastactivity",
             value=self.activity.local(0)) + '\n' + \
-            self.input(type="hidden", name="@action", value="edit") + '\n' + \
+            self.input(type="hidden", name="@action", value=action) + '\n' + \
             self.input(type="submit", name="submit_button", value=self._(label))
 
     def journal(self, direction='descending'):
-        ''' Return a list of HTMLJournalEntry instances.
-        '''
+        """ Return a list of HTMLJournalEntry instances.
+        """
         # XXX do this
         return []
 
@@ -1091,8 +1102,8 @@
         return '\n'.join(l)
 
     def renderQueryForm(self):
-        ''' Render this item, which is a query, as a search form.
-        '''
+        """ Render this item, which is a query, as a search form.
+        """
         # create a new request and override the specified args
         req = HTMLRequest(self._client)
         req.classname = self._klass.get(self._nodeid, 'klass')
@@ -1107,9 +1118,9 @@
         return pt.render(self._client, req.classname, req)
 
     def download_url(self):
-        ''' Assume that this item is a FileClass and that it has a name
+        """ Assume that this item is a FileClass and that it has a name
         and content. Construct a URL for the download of the content.
-        '''
+        """
         name = self._klass.get(self._nodeid, 'name')
         url = '%s%s/%s'%(self._classname, self._nodeid, name)
         return urllib.quote(url)
@@ -1138,23 +1149,23 @@
                 for key, value in query.items()])
 
 class _HTMLUser(_HTMLItem):
-    '''Add ability to check for permissions on users.
-    '''
+    """Add ability to check for permissions on users.
+    """
     _marker = []
     def hasPermission(self, permission, classname=_marker,
             property=None, itemid=None):
-        '''Determine if the user has the Permission.
+        """Determine if the user has the Permission.
 
         The class being tested defaults to the template's class, but may
         be overidden for this test by suppling an alternate classname.
-        '''
+        """
         if classname is self._marker:
             classname = self._client.classname
         return self._db.security.hasPermission(permission,
             self._nodeid, classname, property, itemid)
 
     def hasRole(self, rolename):
-        '''Determine whether the user has the Role.'''
+        """Determine whether the user has the Role."""
         roles = self._db.user.get(self._nodeid, 'roles').split(',')
         for role in roles:
             if role.strip() == rolename: return True
@@ -1167,7 +1178,7 @@
         return _HTMLItem(client, classname, nodeid, anonymous)
 
 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
-    ''' String, Number, Date, Interval HTMLProperty
+    """ String, Number, Date, Interval HTMLProperty
 
         Has useful attributes:
 
@@ -1175,7 +1186,7 @@
          _value the value of the property if any
 
         A wrapper object which may be stringified for the plain() behaviour.
-    '''
+    """
     def __init__(self, client, classname, nodeid, prop, name, value,
             anonymous=0):
         self._client = client
@@ -1208,14 +1219,14 @@
         return not not self._value
 
     def isset(self):
-        '''Is my _value not None?'''
+        """Is my _value not None?"""
         return self._value is not None
 
     def is_edit_ok(self):
-        '''Should the user be allowed to use an edit form field for this
+        """Should the user be allowed to use an edit form field for this
         property. Check "Create" for new items, or "Edit" for existing
         ones.
-        '''
+        """
         if self._nodeid:
             return self._db.security.hasPermission('Edit', self._client.userid,
                 self._classname, self._name, self._nodeid)
@@ -1223,8 +1234,8 @@
             self._classname, self._name)
 
     def is_view_ok(self):
-        ''' Is the user allowed to View the current class?
-        '''
+        """ Is the user allowed to View the current class?
+        """
         if self._db.security.hasPermission('View', self._client.userid,
                 self._classname, self._name, self._nodeid):
             return 1
@@ -1234,6 +1245,19 @@
     hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+[\w/])|'
                           r'(?P<email>[-+=%/\w\.]+@[\w\.\-]+)|'
                           r'(?P<item>(?P<class>[A-Za-z_]+)(\s*)(?P<id>\d+)))')
+    def _hyper_repl_item(self,match,replacement):
+        item = match.group('item')
+        cls = match.group('class').lower()
+        id = match.group('id')
+        try:
+            # make sure cls is a valid tracker classname
+            cl = self._db.getclass(cls)
+            if not cl.hasnode(id):
+                return item
+            return replacement % locals()
+        except KeyError:
+            return item
+
     def _hyper_repl(self, match):
         if match.group('url'):
             s = match.group('url')
@@ -1242,29 +1266,30 @@
             s = match.group('email')
             return '<a href="mailto:%s">%s</a>'%(s, s)
         else:
-            s = match.group('item')
-            s1 = match.group('class').lower()
-            s2 = match.group('id')
-            try:
-                # make sure s1 is a valid tracker classname
-                cl = self._db.getclass(s1)
-                if not cl.hasnode(s2):
-                    return s
-                return '<a href="%s%s">%s</a>'%(s1, s2, s)
-            except KeyError:
-                return s
+            return self._hyper_repl_item(match,
+                '<a href="%(cls)s%(id)s">%(item)s</a>')
+
+    def _hyper_repl_rst(self, match):
+        if match.group('url'):
+            s = match.group('url')
+            return '`%s <%s>`_'%(s, s)
+        elif match.group('email'):
+            s = match.group('email')
+            return '`%s <mailto:%s>`_'%(s, s)
+        else:
+            return self._hyper_repl_item(match,'`%(item)s <%(cls)s%(id)s>`_')
 
     def hyperlinked(self):
-        ''' Render a "hyperlinked" version of the text '''
+        """ Render a "hyperlinked" version of the text """
         return self.plain(hyperlink=1)
 
     def plain(self, escape=0, hyperlink=0):
-        '''Render a "plain" representation of the property
+        """Render a "plain" representation of the property
 
         - "escape" turns on/off HTML quoting
         - "hyperlink" turns on/off in-text hyperlinking of URLs, email
           addresses and designators
-        '''
+        """
         if not self.is_view_ok():
             return self._('[hidden]')
 
@@ -1282,7 +1307,7 @@
         return s
 
     def wrapped(self, escape=1, hyperlink=1):
-        '''Render a "wrapped" representation of the property.
+        """Render a "wrapped" representation of the property.
 
         We wrap long lines at 80 columns on the nearest whitespace. Lines
         with no whitespace are not broken to force wrapping.
@@ -1293,7 +1318,7 @@
         - "escape" turns on/off HTML quoting
         - "hyperlink" turns on/off in-text hyperlinking of URLs, email
           addresses and designators
-        '''
+        """
         if not self.is_view_ok():
             return self._('[hidden]')
 
@@ -1310,10 +1335,10 @@
         return s
 
     def stext(self, escape=0, hyperlink=1):
-        ''' Render the value of the property as StructuredText.
+        """ Render the value of the property as StructuredText.
 
             This requires the StructureText module to be installed separately.
-        '''
+        """
         if not self.is_view_ok():
             return self._('[hidden]')
 
@@ -1322,11 +1347,27 @@
             return s
         return StructuredText(s,level=1,header=0)
 
+    def rst(self, hyperlink=1):
+        """ Render the value of the property as ReStructuredText.
+
+            This requires docutils to be installed separately.
+        """
+        if not self.is_view_ok():
+            return self._('[hidden]')
+
+        if not ReStructuredText:
+            return self.plain(escape=0, hyperlink=hyperlink)
+        s = self.plain(escape=0, hyperlink=0)
+        if hyperlink:
+            s = self.hyper_re.sub(self._hyper_repl_rst, s)
+        return ReStructuredText(s, writer_name="html")["body"].encode("utf-8",
+            "replace")
+
     def field(self, **kwargs):
-        ''' Render the property as a field in HTML.
+        """ Render the property as a field in HTML.
 
             If not editable, just display the value via plain().
-        '''
+        """
         if not self.is_edit_ok():
             return self.plain()
 
@@ -1338,11 +1379,11 @@
         kwargs.update({"name": self._formname, "value": value})
         return self.input(**kwargs)
 
-    def multiline(self, escape=0, rows=5, cols=40):
-        ''' Render a multiline form edit field for the property.
+    def multiline(self, escape=0, rows=5, cols=40, **kwargs):
+        """ Render a multiline form edit field for the property.
 
             If not editable, just display the plain() value in a <pre> tag.
-        '''
+        """
         if not self.is_edit_ok():
             return '<pre>%s</pre>'%self.plain()
 
@@ -1353,13 +1394,15 @@
 
             value = '&quot;'.join(value.split('"'))
         name = self._formname
-        return ('<textarea name="%(name)s" id="%(name)s"'
+        passthrough_args = ' '.join(['%s="%s"' % (k, cgi.escape(str(v), True))
+            for k,v in kwargs.items()])
+        return ('<textarea %(passthrough_args)s name="%(name)s" id="%(name)s"'
                 ' rows="%(rows)s" cols="%(cols)s">'
                  '%(value)s</textarea>') % locals()
 
     def email(self, escape=1):
-        ''' Render the value of the property as an obscured email address
-        '''
+        """ Render the value of the property as an obscured email address
+        """
         if not self.is_view_ok():
             return self._('[hidden]')
 
@@ -1381,8 +1424,8 @@
 
 class PasswordHTMLProperty(HTMLProperty):
     def plain(self):
-        ''' Render a "plain" representation of the property
-        '''
+        """ Render a "plain" representation of the property
+        """
         if not self.is_view_ok():
             return self._('[hidden]')
 
@@ -1391,22 +1434,22 @@
         return self._('*encrypted*')
 
     def field(self, size=30):
-        ''' Render a form edit field for the property.
+        """ Render a form edit field for the property.
 
             If not editable, just display the value via plain().
-        '''
+        """
         if not self.is_edit_ok():
             return self.plain()
 
         return self.input(type="password", name=self._formname, size=size)
 
     def confirm(self, size=30):
-        ''' Render a second form edit field for the property, used for
+        """ Render a second form edit field for the property, used for
             confirmation that the user typed the password correctly. Generates
             a field with name "@confirm at name".
 
             If not editable, display nothing.
-        '''
+        """
         if not self.is_edit_ok():
             return ''
 
@@ -1417,8 +1460,8 @@
 
 class NumberHTMLProperty(HTMLProperty):
     def plain(self):
-        ''' Render a "plain" representation of the property
-        '''
+        """ Render a "plain" representation of the property
+        """
         if not self.is_view_ok():
             return self._('[hidden]')
 
@@ -1428,10 +1471,10 @@
         return str(self._value)
 
     def field(self, size=30):
-        ''' Render a form edit field for the property.
+        """ Render a form edit field for the property.
 
             If not editable, just display the value via plain().
-        '''
+        """
         if not self.is_edit_ok():
             return self.plain()
 
@@ -1442,20 +1485,20 @@
         return self.input(name=self._formname, value=value, size=size)
 
     def __int__(self):
-        ''' Return an int of me
-        '''
+        """ Return an int of me
+        """
         return int(self._value)
 
     def __float__(self):
-        ''' Return a float of me
-        '''
+        """ Return a float of me
+        """
         return float(self._value)
 
 
 class BooleanHTMLProperty(HTMLProperty):
     def plain(self):
-        ''' Render a "plain" representation of the property
-        '''
+        """ Render a "plain" representation of the property
+        """
         if not self.is_view_ok():
             return self._('[hidden]')
 
@@ -1464,10 +1507,10 @@
         return self._value and self._("Yes") or self._("No")
 
     def field(self):
-        ''' Render a form edit field for the property
+        """ Render a form edit field for the property
 
             If not editable, just display the value via plain().
-        '''
+        """
         if not self.is_edit_ok():
             return self.plain()
 
@@ -1507,8 +1550,8 @@
             self._offset = self._prop.offset (self._db)
 
     def plain(self):
-        ''' Render a "plain" representation of the property
-        '''
+        """ Render a "plain" representation of the property
+        """
         if not self.is_view_ok():
             return self._('[hidden]')
 
@@ -1521,11 +1564,11 @@
         return str(self._value.local(offset))
 
     def now(self, str_interval=None):
-        ''' Return the current time.
+        """ Return the current time.
 
             This is useful for defaulting a new value. Returns a
             DateHTMLProperty.
-        '''
+        """
         if not self.is_view_ok():
             return self._('[hidden]')
 
@@ -1546,7 +1589,7 @@
             self._prop, self._formname, ret)
 
     def field(self, size=30, default=None, format=_marker, popcal=True):
-        '''Render a form edit field for the property
+        """Render a form edit field for the property
 
         If not editable, just display the value via plain().
 
@@ -1554,7 +1597,7 @@
         Default=yes.
 
         The format string is a standard python strftime format string.
-        '''
+        """
         if not self.is_edit_ok():
             if format is self._marker:
                 return self.plain()
@@ -1568,7 +1611,7 @@
                 raw_value = None
             else:
                 if isinstance(default, basestring):
-                    raw_value = Date(default, translator=self._client)
+                    raw_value = date.Date(default, translator=self._client)
                 elif isinstance(default, date.Date):
                     raw_value = default
                 elif isinstance(default, DateHTMLProperty):
@@ -1606,10 +1649,10 @@
         return s
 
     def reldate(self, pretty=1):
-        ''' Render the interval between the date and now.
+        """ Render the interval between the date and now.
 
             If the "pretty" flag is true, then make the display pretty.
-        '''
+        """
         if not self.is_view_ok():
             return self._('[hidden]')
 
@@ -1623,13 +1666,13 @@
         return str(interval)
 
     def pretty(self, format=_marker):
-        ''' Render the date in a pretty format (eg. month names, spaces).
+        """ Render the date in a pretty format (eg. month names, spaces).
 
             The format string is a standard python strftime format string.
             Note that if the day is zero, and appears at the start of the
             string, then it'll be stripped from the output. This is handy
             for the situation when a date only specifies a month and a year.
-        '''
+        """
         if not self.is_view_ok():
             return self._('[hidden]')
 
@@ -1646,8 +1689,8 @@
             return self._value.local(offset).pretty()
 
     def local(self, offset):
-        ''' Return the date/time as a local (timezone offset) date/time.
-        '''
+        """ Return the date/time as a local (timezone offset) date/time.
+        """
         if not self.is_view_ok():
             return self._('[hidden]')
 
@@ -1678,8 +1721,8 @@
             self._value.setTranslator(self._client.translator)
 
     def plain(self):
-        ''' Render a "plain" representation of the property
-        '''
+        """ Render a "plain" representation of the property
+        """
         if not self.is_view_ok():
             return self._('[hidden]')
 
@@ -1688,18 +1731,18 @@
         return str(self._value)
 
     def pretty(self):
-        ''' Render the interval in a pretty format (eg. "yesterday")
-        '''
+        """ Render the interval in a pretty format (eg. "yesterday")
+        """
         if not self.is_view_ok():
             return self._('[hidden]')
 
         return self._value.pretty()
 
     def field(self, size=30):
-        ''' Render a form edit field for the property
+        """ Render a form edit field for the property
 
             If not editable, just display the value via plain().
-        '''
+        """
         if not self.is_edit_ok():
             return self.plain()
 
@@ -1710,7 +1753,7 @@
         return self.input(name=self._formname, value=value, size=size)
 
 class LinkHTMLProperty(HTMLProperty):
-    ''' Link HTMLProperty
+    """ Link HTMLProperty
         Include the above as well as being able to access the class
         information. Stringifying the object itself results in the value
         from the item being displayed. Accessing attributes of this object
@@ -1718,7 +1761,7 @@
         property accessed (so item/assignedto/name would look up the user
         entry identified by the assignedto property on item, and then the
         name property of that user)
-    '''
+    """
     def __init__(self, *args, **kw):
         HTMLProperty.__init__(self, *args, **kw)
         # if we're representing a form value, then the -1 from the form really
@@ -1727,7 +1770,7 @@
             self._value = None
 
     def __getattr__(self, attr):
-        ''' return a new HTMLItem '''
+        """ return a new HTMLItem """
         if not self._value:
             # handle a special page templates lookup
             if attr == '__render_with_namespace__':
@@ -1740,8 +1783,8 @@
         return getattr(i, attr)
 
     def plain(self, escape=0):
-        ''' Render a "plain" representation of the property
-        '''
+        """ Render a "plain" representation of the property
+        """
         if not self.is_view_ok():
             return self._('[hidden]')
 
@@ -1749,16 +1792,19 @@
             return ''
         linkcl = self._db.classes[self._prop.classname]
         k = linkcl.labelprop(1)
-        value = str(linkcl.get(self._value, k))
+        if num_re.match(self._value):
+            value = str(linkcl.get(self._value, k))
+        else :
+            value = self._value
         if escape:
             value = cgi.escape(value)
         return value
 
     def field(self, showid=0, size=None):
-        ''' Render a form edit field for the property
+        """ Render a form edit field for the property
 
             If not editable, just display the value via plain().
-        '''
+        """
         if not self.is_edit_ok():
             return self.plain()
 
@@ -1768,7 +1814,7 @@
             value = ''
         else:
             k = linkcl.getkey()
-            if k:
+            if k and num_re.match(self._value):
                 value = linkcl.get(self._value, k)
             else:
                 value = self._value
@@ -1776,7 +1822,7 @@
 
     def menu(self, size=None, height=None, showid=0, additional=[], value=None,
             sort_on=None, **conditions):
-        ''' Render a form select list for this property
+        """ Render a form select list for this property
 
             "size" is used to limit the length of the list labels
             "height" is used to set the <select> tag's "size" attribute
@@ -1794,7 +1840,7 @@
             "filterspec" argument to a Class.filter() call.
 
             If not editable, just display the value via plain().
-        '''
+        """
         if not self.is_edit_ok():
             return self.plain()
 
@@ -1854,7 +1900,7 @@
                 lab = lab + ' (%s)'%', '.join(map(str, m))
 
             # and generate
-            lab = cgi.escape(lab)
+            lab = cgi.escape(self._(lab))
             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
         l.append('</select>')
         return '\n'.join(l)
@@ -1863,16 +1909,16 @@
 
 
 class MultilinkHTMLProperty(HTMLProperty):
-    ''' Multilink HTMLProperty
+    """ Multilink HTMLProperty
 
         Also be iterable, returning a wrapper object like the Link case for
         each entry in the multilink.
-    '''
+    """
     def __init__(self, *args, **kwargs):
         HTMLProperty.__init__(self, *args, **kwargs)
         if self._value:
             display_value = lookupIds(self._db, self._prop, self._value,
-                fail_ok=1)
+                fail_ok=1, do_lookup=False)
             sortfun = make_sort_function(self._db, self._prop.classname)
             # sorting fails if the value contains
             # items not yet stored in the database
@@ -1884,15 +1930,15 @@
             self._value = display_value
 
     def __len__(self):
-        ''' length of the multilink '''
+        """ length of the multilink """
         return len(self._value)
 
     def __getattr__(self, attr):
-        ''' no extended attribute accesses make sense here '''
+        """ no extended attribute accesses make sense here """
         raise AttributeError, attr
 
     def viewableGenerator(self, values):
-        '''Used to iterate over only the View'able items in a class.'''
+        """Used to iterate over only the View'able items in a class."""
         check = self._db.security.hasPermission
         userid = self._client.userid
         classname = self._prop.classname
@@ -1901,36 +1947,36 @@
                 yield HTMLItem(self._client, classname, value)
 
     def __iter__(self):
-        ''' iterate and return a new HTMLItem
-        '''
+        """ iterate and return a new HTMLItem
+        """
         return self.viewableGenerator(self._value)
 
     def reverse(self):
-        ''' return the list in reverse order
-        '''
+        """ return the list in reverse order
+        """
         l = self._value[:]
         l.reverse()
         return self.viewableGenerator(l)
 
     def sorted(self, property):
-        ''' Return this multilink sorted by the given property '''
+        """ Return this multilink sorted by the given property """
         value = list(self.__iter__())
         value.sort(lambda a,b:cmp(a[property], b[property]))
         return value
 
     def __contains__(self, value):
-        ''' Support the "in" operator. We have to make sure the passed-in
+        """ Support the "in" operator. We have to make sure the passed-in
             value is a string first, not a HTMLProperty.
-        '''
+        """
         return str(value) in self._value
 
     def isset(self):
-        '''Is my _value not []?'''
+        """Is my _value not []?"""
         return self._value != []
 
     def plain(self, escape=0):
-        ''' Render a "plain" representation of the property
-        '''
+        """ Render a "plain" representation of the property
+        """
         if not self.is_view_ok():
             return self._('[hidden]')
 
@@ -1948,10 +1994,10 @@
         return value
 
     def field(self, size=30, showid=0):
-        ''' Render a form edit field for the property
+        """ Render a form edit field for the property
 
             If not editable, just display the value via plain().
-        '''
+        """
         if not self.is_edit_ok():
             return self.plain()
 
@@ -1968,7 +2014,7 @@
 
     def menu(self, size=None, height=None, showid=0, additional=[],
              value=None, sort_on=None, **conditions):
-        ''' Render a form <select> list for this property.
+        """ Render a form <select> list for this property.
 
             "size" is used to limit the length of the list labels
             "height" is used to set the <select> tag's "size" attribute
@@ -1986,7 +2032,7 @@
             "filterspec" argument to a Class.filter() call.
 
             If not editable, just display the value via plain().
-        '''
+        """
         if not self.is_edit_ok():
             return self.plain()
 
@@ -2060,8 +2106,8 @@
 )
 
 def make_sort_function(db, classname, sort_on=None):
-    '''Make a sort function for a given class
-    '''
+    """Make a sort function for a given class
+    """
     linkcl = db.getclass(classname)
     if sort_on is None:
         sort_on = find_sort_key(linkcl)
@@ -2076,9 +2122,9 @@
         return linkcl.labelprop()
 
 def handleListCGIValue(value):
-    ''' Value is either a single item or a list of items. Each item has a
+    """ Value is either a single item or a list of items. Each item has a
         .value that we're actually interested in.
-    '''
+    """
     if isinstance(value, type([])):
         return [value.value for value in value]
     else:
@@ -2088,7 +2134,7 @@
         return [v.strip() for v in value.split(',')]
 
 class HTMLRequest(HTMLInputMixin):
-    '''The *request*, holding the CGI form and environment.
+    """The *request*, holding the CGI form and environment.
 
     - "form" the CGI form as a cgi.FieldStorage
     - "env" the CGI environment variables
@@ -2107,7 +2153,7 @@
     - "filter" properties to filter the index on
     - "filterspec" values to filter the index on
     - "search_text" text to perform a full-text search on for an index
-    '''
+    """
     def __repr__(self):
         return '<HTMLRequest %r>'%self.__dict__
 
@@ -2145,8 +2191,8 @@
         return self.indexargs_url(url, args)
 
     def _parse_sort(self, var, name):
-        ''' Parse sort/group options. Append to var
-        '''
+        """ Parse sort/group options. Append to var
+        """
         fields = []
         dirs = []
         for special in '@:':
@@ -2180,8 +2226,8 @@
                 var.append(('+', f))
 
     def _post_init(self):
-        ''' Set attributes based on self.form
-        '''
+        """ Set attributes based on self.form
+        """
         # extract the index display information from the form
         self.columns = []
         for name in ':columns @columns'.split():
@@ -2215,8 +2261,9 @@
                 fv = self.form[name]
                 if (isinstance(prop, hyperdb.Link) or
                         isinstance(prop, hyperdb.Multilink)):
-                    self.filterspec[name] = lookupIds(db, prop,
-                        handleListCGIValue(fv))
+                    ids = lookupIds(db, prop, handleListCGIValue(fv))
+                    if ids :
+                        self.filterspec[name] = ids
                 else:
                     if isinstance(fv, type([])):
                         self.filterspec[name] = [v.value for v in fv]
@@ -2260,24 +2307,24 @@
             self.dispname = None
 
     def updateFromURL(self, url):
-        ''' Parse the URL for query args, and update my attributes using the
+        """ Parse the URL for query args, and update my attributes using the
             values.
-        '''
+        """
         env = {'QUERY_STRING': url}
         self.form = cgi.FieldStorage(environ=env)
 
         self._post_init()
 
     def update(self, kwargs):
-        ''' Update my attributes using the keyword args
-        '''
+        """ Update my attributes using the keyword args
+        """
         self.__dict__.update(kwargs)
         if kwargs.has_key('columns'):
             self.show = support.TruthDict(self.columns)
 
     def description(self):
-        ''' Return a description of the request - handle for the page title.
-        '''
+        """ Return a description of the request - handle for the page title.
+        """
         s = [self.client.db.config.TRACKER_NAME]
         if self.classname:
             if self.client.nodeid:
@@ -2304,7 +2351,7 @@
         for k,v in self.env.items():
             e += '\n     %r=%r'%(k, v)
         d['env'] = e
-        return '''
+        return """
 form: %(form)s
 base: %(base)r
 classname: %(classname)r
@@ -2317,11 +2364,11 @@
 pagesize: %(pagesize)r
 startwith: %(startwith)r
 env: %(env)s
-'''%d
+"""%d
 
     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
             filterspec=1, search_text=1):
-        ''' return the current index args as form elements '''
+        """ return the current index args as form elements """
         l = []
         sc = self.special_char
         def add(k, v):
@@ -2363,8 +2410,8 @@
         return '\n'.join(l)
 
     def indexargs_url(self, url, args):
-        ''' Embed the current index args in a URL
-        '''
+        """ Embed the current index args in a URL
+        """
         q = urllib.quote
         sc = self.special_char
         l = ['%s=%s'%(k,v) for k,v in args.items()]
@@ -2410,7 +2457,7 @@
                 if not args.has_key(k):
                     if type(v) == type([]):
                         prop = cls.get_transitive_prop(k)
-                        if isinstance(prop, hyperdb.String):
+                        if k != 'id' and isinstance(prop, hyperdb.String):
                             l.append('%s=%s'%(k, '%20'.join([q(i) for i in v])))
                         else:
                             l.append('%s=%s'%(k, ','.join([q(i) for i in v])))
@@ -2420,7 +2467,7 @@
     indexargs_href = indexargs_url
 
     def base_javascript(self):
-        return '''
+        return """
 <script type="text/javascript">
 submitted = false;
 function submit_once() {
@@ -2437,11 +2484,11 @@
     HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
 }
 </script>
-'''%self.base
+"""%self.base
 
     def batch(self):
-        ''' Return a batch object for results from the "current search"
-        '''
+        """ Return a batch object for results from the "current search"
+        """
         filterspec = self.filterspec
         sort = self.sort
         group = self.group
@@ -2470,7 +2517,7 @@
 # extend the standard ZTUtils Batch object to remove dependency on
 # Acquisition and add a couple of useful methods
 class Batch(ZTUtils.Batch):
-    ''' Use me to turn a list of items, or item ids of a given class, into a
+    """ Use me to turn a list of items, or item ids of a given class, into a
         series of batches.
 
         ========= ========================================================
@@ -2492,7 +2539,7 @@
         the batch.
 
         "sequence_length" is the length of the original, unbatched, sequence.
-    '''
+    """
     def __init__(self, client, sequence, size, start, end=0, orphan=0,
             overlap=0, classname=None):
         self.client = client
@@ -2526,9 +2573,9 @@
         return item
 
     def propchanged(self, *properties):
-        ''' Detect if one of the properties marked as being a group
+        """ Detect if one of the properties marked as being a group
             property changed in the last iteration fetch
-        '''
+        """
         # we poke directly at the _value here since MissingValue can screw
         # us up and cause Nones to compare strangely
         if self.last_item is None:
@@ -2561,8 +2608,8 @@
             self.end - self.overlap, 0, self.orphan, self.overlap)
 
 class TemplatingUtils:
-    ''' Utilities for templating
-    '''
+    """ Utilities for templating
+    """
     def __init__(self, client):
         self.client = client
     def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
@@ -2570,15 +2617,15 @@
             overlap)
 
     def url_quote(self, url):
-        '''URL-quote the supplied text.'''
+        """URL-quote the supplied text."""
         return urllib.quote(url)
 
     def html_quote(self, html):
-        '''HTML-quote the supplied text.'''
+        """HTML-quote the supplied text."""
         return cgi.escape(html)
 
     def __getattr__(self, name):
-        '''Try the tracker's templating_utils.'''
+        """Try the tracker's templating_utils."""
         if not hasattr(self.client.instance, 'templating_utils'):
             # backwards-compatibility
             raise AttributeError, name

Modified: tracker/roundup-src/roundup/configuration.py
==============================================================================
--- tracker/roundup-src/roundup/configuration.py	(original)
+++ tracker/roundup-src/roundup/configuration.py	Sun Mar  9 09:26:16 2008
@@ -1,16 +1,18 @@
 # Roundup Issue Tracker configuration support
 #
-# $Id: configuration.py,v 1.39 2006/12/18 06:06:03 richard Exp $
+# $Id: configuration.py,v 1.50 2007/11/14 14:57:47 schlatterbeck Exp $
 #
 __docformat__ = "restructuredtext"
 
+import ConfigParser
 import getopt
 import imp
-import os
-import time
-import ConfigParser
 import logging, logging.config
+import os
+import re
 import sys
+import time
+import smtplib
 
 import roundup.date
 
@@ -39,7 +41,7 @@
 
     Configuration options may be accessed as configuration object
     attributes or items.  So this exception instances also are
-    instances of KeyError (invalid item access) and AttrributeError
+    instances of KeyError (invalid item access) and AttributeError
     (invalid attribute access).
 
     Constructor parameter: option name
@@ -192,7 +194,7 @@
         return self._value == self._default_value
 
     def isset(self):
-        """Return True if the value is avaliable (either set or default)"""
+        """Return True if the value is available (either set or default)"""
         return self._value != NODEFAULT
 
     def __str__(self):
@@ -423,6 +425,37 @@
                     "Timezone name or numeric hour offset required")
         return value
 
+class RegExpOption(Option):
+
+    """Regular Expression option (value is Regular Expression Object)"""
+
+    class_description = "Value is Python Regular Expression (UTF8-encoded)."
+
+    RE_TYPE = type(re.compile(""))
+
+    def __init__(self, config, section, setting,
+        default=NODEFAULT, description=None, aliases=None,
+        flags=0,
+    ):
+        self.flags = flags
+        Option.__init__(self, config, section, setting, default,
+            description, aliases)
+
+    def _value2str(self, value):
+        assert isinstance(value, self.RE_TYPE)
+        return value.pattern
+
+    def str2value(self, value):
+        if not isinstance(value, unicode):
+            value = str(value)
+            # if it is 7-bit ascii, use it as string,
+            # otherwise convert to unicode.
+            try:
+                value.decode("ascii")
+            except UnicodeError:
+                value = value.decode("utf-8")
+        return re.compile(value, self.flags)
+
 ### Main configuration layout.
 # Config is described as a sequence of sections,
 # where each section name is followed by a sequence
@@ -582,6 +615,12 @@
             "If username is not empty, password (below) MUST be set!"),
         (Option, "password", NODEFAULT, "SMTP login password.\n"
             "Set this if your mail host requires authenticated access."),
+        (IntegerNumberOption, "port", smtplib.SMTP_PORT,
+            "Default port to send SMTP on.\n"
+            "Set this if your mail server runs on a different port."),
+        (NullableOption, "local_hostname", '',
+            "The local hostname to use during SMTP transmission.\n"
+            "Set this if your mail server requires something specific."),
         (BooleanOption, "tls", "no",
             "If your SMTP mail host provides or requires TLS\n"
             "(Transport Layer Security) then set this option to 'yes'."),
@@ -603,6 +642,15 @@
             "messages to this file *instead* of sending them.\n"
             "This option has the same effect as environment variable"
             " SENDMAILDEBUG.\nEnvironment variable takes precedence."),
+        (BooleanOption, "add_authorinfo", "yes",
+            "Add a line with author information at top of all messages\n"
+            "sent by roundup"),
+        (BooleanOption, "add_authoremail", "yes",
+            "Add the mail address of the author to the author information at\n"
+            "the top of all messages.\n"
+            "If this is false but add_authorinfo is true, only the name\n"
+            "of the actor is added which protects the mail address of the\n"
+            "actor from being exposed at mail archives, etc."),
     ), "Outgoing email options.\nUsed for nozy messages and approval requests"),
     ("mailgw", (
         (BooleanOption, "keep_quoted_text", "yes",
@@ -653,7 +701,39 @@
             "will match an issue for the interval after the issue's\n"
             "creation or last activity. The interval is a standard\n"
             "Roundup interval."),
+        (RegExpOption, "refwd_re", "(\s*\W?\s*(fw|fwd|re|aw|sv|ang)\W)+",
+            "Regular expression matching a single reply or forward\n"
+            "prefix prepended by the mailer. This is explicitly\n"
+            "stripped from the subject during parsing."),
+        (RegExpOption, "origmsg_re",
+            "^[>|\s]*-----\s?Original Message\s?-----$",
+            "Regular expression matching start of an original message\n"
+            "if quoted the in body."),
+        (RegExpOption, "sign_re", "^[>|\s]*-- ?$",
+            "Regular expression matching the start of a signature\n"
+            "in the message body."),
+        (RegExpOption, "eol_re", r"[\r\n]+",
+            "Regular expression matching end of line."),
+        (RegExpOption, "blankline_re", r"[\r\n]+\s*[\r\n]+",
+            "Regular expression matching a blank line."),
+        (BooleanOption, "ignore_alternatives", "no",
+            "When parsing incoming mails, roundup uses the first\n"
+            "text/plain part it finds. If this part is inside a\n"
+            "multipart/alternative, and this option is set, all other\n"
+            "parts of the multipart/alternative are ignored. The default\n"
+            "is to keep all parts and attach them to the issue."),
     ), "Roundup Mail Gateway options"),
+    ("pgp", (
+        (BooleanOption, "enable", "no",
+            "Enable PGP processing. Requires pyme."),
+        (NullableOption, "roles", "",
+            "If specified, a comma-separated list of roles to perform\n"
+            "PGP processing on. If not specified, it happens for all\n"
+            "users."),
+        (NullableOption, "homedir", "",
+            "Location of PGP directory. Defaults to $HOME/.gnupg if\n"
+            "not specified."),
+    ), "OpenPGP mail processing options"),
     ("nosy", (
         (RunDetectorOption, "messages_to_author", "no",
             "Send nosy messages to the author of the message.",
@@ -681,6 +761,10 @@
             "\"multiple\" then a separate email is sent to each\n"
             "recipient. If \"single\" then a single email is sent with\n"
             "each recipient as a CC address."),
+        (IntegerNumberOption, "max_attachment_size", sys.maxint,
+            "Attachments larger than the given number of bytes\n"
+            "won't be attached to nosy mails. They will be replaced by\n"
+            "a link to the tracker's download page for the file.")
     ), "Nosy messages sending"),
 )
 

Modified: tracker/roundup-src/roundup/date.py
==============================================================================
--- tracker/roundup-src/roundup/date.py	(original)
+++ tracker/roundup-src/roundup/date.py	Sun Mar  9 09:26:16 2008
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-# $Id: date.py,v 1.88 2006/09/09 05:50:17 richard Exp $
+# $Id: date.py,v 1.94 2007/12/23 00:23:23 richard Exp $
 
 """Date, time and time interval handling.
 """
@@ -33,15 +33,6 @@
 
 from roundup import i18n
 
-def _add_granularity(src, order, value = 1):
-    '''Increment first non-None value in src dictionary ordered by 'order'
-    parameter
-    '''
-    for gran in order:
-        if src[gran]:
-            src[gran] = int(src[gran]) + value
-            break
-
 # no, I don't know why we must anchor the date RE when we only ever use it
 # in a match()
 date_re = re.compile(r'''^
@@ -243,7 +234,8 @@
         <Date 2003-07-01.00:00:0.000000>
     '''
 
-    def __init__(self, spec='.', offset=0, add_granularity=0, translator=i18n):
+    def __init__(self, spec='.', offset=0, add_granularity=False,
+            translator=i18n):
         """Construct a date given a specification and a time zone offset.
 
         'spec'
@@ -263,7 +255,6 @@
         elif isinstance(spec, datetime.datetime):
             # Python 2.3+ datetime object
             y,m,d,H,M,S,x,x,x = spec.timetuple()
-            if y < 1970: raise ValueError, 'year must be > 1970'
             S += spec.microsecond/1000000.
             spec = (y,m,d,H,M,S,x,x,x)
         elif hasattr(spec, 'tuple'):
@@ -272,17 +263,17 @@
             spec = spec.get_tuple()
         try:
             y,m,d,H,M,S,x,x,x = spec
-            if y < 1970: raise ValueError, 'year must be > 1970'
             frac = S - int(S)
             self.year, self.month, self.day, self.hour, self.minute, \
                 self.second = _local_to_utc(y, m, d, H, M, S, offset)
             # we lost the fractional part
             self.second = self.second + frac
+            if str(self.second) == '60.0': self.second = 59.9
         except:
             raise ValueError, 'Unknown spec %r' % (spec,)
 
     def set(self, spec, offset=0, date_re=date_re,
-            serialised_re=serialised_date_re, add_granularity=0):
+            serialised_re=serialised_date_re, add_granularity=False):
         ''' set the date to the value in spec
         '''
 
@@ -304,15 +295,24 @@
 
         info = m.groupdict()
 
+        # determine whether we need to add anything at the end
         if add_granularity:
-            _add_granularity(info, 'SMHdmyab')
+            for gran in 'SMHdmy':
+                if info[gran] is not None:
+                    if gran == 'S':
+                        raise ValueError
+                    elif gran == 'M':
+                        add_granularity = Interval('00:01')
+                    elif gran == 'H':
+                        add_granularity = Interval('01:00')
+                    else:
+                        add_granularity = Interval('+1%s'%gran)
+                    break
 
         # get the current date as our default
-        ts = time.time()
-        frac = ts - int(ts)
-        y,m,d,H,M,S,x,x,x = time.gmtime(ts)
-        # gmtime loses the fractional seconds
-        S = S + frac
+        dt = datetime.datetime.utcnow()
+        y,m,d,H,M,S,x,x,x = dt.timetuple()
+        S += dt.microsecond/1000000.
 
         # whether we need to convert to UTC
         adjust = False
@@ -320,7 +320,6 @@
         if info['y'] is not None or info['a'] is not None:
             if info['y'] is not None:
                 y = int(info['y'])
-                if y < 1970: raise ValueError, 'year must be > 1970'
                 m,d = (1,1)
                 if info['m'] is not None:
                     m = int(info['m'])
@@ -342,19 +341,17 @@
                 S = float(info['S'])
             adjust = True
 
-        if add_granularity:
-            S = S - 1
 
         # now handle the adjustment of hour
         frac = S - int(S)
-        ts = calendar.timegm((y,m,d,H,M,S,0,0,0))
-        y, m, d, H, M, S, x, x, x = time.gmtime(ts)
+        dt = datetime.datetime(y,m,d,H,M,int(S), int(frac * 1000000.))
+        y, m, d, H, M, S, x, x, x = dt.timetuple()
         if adjust:
             y, m, d, H, M, S = _local_to_utc(y, m, d, H, M, S, offset)
         self.year, self.month, self.day, self.hour, self.minute, \
             self.second = y, m, d, H, M, S
         # we lost the fractional part along the way
-        self.second = self.second + frac
+        self.second += dt.microsecond/1000000.
 
         if info.get('o', None):
             try:
@@ -364,6 +361,11 @@
                     '"yyyy-mm-dd", "mm-dd", "HH:MM", "HH:MM:SS" or '
                     '"yyyy-mm-dd.HH:MM:SS.SSS"')%(spec,)
 
+        # adjust by added granularity
+        if add_granularity:
+            self.applyInterval(add_granularity)
+            self.applyInterval(Interval('- 00:00:01'))
+
     def addInterval(self, interval):
         ''' Add the interval to this date, returning the date tuple
         '''
@@ -490,7 +492,7 @@
         return self.formal()
 
     def formal(self, sep='.', sec='%02d'):
-        f = '%%4d-%%02d-%%02d%s%%02d:%%02d:%s'%(sep, sec)
+        f = '%%04d-%%02d-%%02d%s%%02d:%%02d:%s'%(sep, sec)
         return f%(self.year, self.month, self.day, self.hour, self.minute,
             self.second)
 
@@ -500,13 +502,10 @@
             Note that if the day is zero, and the day appears first in the
             format, then the day number will be removed from output.
         '''
-        # Python2.4 strftime() enforces the non-zero-ness of the day-of-year
-        # component of the time tuple, so we need to figure it out
-        t = (self.year, self.month, self.day, self.hour, self.minute,
-            int(self.second), 0, 0, 0)
-        t = calendar.timegm(t)
-        t = time.gmtime(t)
-        str = time.strftime(format, t)
+        dt = datetime.datetime(self.year, self.month, self.day, self.hour,
+            self.minute, int(self.second),
+            int ((self.second - int (self.second)) * 1000000.))
+        str = dt.strftime(format)
 
         # handle zero day by removing it
         if format.startswith('%d') and str[0] == '0':
@@ -532,7 +531,7 @@
             self.second, 0, 0, 0)
 
     def serialise(self):
-        return '%4d%02d%02d%02d%02d%06.3f'%(self.year, self.month,
+        return '%04d%02d%02d%02d%02d%06.3f'%(self.year, self.month,
             self.day, self.hour, self.minute, self.second)
 
     def timestamp(self):
@@ -555,6 +554,17 @@
         self._ = translator.gettext
         self.ngettext = translator.ngettext
 
+    def fromtimestamp(cls, ts):
+        """Create a date object from a timestamp.
+
+        The timestamp may be outside the gmtime year-range of
+        1902-2038.
+        """
+        usec = int((ts - int(ts)) * 1000000.)
+        delta = datetime.timedelta(seconds = int(ts), microseconds = usec)
+        return cls(datetime.datetime(1970, 1, 1) + delta)
+    fromtimestamp = classmethod(fromtimestamp)
+
 class Interval:
     '''
     Date intervals are specified using the suffixes "y", "m", and "d". The
@@ -607,7 +617,7 @@
 
     TODO: more examples, showing the order of addition operation
     '''
-    def __init__(self, spec, sign=1, allowdate=1, add_granularity=0,
+    def __init__(self, spec, sign=1, allowdate=1, add_granularity=False,
         translator=i18n
     ):
         """Construct an interval given a specification."""
@@ -649,7 +659,7 @@
                )?''', re.VERBOSE), serialised_re=re.compile('''
             (?P<s>[+-])?1?(?P<y>([ ]{3}\d|\d{4}))(?P<m>\d{2})(?P<d>\d{2})
             (?P<H>\d{2})(?P<M>\d{2})(?P<S>\d{2})''', re.VERBOSE),
-            add_granularity=0):
+            add_granularity=False):
         ''' set the date to the value in spec
         '''
         self.year = self.month = self.week = self.day = self.hour = \
@@ -667,7 +677,10 @@
         # pull out all the info specified
         info = m.groupdict()
         if add_granularity:
-            _add_granularity(info, 'SMHdwmy', (info['s']=='-' and -1 or 1))
+            for gran in 'SMHdwmy':
+                if info[gran] is not None:
+                    info[gran] = int(info[gran]) + (info['s']=='-' and -1 or 1)
+                    break
 
         valid = 0
         for group, attr in {'y':'year', 'm':'month', 'w':'week', 'd':'day',
@@ -998,7 +1011,7 @@
         <Range from None to 2003-03-09.20:00:00>
 
     """
-    def __init__(self, spec, Type, allow_granularity=1, **params):
+    def __init__(self, spec, Type, allow_granularity=True, **params):
         """Initializes Range of type <Type> from given <spec> string.
 
         Sets two properties - from_value and to_value. None assigned to any of
@@ -1007,20 +1020,19 @@
 
         The Type parameter here should be class itself (e.g. Date), not a
         class instance.
-
         """
         self.range_type = Type
         re_range = r'(?:^|from(.+?))(?:to(.+?)$|$)'
         re_geek_range = r'(?:^|(.+?));(?:(.+?)$|$)'
         # Check which syntax to use
-        if  spec.find(';') == -1:
-            # Native english
-            mch_range = re.search(re_range, spec.strip(), re.IGNORECASE)
-        else:
+        if ';' in spec:
             # Geek
-            mch_range = re.search(re_geek_range, spec.strip())
-        if mch_range:
-            self.from_value, self.to_value = mch_range.groups()
+            m = re.search(re_geek_range, spec.strip())
+        else:
+            # Native english
+            m = re.search(re_range, spec.strip(), re.IGNORECASE)
+        if m:
+            self.from_value, self.to_value = m.groups()
             if self.from_value:
                 self.from_value = Type(self.from_value.strip(), **params)
             if self.to_value:
@@ -1028,7 +1040,7 @@
         else:
             if allow_granularity:
                 self.from_value = Type(spec, **params)
-                self.to_value = Type(spec, add_granularity=1, **params)
+                self.to_value = Type(spec, add_granularity=True, **params)
             else:
                 raise ValueError, "Invalid range"
 

Modified: tracker/roundup-src/roundup/hyperdb.py
==============================================================================
--- tracker/roundup-src/roundup/hyperdb.py	(original)
+++ tracker/roundup-src/roundup/hyperdb.py	Sun Mar  9 09:26:16 2008
@@ -15,19 +15,20 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-# $Id: hyperdb.py,v 1.128 2006/11/09 03:08:22 richard Exp $
+# $Id: hyperdb.py,v 1.131 2007/09/27 06:18:53 jpend Exp $
 
 """Hyperdatabase implementation, especially field types.
 """
 __docformat__ = 'restructuredtext'
 
 # standard python modules
-import sys, os, time, re, shutil, weakref
+import os, re, shutil, weakref
 from sets import Set
 
 # roundup modules
 import date, password
 from support import ensureParentsExist, PrioList, sorted, reversed
+from roundup.i18n import _
 
 #
 # Types
@@ -73,15 +74,17 @@
             p = password.Password()
             p.scheme = m.group(1)
             if p.scheme not in 'SHA crypt plaintext'.split():
-                raise HyperdbValueError, 'property %s: unknown encryption '\
-                    'scheme %r'%(kw['propname'], p.scheme)
+                raise HyperdbValueError, \
+                        ('property %s: unknown encryption scheme %r') %\
+                        (kw['propname'], p.scheme)
             p.password = m.group(2)
             value = p
         else:
             try:
                 value = password.Password(value)
             except password.PasswordValueError, message:
-                raise HyperdbValueError, 'property %s: %s'%(propname, message)
+                raise HyperdbValueError, \
+                        _('property %s: %s')%(kw['propname'], message)
         return value
     def sort_repr (self, cls, val, name):
         if not val:
@@ -101,8 +104,8 @@
         try:
             value = date.Date(value, self.offset(db))
         except ValueError, message:
-            raise HyperdbValueError, 'property %s: %r is an invalid '\
-                'date (%s)'%(kw['propname'], value, message)
+            raise HyperdbValueError, _('property %s: %r is an invalid '\
+                'date (%s)')%(kw['propname'], value, message)
         return value
     def range_from_raw(self, value, db):
         """return Range value from given raw value with offset correction"""
@@ -118,8 +121,8 @@
         try:
             value = date.Interval(value)
         except ValueError, message:
-            raise HyperdbValueError, 'property %s: %r is an invalid '\
-                'date interval (%s)'%(kw['propname'], value, message)
+            raise HyperdbValueError, _('property %s: %r is an invalid '\
+                'date interval (%s)')%(kw['propname'], value, message)
         return value
     def sort_repr (self, cls, val, name):
         if not val:
@@ -213,8 +216,8 @@
                 try:
                     curvalue.remove(itemid)
                 except ValueError:
-                    raise HyperdbValueError, 'property %s: %r is not ' \
-                        'currently an element'%(propname, item)
+                    raise HyperdbValueError, _('property %s: %r is not ' \
+                        'currently an element')%(propname, item)
             else:
                 newvalue.append(itemid)
                 if itemid not in curvalue:
@@ -257,7 +260,7 @@
         try:
             value = float(value)
         except ValueError:
-            raise HyperdbValueError, 'property %s: %r is not a number'%(
+            raise HyperdbValueError, _('property %s: %r is not a number')%(
                 kw['propname'], value)
         return value
 #
@@ -270,7 +273,7 @@
     '''
     m = dre.match(designator)
     if m is None:
-        raise DesignatorError, '"%s" not a node designator'%designator
+        raise DesignatorError, _('"%s" not a node designator')%designator
     return m.group(1), m.group(2)
 
 class Proptree(object):
@@ -487,7 +490,7 @@
         else:
             self._val = val
         self.has_values = True
-    
+
     val = property(lambda self: self._val, _set_val)
 
     def _sort(self, val):
@@ -943,7 +946,7 @@
            resolution order.
         """
         if labelprop not in self.getprops():
-            raise ValueError, "Not a property name: %s" % labelprop
+            raise ValueError, _("Not a property name: %s") % labelprop
         self._labelprop = labelprop
 
     def setorderprop(self, orderprop):
@@ -951,7 +954,7 @@
            resolution order
         """
         if orderprop not in self.getprops():
-            raise ValueError, "Not a property name: %s" % orderprop
+            raise ValueError, _("Not a property name: %s") % orderprop
         self._orderprop = orderprop
 
     def getkey(self):
@@ -1136,7 +1139,7 @@
         for all issues where a message was added by a certain user in
         the last week with a filterspec of
         {'messages.author' : '42', 'messages.creation' : '.-1w;'}
-        
+
         Implementation note:
         This implements a non-optimized version of Transitive search
         using _filter implemented in a backend class. A more efficient
@@ -1234,11 +1237,11 @@
             try:
                 value = linkcl.lookup(value)
             except KeyError, message:
-                raise HyperdbValueError, 'property %s: %r is not a %s.'%(
+                raise HyperdbValueError, _('property %s: %r is not a %s.')%(
                     propname, value, prop.classname)
         else:
-            raise HyperdbValueError, 'you may only enter ID values '\
-                'for property %s'%propname
+            raise HyperdbValueError, _('you may only enter ID values '\
+                'for property %s')%propname
     return value
 
 def fixNewlines(text):
@@ -1267,7 +1270,7 @@
     try:
         proptype =  properties[propname]
     except KeyError:
-        raise HyperdbValueError, '%r is not a property of %s'%(propname,
+        raise HyperdbValueError, _('%r is not a property of %s')%(propname,
             klass.classname)
 
     # if we got a string, strip it now
@@ -1291,7 +1294,7 @@
         property.
         '''
         if not properties.has_key('content'):
-            properties['content'] = hyperdb.String(indexme='yes')
+            properties['content'] = String(indexme='yes')
 
     def export_propnames(self):
         ''' Don't export the "content" property
@@ -1324,12 +1327,14 @@
         shutil.copyfile(source, dest)
 
         mime_type = None
-        if self.getprops().has_key('type'):
+        props = self.getprops()
+        if props.has_key('type'):
             mime_type = self.get(nodeid, 'type')
         if not mime_type:
             mime_type = self.default_mime_type
-        self.db.indexer.add_text((self.classname, nodeid, 'content'),
-            self.get(nodeid, 'content'), mime_type)
+        if props['content'].indexme:
+            self.db.indexer.add_text((self.classname, nodeid, 'content'),
+                self.get(nodeid, 'content'), mime_type)
 
 class Node:
     ''' A convenience wrapper for the given node
@@ -1388,6 +1393,6 @@
     cl = Class(db, name, name=String(), order=String())
     for i in range(len(options)):
         cl.create(name=options[i], order=i)
-    return hyperdb.Link(name)
+    return Link(name)
 
 # vim: set filetype=python sts=4 sw=4 et si :

Modified: tracker/roundup-src/roundup/mailer.py
==============================================================================
--- tracker/roundup-src/roundup/mailer.py	(original)
+++ tracker/roundup-src/roundup/mailer.py	Sun Mar  9 09:26:16 2008
@@ -1,7 +1,7 @@
 """Sending Roundup-specific mail over SMTP.
 """
 __docformat__ = 'restructuredtext'
-# $Id: mailer.py,v 1.18 2006/08/11 01:41:25 richard Exp $
+# $Id: mailer.py,v 1.21 2007/11/14 05:53:20 jpend Exp $
 
 import time, quopri, os, socket, smtplib, re, sys, traceback
 
@@ -10,6 +10,7 @@
 
 from roundup.rfc2822 import encode_header
 from roundup import __version__
+from roundup.date import get_timezone
 
 try:
     from email.Utils import formatdate
@@ -30,6 +31,16 @@
         self.debug = os.environ.get('SENDMAILDEBUG', '') \
             or config["MAIL_DEBUG"]
 
+        # set timezone so that things like formatdate(localtime=True)
+        # use the configured timezone
+        # apparently tzset doesn't exist in python under Windows, my bad.
+        # my pathetic attempts at googling a Windows-solution failed
+        # so if you're on Windows your mail won't use your configured
+        # timezone.
+        if hasattr(time, 'tzset'):
+            os.environ['TZ'] = get_timezone(self.config.TIMEZONE).tzname(None)
+            time.tzset()
+
     def get_standard_message(self, to, subject, author=None):
         '''Form a standard email message from Roundup.
 
@@ -60,7 +71,7 @@
         writer.addheader('Subject', encode_header(subject, charset))
         writer.addheader('To', ', '.join(to))
         writer.addheader('From', author)
-        writer.addheader('Date', formatdate())
+        writer.addheader('Date', formatdate(localtime=True))
 
         # Add a unique Roundup header to help filtering
         writer.addheader('X-Roundup-Name', encode_header(tracker_name,
@@ -190,8 +201,8 @@
     ''' Open an SMTP connection to the mailhost specified in the config
     '''
     def __init__(self, config):
-
-        smtplib.SMTP.__init__(self, config.MAILHOST)
+        smtplib.SMTP.__init__(self, config.MAILHOST, port=config['MAIL_PORT'],
+                              local_hostname=config['MAIL_LOCAL_HOSTNAME'])
 
         # start the TLS if requested
         if config["MAIL_TLS"]:

Modified: tracker/roundup-src/roundup/mailgw.py
==============================================================================
--- tracker/roundup-src/roundup/mailgw.py	(original)
+++ tracker/roundup-src/roundup/mailgw.py	Sun Mar  9 09:26:16 2008
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
 #
 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
 # This module is free software, and you may redistribute it and/or modify
@@ -80,10 +81,15 @@
 import time, random, sys, logging
 import traceback, MimeWriter, rfc822
 
-from roundup import hyperdb, date, password, rfc2822, exceptions
+from roundup import configuration, hyperdb, date, password, rfc2822, exceptions
 from roundup.mailer import Mailer, MessageSendError
 from roundup.i18n import _
 
+try:
+    import pyme, pyme.core, pyme.gpgme
+except ImportError:
+    pyme = None
+
 SENDMAILDEBUG = os.environ.get('SENDMAILDEBUG', '')
 
 class MailGWError(ValueError):
@@ -140,6 +146,70 @@
                 return rfc822.unquote(f[i+1:].strip())
     return None
 
+def gpgh_key_getall(key, attr):
+    ''' return list of given attribute for all uids in
+        a key
+    '''
+    u = key.uids
+    while u:
+        yield getattr(u, attr)
+        u = u.next
+
+def gpgh_sigs(sig):
+    ''' more pythonic iteration over GPG signatures '''
+    while sig:
+        yield sig
+        sig = sig.next
+
+
+def iter_roles(roles):
+    ''' handle the text processing of turning the roles list
+        into something python can use more easily
+    '''
+    for role in [x.lower().strip() for x in roles.split(',')]:
+        yield role
+
+def user_has_role(db, userid, role_list):
+    ''' see if the given user has any roles that appear
+        in the role_list
+    '''
+    for role in iter_roles(db.user.get(userid, 'roles')):
+        if role in iter_roles(role_list):
+            return True
+    return False
+
+
+def check_pgp_sigs(sig, gpgctx, author):
+    ''' Theoretically a PGP message can have several signatures. GPGME
+        returns status on all signatures in a linked list. Walk that
+        linked list looking for the author's signature
+    '''
+    for sig in gpgh_sigs(sig):
+        key = gpgctx.get_key(sig.fpr, False)
+        # we really only care about the signature of the user who
+        # submitted the email
+        if key and (author in gpgh_key_getall(key, 'email')):
+            if sig.summary & pyme.gpgme.GPGME_SIGSUM_VALID:
+                return True
+            else:
+                # try to narrow down the actual problem to give a more useful
+                # message in our bounce
+                if sig.summary & pyme.gpgme.GPGME_SIGSUM_KEY_MISSING:
+                    raise MailUsageError, \
+                        _("Message signed with unknown key: %s") % sig.fpr
+                elif sig.summary & pyme.gpgme.GPGME_SIGSUM_KEY_EXPIRED:
+                    raise MailUsageError, \
+                        _("Message signed with an expired key: %s") % sig.fpr
+                elif sig.summary & pyme.gpgme.GPGME_SIGSUM_KEY_REVOKED:
+                    raise MailUsageError, \
+                        _("Message signed with a revoked key: %s") % sig.fpr
+                else:
+                    raise MailUsageError, \
+                        _("Invalid PGP signature detected.")
+
+    # we couldn't find a key belonging to the author of the email
+    raise MailUsageError, _("Message signed with unknown key: %s") % sig.fpr
+
 class Message(mimetools.Message):
     ''' subclass mimetools.Message so we can retrieve the parts of the
         message...
@@ -156,6 +226,17 @@
             if not line:
                 break
             if line.strip() in (mid, end):
+                # according to rfc 1431 the preceding line ending is part of
+                # the boundary so we need to strip that
+                length = s.tell()
+                s.seek(-2, 1)
+                lineending = s.read(2)
+                if lineending == '\r\n':
+                    s.truncate(length - 2)
+                elif lineending[1] in ('\r', '\n'):
+                    s.truncate(length - 1)
+                else:
+                    raise ValueError('Unknown line ending in message.')
                 break
             s.write(line)
         if not s.getvalue().strip():
@@ -166,6 +247,7 @@
     def getparts(self):
         """Get all parts of this multipart message."""
         # skip over the intro to the first boundary
+        self.fp.seek(0)
         self.getpart()
 
         # accumulate the other parts
@@ -264,8 +346,13 @@
     # multipart/form-data:
     #   For web forms only.
 
-    def extract_content(self, parent_type=None):
-        """Extract the body and the attachments recursively."""
+    def extract_content(self, parent_type=None, ignore_alternatives = False):
+        """Extract the body and the attachments recursively.
+        
+           If the content is hidden inside a multipart/alternative part,
+           we use the *last* text/plain part of the *first*
+           multipart/alternative in the whole message.
+        """
         content_type = self.gettype()
         content = None
         attachments = []
@@ -273,17 +360,35 @@
         if content_type == 'text/plain':
             content = self.getbody()
         elif content_type[:10] == 'multipart/':
+            content_found = bool (content)
+            ig = ignore_alternatives and not content_found
             for part in self.getparts():
-                new_content, new_attach = part.extract_content(content_type)
+                new_content, new_attach = part.extract_content(content_type,
+                    not content and ig)
 
                 # If we haven't found a text/plain part yet, take this one,
                 # otherwise make it an attachment.
                 if not content:
                     content = new_content
+                    cpart   = part
                 elif new_content:
-                    attachments.append(part.as_attachment())
+                    if content_found or content_type != 'multipart/alternative':
+                        attachments.append(part.text_as_attachment())
+                    else:
+                        # if we have found a text/plain in the current
+                        # multipart/alternative and find another one, we
+                        # use the first as an attachment (if configured)
+                        # and use the second one because rfc 2046, sec.
+                        # 5.1.4. specifies that later parts are better
+                        # (thanks to Philipp Gortan for pointing this
+                        # out)
+                        attachments.append(cpart.text_as_attachment())
+                        content = new_content
+                        cpart   = part
 
                 attachments.extend(new_attach)
+            if ig and content_type == 'multipart/alternative' and content:
+                attachments = []
         elif (parent_type == 'multipart/signed' and
               content_type == 'application/pgp-signature'):
             # ignore it so it won't be saved as an attachment
@@ -292,10 +397,108 @@
             attachments.append(self.as_attachment())
         return content, attachments
 
+    def text_as_attachment(self):
+        """Return first text/plain part as Message"""
+        if not self.gettype().startswith ('multipart/'):
+            return self.as_attachment()
+        for part in self.getparts():
+            content_type = part.gettype()
+            if content_type == 'text/plain':
+                return part.as_attachment()
+            elif content_type.startswith ('multipart/'):
+                p = part.text_as_attachment()
+                if p:
+                    return p
+        return None
+
     def as_attachment(self):
         """Return this message as an attachment."""
         return (self.getname(), self.gettype(), self.getbody())
 
+    def pgp_signed(self):
+        ''' RFC 3156 requires OpenPGP MIME mail to have the protocol parameter
+        '''
+        return self.gettype() == 'multipart/signed' \
+            and self.typeheader.find('protocol="application/pgp-signature"') != -1
+
+    def pgp_encrypted(self):
+        ''' RFC 3156 requires OpenPGP MIME mail to have the protocol parameter
+        '''
+        return self.gettype() == 'multipart/encrypted' \
+            and self.typeheader.find('protocol="application/pgp-encrypted"') != -1
+
+    def decrypt(self, author):
+        ''' decrypt an OpenPGP MIME message
+            This message must be signed as well as encrypted using the "combined"
+            method. The decrypted contents are returned as a new message.
+        '''
+        (hdr, msg) = self.getparts()
+        # According to the RFC 3156 encrypted mail must have exactly two parts.
+        # The first part contains the control information. Let's verify that
+        # the message meets the RFC before we try to decrypt it.
+        if hdr.getbody() != 'Version: 1' or hdr.gettype() != 'application/pgp-encrypted':
+            raise MailUsageError, \
+                _("Unknown multipart/encrypted version.")
+
+        context = pyme.core.Context()
+        ciphertext = pyme.core.Data(msg.getbody())
+        plaintext = pyme.core.Data()
+
+        result = context.op_decrypt_verify(ciphertext, plaintext)
+
+        if result:
+            raise MailUsageError, _("Unable to decrypt your message.")
+
+        # we've decrypted it but that just means they used our public
+        # key to send it to us. now check the signatures to see if it
+        # was signed by someone we trust
+        result = context.op_verify_result()
+        check_pgp_sigs(result.signatures, context, author)
+
+        plaintext.seek(0,0)
+        # pyme.core.Data implements a seek method with a different signature
+        # than roundup can handle. So we'll put the data in a container that
+        # the Message class can work with.
+        c = cStringIO.StringIO()
+        c.write(plaintext.read())
+        c.seek(0)
+        return Message(c)
+
+    def verify_signature(self, author):
+        ''' verify the signature of an OpenPGP MIME message
+            This only handles detached signatures. Old style
+            PGP mail (i.e. '-----BEGIN PGP SIGNED MESSAGE----')
+            is archaic and not supported :)
+        '''
+        # we don't check the micalg parameter...gpgme seems to
+        # figure things out on its own
+        (msg, sig) = self.getparts()
+
+        if sig.gettype() != 'application/pgp-signature':
+            raise MailUsageError, \
+                _("No PGP signature found in message.")
+
+        context = pyme.core.Context()
+        # msg.getbody() is skipping over some headers that are
+        # required to be present for verification to succeed so
+        # we'll do this by hand
+        msg.fp.seek(0)
+        # according to rfc 3156 the data "MUST first be converted
+        # to its content-type specific canonical form. For
+        # text/plain this means conversion to an appropriate
+        # character set and conversion of line endings to the
+        # canonical <CR><LF> sequence."
+        # TODO: what about character set conversion?
+        canonical_msg = re.sub('(?<!\r)\n', '\r\n', msg.fp.read())
+        msg_data = pyme.core.Data(canonical_msg)
+        sig_data = pyme.core.Data(sig.getbody())
+
+        context.op_verify(sig_data, msg_data, None)
+
+        # check all signatures for validity
+        result = context.op_verify_result()
+        check_pgp_sigs(result.signatures, context, author)
+
 class MailGW:
 
     def __init__(self, instance, db, arguments=()):
@@ -561,12 +764,15 @@
 
             # bounce the message back to the sender with the error message
             # let the admin know that something very bad is happening
-            sendto = [sendto[0][1], self.instance.config.ADMIN_EMAIL]
             m = ['']
             m.append('An unexpected error occurred during the processing')
             m.append('of your message. The tracker administrator is being')
             m.append('notified.\n')
-            self.mailer.bounce_message(message, sendto, m)
+            self.mailer.bounce_message(message, sendto[0][1], m)
+
+            m.append('----------------')
+            m.append(traceback.format_exc())
+            self.mailer.bounce_message(message, [self.instance.config.ADMIN_EMAIL], m)
 
     def handle_message(self, message):
         ''' message - a Message instance
@@ -620,8 +826,9 @@
         # Matches subjects like:
         # Re: "[issue1234] title of issue [status=resolved]"
 
-        tmpsubject = subject # We need subject untouched for later use
-                             # in error messages
+        # Alias since we need a reference to the original subject for
+        # later use in error messages
+        tmpsubject = subject
 
         sd_open, sd_close = config['MAILGW_SUBJECT_SUFFIX_DELIMITERS']
         delim_open = re.escape(sd_open)
@@ -633,41 +840,49 @@
                                  'nodeid', 'title', 'args',
                                  'argswhole'])
 
-
         # Look for Re: et. al. Used later on for MAILGW_SUBJECT_CONTENT_MATCH
-        re_re = r'''(?P<refwd>(\s*\W?\s*(fw|fwd|re|aw|sv|ang)\W)+)\s*'''
-        m = re.match(re_re, tmpsubject, re.IGNORECASE|re.VERBOSE)
+        re_re = r"(?P<refwd>%s)\s*" % config["MAILGW_REFWD_RE"].pattern
+        m = re.match(re_re, tmpsubject, re.IGNORECASE|re.VERBOSE|re.UNICODE)
         if m:
-            matches.update(m.groupdict())
-            tmpsubject = tmpsubject[len(matches['refwd']):] # Consume Re:
+            m = m.groupdict()
+            if m['refwd']:
+                matches.update(m)
+                tmpsubject = tmpsubject[len(m['refwd']):] # Consume Re:
 
         # Look for Leading "
-        m = re.match(r'''(?P<quote>\s*")''', tmpsubject,
-                     re.IGNORECASE|re.VERBOSE)
+        m = re.match(r'(?P<quote>\s*")', tmpsubject,
+                     re.IGNORECASE)
         if m:
             matches.update(m.groupdict())
             tmpsubject = tmpsubject[len(matches['quote']):] # Consume quote
 
-        class_re = r'''%s(?P<classname>(%s))+(?P<nodeid>\d+)?%s''' % \
-                   (delim_open, "|".join(self.db.getclasses()), delim_close)
+        has_prefix = re.search(r'^%s(\w+)%s'%(delim_open,
+            delim_close), tmpsubject.strip())
+
+        class_re = r'%s(?P<classname>(%s))(?P<nodeid>\d+)?%s'%(delim_open,
+            "|".join(self.db.getclasses()), delim_close)
         # Note: re.search, not re.match as there might be garbage
         # (mailing list prefix, etc.) before the class identifier
-        m = re.search(class_re, tmpsubject, re.IGNORECASE|re.VERBOSE)
+        m = re.search(class_re, tmpsubject, re.IGNORECASE)
         if m:
             matches.update(m.groupdict())
             # Skip to the end of the class identifier, including any
             # garbage before it.
-            
+
             tmpsubject = tmpsubject[m.end():]
 
-        m = re.match(r'''(?P<title>[^%s]+)''' % delim_open, tmpsubject,
-                     re.IGNORECASE|re.VERBOSE)
+        # if we've not found a valid classname prefix then force the
+        # scanning to handle there being a leading delimiter
+        title_re = r'(?P<title>%s[^%s]+)'%(
+            not matches['classname'] and '.' or '', delim_open)
+        m = re.match(title_re, tmpsubject.strip(), re.IGNORECASE)
         if m:
             matches.update(m.groupdict())
             tmpsubject = tmpsubject[len(matches['title']):] # Consume title
 
-        args_re = r'''(?P<argswhole>%s(?P<args>.+?)%s)?''' % (delim_open, delim_close)
-        m = re.search(args_re, tmpsubject, re.IGNORECASE|re.VERBOSE)
+        args_re = r'(?P<argswhole>%s(?P<args>.+?)%s)?'%(delim_open,
+            delim_close)
+        m = re.search(args_re, tmpsubject.strip(), re.IGNORECASE|re.VERBOSE)
         if m:
             matches.update(m.groupdict())
 
@@ -687,21 +902,14 @@
                 sendto = [from_list[0][1]]
                 self.mailer.standard_message(sendto, subject, '')
                 return
+
         # get the classname
         if pfxmode == 'none':
             classname = None
         else:
             classname = matches['classname']
-        if classname is None:
-            if self.default_class:
-                classname = self.default_class
-            else:
-                classname = config['MAILGW_DEFAULT_CLASS']
-                if not classname:
-                    # fail
-                    m = None
 
-        if not classname and pfxmode == 'strict':
+        if not classname and has_prefix and pfxmode == 'strict':
             raise MailUsageError, _("""
 The message you sent to roundup did not contain a properly formed subject
 line. The subject must contain a class name or designator to indicate the
@@ -716,30 +924,51 @@
 Subject was: '%(subject)s'
 """) % locals()
 
-        # try to get the class specified - if "loose" then fall back on the
-        # default
-        attempts = [classname]
-        if pfxmode == 'loose':
-            if self.default_class:
-                attempts.append(self.default_class)
-            else:
-                attempts.append(config['MAILGW_DEFAULT_CLASS'])
+        # try to get the class specified - if "loose" or "none" then fall
+        # back on the default
+        attempts = []
+        if classname:
+            attempts.append(classname)
+
+        if self.default_class:
+            attempts.append(self.default_class)
+        else:
+            attempts.append(config['MAILGW_DEFAULT_CLASS'])
+
+        # first valid class name wins
         cl = None
         for trycl in attempts:
             try:
-                cl = self.db.getclass(classname)
+                cl = self.db.getclass(trycl)
+                classname = trycl
                 break
             except KeyError:
                 pass
+
         if not cl:
             validname = ', '.join(self.db.getclasses())
-            raise MailUsageError, _("""
-The class name you identified in the subject line ("%(classname)s") does not exist in the
-database.
+            if classname:
+                raise MailUsageError, _("""
+The class name you identified in the subject line ("%(classname)s") does
+not exist in the database.
 
 Valid class names are: %(validname)s
 Subject was: "%(subject)s"
 """) % locals()
+            else:
+                raise MailUsageError, _("""
+You did not identify a class name in the subject line and there is no
+default set for this tracker. The subject must contain a class name or
+designator to indicate the 'topic' of the message. For example:
+    Subject: [issue] This is a new issue
+      - this will create a new issue in the tracker with the title 'This is
+        a new issue'.
+    Subject: [issue1234] This is a followup to issue 1234
+      - this will append the message's contents to the existing issue 1234
+        in the tracker.
+
+Subject was: '%(subject)s'
+""") % locals()
 
         # get the optional nodeid
         if pfxmode == 'none':
@@ -770,7 +999,7 @@
         if nodeid is None and not title:
             raise MailUsageError, _("""
 I cannot match your message to a node in the database - you need to either
-supply a full designator (with number, eg "[issue123]" or keep the
+supply a full designator (with number, eg "[issue123]") or keep the
 previous subject title intact so I can match that.
 
 Subject was: "%(subject)s"
@@ -882,13 +1111,18 @@
             if author == anonid:
                 # we're anonymous and we need to be a registered user
                 from_address = from_list[0][1]
-                tracker_web = self.instance.config.TRACKER_WEB
-                raise Unauthorized, _("""
-You are not a registered user. Please register at:
+                registration_info = ""
+                if self.db.security.hasPermission('Web Access', author) and \
+                   self.db.security.hasPermission('Create', anonid, 'user'):
+                    tracker_web = self.instance.config.TRACKER_WEB
+                    registration_info = """ Please register at:
+
+%(tracker_web)suser?template=register
 
-%(tracker_web)suser?@template=register
+...before sending mail to the tracker.""" % locals()
 
-...before sending mail to the tracker.
+                raise Unauthorized, _("""
+You are not a registered user.%(registration_info)s
 
 Unknown address: %(from_address)s
 """) % locals()
@@ -978,21 +1212,48 @@
             messageid = "<%s.%s.%s%s@%s>"%(time.time(), random.random(),
                 classname, nodeid, config['MAIL_DOMAIN'])
 
+        # if they've enabled PGP processing then verify the signature
+        # or decrypt the message
+
+        # if PGP_ROLES is specified the user must have a Role in the list
+        # or we will skip PGP processing
+        def pgp_role():
+            if self.instance.config.PGP_ROLES:
+                return user_has_role(self.db, author,
+                    self.instance.config.PGP_ROLES)
+            else:
+                return True
+
+        if self.instance.config.PGP_ENABLE and pgp_role():
+            assert pyme, 'pyme is not installed'
+            # signed/encrypted mail must come from the primary address
+            author_address = self.db.user.get(author, 'address')
+            if self.instance.config.PGP_HOMEDIR:
+                os.environ['GNUPGHOME'] = self.instance.config.PGP_HOMEDIR
+            if message.pgp_signed():
+                message.verify_signature(author_address)
+            elif message.pgp_encrypted():
+                # replace message with the contents of the decrypted
+                # message for content extraction
+                # TODO: encrypted message handling is far from perfect
+                # bounces probably include the decrypted message, for
+                # instance :(
+                message = message.decrypt(author_address)
+            else:
+                raise MailUsageError, _("""
+This tracker has been configured to require all email be PGP signed or
+encrypted.""")
         # now handle the body - find the message
-        content, attachments = message.extract_content()
+        ig = self.instance.config.MAILGW_IGNORE_ALTERNATIVES
+        content, attachments = message.extract_content(ignore_alternatives = ig)
         if content is None:
             raise MailUsageError, _("""
 Roundup requires the submission to be plain text. The message parser could
 not find a text/plain part to use.
 """)
 
-        # figure how much we should muck around with the email body
-        keep_citations = config['MAILGW_KEEP_QUOTED_TEXT']
-        keep_body = config['MAILGW_LEAVE_BODY_UNCHANGED']
-
         # parse the body of the message, stripping out bits as appropriate
-        summary, content = parseContent(content, keep_citations,
-            keep_body)
+        summary, content = parseContent(content, config=config)
         content = content.strip()
 
         #
@@ -1085,7 +1346,7 @@
                 cl.set(nodeid, **props)
             else:
                 nodeid = cl.create(**props)
-        except (TypeError, IndexError, ValueError), message:
+        except (TypeError, IndexError, ValueError, exceptions.Reject), message:
             raise MailUsageError, _("""
 There was a problem with the message you sent:
    %(message)s
@@ -1193,30 +1454,45 @@
     else:
         return 0
 
+def parseContent(content, keep_citations=None, keep_body=None, config=None):
+    """Parse mail message; return message summary and stripped content
+
+    The message body is divided into sections by blank lines.
+    Sections where the second and all subsequent lines begin with a ">"
+    or "|" character are considered "quoting sections". The first line of
+    the first non-quoting section becomes the summary of the message.
+
+    Arguments:
+
+        keep_citations: declared for backward compatibility.
+            If omitted or None, use config["MAILGW_KEEP_QUOTED_TEXT"]
+
+        keep_body: declared for backward compatibility.
+            If omitted or None, use config["MAILGW_LEAVE_BODY_UNCHANGED"]
+
+        config: tracker configuration object.
+            If omitted or None, use default configuration.
+
+    """
+    if config is None:
+        config = configuration.CoreConfig()
+    if keep_citations is None:
+        keep_citations = config["MAILGW_KEEP_QUOTED_TEXT"]
+    if keep_body is None:
+        keep_body = config["MAILGW_LEAVE_BODY_UNCHANGED"]
+    eol = config["MAILGW_EOL_RE"]
+    signature = config["MAILGW_SIGN_RE"]
+    original_msg = config["MAILGW_ORIGMSG_RE"]
 
-def parseContent(content, keep_citations, keep_body,
-        blank_line=re.compile(r'[\r\n]+\s*[\r\n]+'),
-        eol=re.compile(r'[\r\n]+'),
-        signature=re.compile(r'^[>|\s]*-- ?$'),
-        original_msg=re.compile(r'^[>|\s]*-----\s?Original Message\s?-----$')):
-    ''' The message body is divided into sections by blank lines.
-        Sections where the second and all subsequent lines begin with a ">"
-        or "|" character are considered "quoting sections". The first line of
-        the first non-quoting section becomes the summary of the message.
-
-        If keep_citations is true, then we keep the "quoting sections" in the
-        content.
-        If keep_body is true, we even keep the signature sections.
-    '''
     # strip off leading carriage-returns / newlines
     i = 0
     for i in range(len(content)):
         if content[i] not in '\r\n':
             break
     if i > 0:
-        sections = blank_line.split(content[i:])
+        sections = config["MAILGW_BLANKLINE_RE"].split(content[i:])
     else:
-        sections = blank_line.split(content)
+        sections = config["MAILGW_BLANKLINE_RE"].split(content)
 
     # extract out the summary from the message
     summary = ''

Modified: tracker/roundup-src/roundup/roundupdb.py
==============================================================================
--- tracker/roundup-src/roundup/roundupdb.py	(original)
+++ tracker/roundup-src/roundup/roundupdb.py	Sun Mar  9 09:26:16 2008
@@ -16,7 +16,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-# $Id: roundupdb.py,v 1.128 2006/12/18 11:34:41 a1s Exp $
+# $Id: roundupdb.py,v 1.138 2008/01/08 20:58:31 richard Exp $
 
 """Extending hyperdb with types specific to issue-tracking.
 """
@@ -24,6 +24,7 @@
 
 import re, os, smtplib, socket, time, random
 import cStringIO, base64, quopri, mimetypes
+import os.path
 
 from rfc2822 import encode_header
 
@@ -141,10 +142,10 @@
     #
     # Note that this list also includes properties
     # defined in the classic template:
-    # assignedto, topic, priority, status.
+    # assignedto, keyword, priority, status.
     (
         ''"title", ''"messages", ''"files", ''"nosy", ''"superseder",
-        ''"assignedto", ''"topic", ''"priority", ''"status",
+        ''"assignedto", ''"keyword", ''"priority", ''"status",
         # following properties are common for all hyperdb classes
         # they are listed here to keep things in one place
         ''"actor", ''"activity", ''"creator", ''"creation",
@@ -287,18 +288,17 @@
             authaddr = users.get(authid, 'address', '')            
         elif msgid:
             authid = messages.get(msgid, 'author')
-            authname = users.get(authid, 'realname')
-            if not authname:
-                authname = users.get(authid, 'username', '')
-            authaddr = users.get(authid, 'address', '')
         else:
-            # "system message"
-            authid = None
-            authname = 'admin'
-            authaddr = self.db.config.ADMIN_EMAIL
+            authid = self.db.getuid()
+        authname = users.get(authid, 'realname')
+        if not authname:
+            authname = users.get(authid, 'username', '')
+        authaddr = users.get(authid, 'address', '')
 
-        if authaddr:
+        if authaddr and self.db.config.MAIL_ADD_AUTHOREMAIL:
             authaddr = " <%s>" % straddr( ('',authaddr) )
+        elif authaddr:
+            authaddr = ""
 
         # make the message body
         m = ['']
@@ -308,24 +308,38 @@
             m.append(self.email_signature(nodeid, msgid))
 
         # add author information
-        if authid:
+        if authid and self.db.config.MAIL_ADD_AUTHORINFO:
             if msgid and len(self.get(nodeid,'messages')) == 1:
-                m.append(_("New submission from %(authname)s:")
+                m.append(_("New submission from %(authname)s%(authaddr)s:")
                     % locals())
             elif msgid:
-                m.append(_("%(authname)s added the comment:")
+                m.append(_("%(authname)s%(authaddr)s added the comment:")
                     % locals())
             else:
-                m.append(_("Changes by %(authname)s:")
+                m.append(_("Changes by %(authname)s%(authaddr)s:")
                          % locals())
-        else:
-            m.append(_("System message:"))
-        m.append('')
+            m.append('')
 
         # add the content
         if msgid is not None:
             m.append(messages.get(msgid, 'content', ''))
 
+        # get the files for this message
+        message_files = []
+        if msgid :
+            for fileid in messages.get(msgid, 'files') :
+                # check the attachment size
+                filename = self.db.filename('file', fileid, None)
+                filesize = os.path.getsize(filename)
+                if filesize <= self.db.config.NOSY_MAX_ATTACHMENT_SIZE:
+                    message_files.append(fileid)
+                else:
+                    base = self.db.config.TRACKER_WEB
+                    link = "".join((base, files.classname, fileid))
+                    filename = files.get(fileid, 'name')
+                    m.append(_("File '%(filename)s' not attached - "
+                        "you can download it from %(link)s.") % locals())
+
         # add the change note
         if note:
             m.append(note)
@@ -344,12 +358,6 @@
         quopri.encode(content, content_encoded, 0)
         content_encoded = content_encoded.getvalue()
 
-        # get the files for this message
-        if msgid is None:
-            message_files = None
-        else:
-            message_files = messages.get(msgid, 'files')
-
         # make sure the To line is always the same (for testing mostly)
         sendto.sort()
 
@@ -431,6 +439,36 @@
                         writer.addheader('In-Reply-To', inreplyto)
             # end additional headers
 
+            # Generate a header for each link or multilink to
+            # a class that has a name attribute
+            for propname, prop in self.getprops().items():
+                if not isinstance(prop, (hyperdb.Link, hyperdb.Multilink)):
+                    continue
+                cl = self.db.getclass(prop.classname)
+                if not 'name' in cl.getprops():
+                    continue
+                if isinstance(prop, hyperdb.Link):
+                    value = self.get(nodeid, propname)
+                    if value is None:
+                        continue
+                    values = [value]
+                else:
+                    values = self.get(nodeid, propname)
+                    if not values:
+                        continue
+                values = [cl.get(v, 'name') for v in values]
+                values = ', '.join(values)
+                writer.addheader("X-Roundup-%s-%s" % (self.classname, propname),
+                                 values)
+            if not inreplyto:
+                # Default the reply to the first message
+                msgs = self.get(nodeid, 'messages')
+                # Assume messages are sorted by increasing message number here
+                if msgs[0] != nodeid:
+                    inreplyto = messages.get(msgs[0], 'messageid')
+                    if inreplyto:
+                        writer.addheader('In-Reply-To', inreplyto)
+
             # attach files
             if message_files:
                 part = writer.startmultipartbody('mixed')

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	Sun Mar  9 09:26:16 2008
@@ -17,19 +17,25 @@
 
 """Command-line script that runs a server over roundup.cgi.client.
 
-$Id: roundup_server.py,v 1.87 2006/12/18 05:56:49 a1s Exp $
+$Id: roundup_server.py,v 1.94 2007/09/25 04:27:12 jpend Exp $
 """
 __docformat__ = 'restructuredtext'
 
 import errno, cgi, getopt, os, socket, sys, traceback, urllib, time
 import ConfigParser, BaseHTTPServer, SocketServer, StringIO
 
+try:
+    from OpenSSL import SSL
+except ImportError:
+    SSL = None
+
 # python version check
 from roundup import configuration, version_check
 from roundup import __version__ as roundup_version
 
 # Roundup modules of use here
 from roundup.cgi import cgitb, client
+from roundup.cgi.PageTemplates.PageTemplate import PageTemplate
 import roundup.instance
 from roundup.i18n import _
 
@@ -66,6 +72,82 @@
     MULTIPROCESS_TYPES.append("fork")
 DEFAULT_MULTIPROCESS = MULTIPROCESS_TYPES[-1]
 
+def auto_ssl():
+    print _('WARNING: generating temporary SSL certificate')
+    import OpenSSL, time, random, sys
+    pkey = OpenSSL.crypto.PKey()
+    pkey.generate_key(OpenSSL.crypto.TYPE_RSA, 768)
+    cert = OpenSSL.crypto.X509()
+    cert.set_serial_number(random.randint(0, sys.maxint))
+    cert.gmtime_adj_notBefore(0)
+    cert.gmtime_adj_notAfter(60 * 60 * 24 * 365) # one year
+    cert.get_subject().CN = '*'
+    cert.get_subject().O = 'Roundup Dummy Certificate'
+    cert.get_issuer().CN = 'Roundup Dummy Certificate Authority'
+    cert.get_issuer().O = 'Self-Signed'
+    cert.set_pubkey(pkey)
+    cert.sign(pkey, 'md5')
+    ctx = SSL.Context(SSL.SSLv23_METHOD)
+    ctx.use_privatekey(pkey)
+    ctx.use_certificate(cert)
+
+    return ctx
+
+class SecureHTTPServer(BaseHTTPServer.HTTPServer):
+    def __init__(self, server_address, HandlerClass, ssl_pem=None):
+        assert SSL, "pyopenssl not installed"
+        BaseHTTPServer.HTTPServer.__init__(self, server_address, HandlerClass)
+        self.socket = socket.socket(self.address_family, self.socket_type)
+        if ssl_pem:
+            ctx = SSL.Context(SSL.SSLv23_METHOD)
+            ctx.use_privatekey_file(ssl_pem)
+            ctx.use_certificate_file(ssl_pem)
+        else:
+            ctx = auto_ssl()
+        self.ssl_context = ctx
+        self.socket = SSL.Connection(ctx, self.socket)
+        self.server_bind()
+        self.server_activate()
+
+    def get_request(self):
+        (conn, info) = self.socket.accept()
+        if self.ssl_context:
+
+            class RetryingFile(object):
+                """ SSL.Connection objects can return Want__Error
+                    on recv/write, meaning "try again". We'll handle
+                    the try looping here """
+                def __init__(self, fileobj):
+                    self.__fileobj = fileobj
+
+                def readline(self, *args):
+                    """ SSL.Connection can return WantRead """
+                    line = None
+                    while not line:
+                        try:
+                            line = self.__fileobj.readline(*args)
+                        except SSL.WantReadError:
+                            line = None
+                    return line
+
+                def __getattr__(self, attrib):
+                    return getattr(self.__fileobj, attrib)
+
+            class ConnFixer(object):
+                """ wraps an SSL socket so that it implements makefile
+                    which the HTTP handlers require """
+                def __init__(self, conn):
+                    self.__conn = conn
+                def makefile(self, mode, bufsize):
+                    fo = socket._fileobject(self.__conn, mode, bufsize)
+                    return RetryingFile(fo)
+
+                def __getattr__(self, attrib):
+                    return getattr(self.__conn, attrib)
+
+            conn = ConnFixer(conn)
+        return (conn, info)
+
 class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
     TRACKER_HOMES = {}
     TRACKERS = None
@@ -149,19 +231,33 @@
         if len(keys) == 1:
             self.send_response(302)
             self.send_header('Location', urllib.quote(keys[0]) + '/index')
+            self.end_headers()
         else:
             self.send_response(200)
+
         self.send_header('Content-Type', 'text/html')
         self.end_headers()
         w = self.wfile.write
-        w(_('<html><head><title>Roundup trackers index</title></head>\n'
-            '<body><h1>Roundup trackers index</h1><ol>\n'))
-        keys.sort()
-        for tracker in keys:
-            w('<li><a href="%(tracker_url)s/index">%(tracker_name)s</a>\n'%{
-                'tracker_url': urllib.quote(tracker),
-                'tracker_name': cgi.escape(tracker)})
-        w('</ol></body></html>')
+
+        if self.CONFIG and self.CONFIG['TEMPLATE']:
+            template = open(self.CONFIG['TEMPLATE']).read()
+            pt = PageTemplate()
+            pt.write(template)
+            extra = { 'trackers': self.TRACKERS,
+                'nothing' : None,
+                'true' : 1,
+                'false' : 0,
+            }
+            w(pt.pt_render(extra_context=extra))
+        else:
+            w(_('<html><head><title>Roundup trackers index</title></head>\n'
+                '<body><h1>Roundup trackers index</h1><ol>\n'))
+            keys.sort()
+            for tracker in keys:
+                w('<li><a href="%(tracker_url)s/index">%(tracker_name)s</a>\n'%{
+                    'tracker_url': urllib.quote(tracker),
+                    'tracker_name': cgi.escape(tracker)})
+            w('</ol></body></html>')
 
     def inner_run_cgi(self):
         ''' This is the inner part of the CGI handling
@@ -216,7 +312,7 @@
 
         # figure the tracker
         l_path = rest.split('/')
-        tracker_name = urllib.unquote(l_path[1])
+        tracker_name = urllib.unquote(l_path[1]).lower()
 
         # handle missing trailing '/'
         if len(l_path) == 2:
@@ -393,6 +489,13 @@
             (configuration.Option, "multiprocess", DEFAULT_MULTIPROCESS,
                 "Set processing of each request in separate subprocess.\n"
                 "Allowed values: %s." % ", ".join(MULTIPROCESS_TYPES)),
+            (configuration.NullableFilePathOption, "template", "",
+                "Tracker index template. If unset, built-in will be used."),
+            (configuration.BooleanOption, "ssl", "no",
+                "Enable SSL support (requires pyopenssl)"),
+            (configuration.NullableFilePathOption, "pem", "",
+                "PEM file used for SSL. A temporary self-signed certificate\n"
+                "will be used if left blank."),
         )),
         ("trackers", (), "Roundup trackers to serve.\n"
             "Each option in this section defines single Roundup tracker.\n"
@@ -413,6 +516,9 @@
         "nodaemon": "D",
         "log_hostnames": "N",
         "multiprocess": "t:",
+        "template": "i:",
+        "ssl": "s",
+        "pem": "e:",
     }
 
     def __init__(self, config_file=None):
@@ -476,28 +582,38 @@
             DEBUG_MODE = self["MULTIPROCESS"] == "debug"
             CONFIG = self
 
+        if self["SSL"]:
+            base_server = SecureHTTPServer
+        else:
+            base_server = BaseHTTPServer.HTTPServer
+
         # obtain request server class
         if self["MULTIPROCESS"] not in MULTIPROCESS_TYPES:
             print _("Multiprocess mode \"%s\" is not available, "
                 "switching to single-process") % self["MULTIPROCESS"]
             self["MULTIPROCESS"] = "none"
-            server_class = BaseHTTPServer.HTTPServer
+            server_class = base_server
         elif self["MULTIPROCESS"] == "fork":
             class ForkingServer(SocketServer.ForkingMixIn,
-                BaseHTTPServer.HTTPServer):
+                base_server):
                     pass
             server_class = ForkingServer
         elif self["MULTIPROCESS"] == "thread":
             class ThreadingServer(SocketServer.ThreadingMixIn,
-                BaseHTTPServer.HTTPServer):
+                base_server):
                     pass
             server_class = ThreadingServer
         else:
-            server_class = BaseHTTPServer.HTTPServer
+            server_class = base_server
+
         # obtain server before changing user id - allows to
         # use port < 1024 if started as root
         try:
-            httpd = server_class((self["HOST"], self["PORT"]), RequestHandler)
+            args = ((self["HOST"], self["PORT"]), RequestHandler)
+            kwargs = {}
+            if self["SSL"]:
+                kwargs['ssl_pem'] = self["PEM"]
+            httpd = server_class(*args, **kwargs)
         except socket.error, e:
             if e[0] == errno.EADDRINUSE:
                 raise socket.error, \
@@ -594,6 +710,9 @@
  -p <port>     set the port to listen on (default: %(port)s)
  -l <fname>    log to the file indicated by fname instead of stderr/stdout
  -N            log client machine names instead of IP addresses (much slower)
+ -i <fname>    set tracker index template
+ -s            enable SSL
+ -e <fname>    PEM file containing SSL key and certificate
  -t <mode>     multiprocess mode (default: %(mp_def)s).
                Allowed values: %(mp_types)s.
 %(os_part)s
@@ -796,4 +915,4 @@
 if __name__ == '__main__':
     run()
 
-# vim: set filetype=python sts=4 sw=4 et si :
+# vim: sts=4 sw=4 et si

Modified: tracker/roundup-src/scripts/import_sf.py
==============================================================================
--- tracker/roundup-src/scripts/import_sf.py	(original)
+++ tracker/roundup-src/scripts/import_sf.py	Sun Mar  9 09:26:16 2008
@@ -224,7 +224,7 @@
         d['creator'] = users[artifact['submitted_by']]
         actor = d['creator']
         if categories[artifact['category']]:
-            d['topic'] = [categories[artifact['category']]]
+            d['keyword'] = [categories[artifact['category']]]
         issue_journal.append((
             d['id'], d['creation'].get_tuple(), d['creator'], "'create'", {}
         ))

Modified: tracker/roundup-src/scripts/roundup-reminder
==============================================================================
--- tracker/roundup-src/scripts/roundup-reminder	(original)
+++ tracker/roundup-src/scripts/roundup-reminder	Sun Mar  9 09:26:16 2008
@@ -19,7 +19,7 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
-# $Id: roundup-reminder,v 1.8 2004/02/11 00:21:46 richard Exp $
+# $Id: roundup-reminder,v 1.9 2007/02/15 03:52:35 richard Exp $
 
 '''
 Simple script that emails all users of a tracker with the issues that
@@ -96,7 +96,7 @@
     body = part.startbody('text/plain')
     
     # do the plain text bit
-    print >>body, 'Created     ID   Urgency   Title'
+    print >>body, 'Created     ID   Activity  Title'
     print >>body, '='*75
     #             '2 months    213  immediate cc_daemon barfage
     old_priority = None
@@ -106,10 +106,10 @@
             old_priority = priority
             print >>body, '    ', db.priority.get(priority,'name')
         # pretty creation
-        creation = (date.Date('.') - creation_date).pretty()
+        creation = (creation_date - date.Date('.')).pretty()
         if creation is None:
             creation = creation_date.pretty()
-        activity = (date.Date('.') - activity_date).pretty()
+        activity = (activity_date - date.Date('.')).pretty()
         title = db.issue.get(issue_id, 'title')
         if len(title) > 42:
             title = title[:38] + ' ...'
@@ -147,11 +147,11 @@
            print >>body, '<tr><td>-></td><td>-></td><td>-></td><td><b>%s</b></td></tr>'%db.priority.get(priority,'name')
         creation = (date.Date('.') - creation_date).pretty()
         if creation is None:
-            creation = creation_date.pretty()
+            creation = (creation_date - date.Date('.')).pretty()
         title = db.issue.get(issue_id, 'title')
         issue_id = '<a href="%sissue%s">%s</a>'%(db.config.TRACKER_WEB,
             issue_id, issue_id)
-        activity = (date.Date('.') - activity_date).pretty()
+        activity = (activity_date - date.Date('.')).pretty()
         print >>body, '''<tr><td>%s</td><td>%s</td><td>%s</td>
     <td>%s</td></tr>'''%(creation, issue_id, activity, title)
     print >>body, '</table>'

Modified: tracker/roundup-src/setup.py
==============================================================================
--- tracker/roundup-src/setup.py	(original)
+++ tracker/roundup-src/setup.py	Sun Mar  9 09:26:16 2008
@@ -16,7 +16,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-# $Id: setup.py,v 1.96 2006/12/19 03:03:36 richard Exp $
+# $Id: setup.py,v 1.99 2007/11/07 21:24:24 richard Exp $
 
 from distutils.core import setup, Extension
 from distutils.util import get_platform
@@ -219,6 +219,11 @@
     finally:
         f.close()
     err = [line for line in manifest if not os.path.exists(line)]
+    err.sort()
+    # ignore auto-generated files
+    if err == ['roundup-admin', 'roundup-demo', 'roundup-gettext',
+            'roundup-mailgw', 'roundup-server']:
+        err = []
     if err:
         n = len(manifest)
         print '\n*** SOURCE WARNING: There are files missing (%d/%d found)!'%(
@@ -347,25 +352,35 @@
 '''In this release
 ===============
 
-Fixed in 1.3.2:
+The metakit backend has been removed due to lack of maintenance and
+presence of good alternatives (in particular sqlite built into Python 2.5)
 
-- relax rules for required fields in form_parser.py (sf bug 1599740)
-- documentation cleanup from Luke Ross (sf patch 1594860)
-- updated Spanish translation from Ramiro Morales (sf patch 1594718)
-- handle 8-bit untranslateable messages in tracker templates
-- handling of required for boolean False and numeric 0 (sf bug 1608200)
-- removed bogus args attr of ConfigurationError (sf bug 1608056)
-- implemented start_response in roundup.cgi (sf bug 1604304)
-- clarified windows service documentation (sf patch 1597713)
-- HTMLClass fixed to work with new item permissions check (sf bug 1602983)
-- support POP over SSL (sf patch 1597703)
-- clean up input field generation and quoting of values (sf bug 1615616)
-- allow use of roundup-server pidfile without forking (sf bug 1614753)
-- allow translation of status/priority menu options (sf bug 1613976)
+Release 1.4.1 removes an old trace of the metakit backend that was
+preventing new tracker installation.
 
-New Features in 1.3.0:
+New Features in 1.4.0:
 
-- WSGI support via roundup.cgi.wsgi_handler
+- Roundup has a new xmlrpc frontend that gives access to a tracker using
+  XMLRPC.
+- Dates can now be in the year-range 1-9999
+- Add simple anti-spam recipe to docs
+- Allow customisation of regular expressions used in email parsing, thanks
+  Bruno Damour
+- Italian translation by Marco Ghidinelli
+- Multilinks take any iterable
+- config option: specify port and local hostname for SMTP connections
+- Tracker index templating (i.e. when roundup_server is serving multiple
+  trackers) (sf bug 1058020)
+- config option: Limit nosy attachments based on size (Philipp Gortan)
+- roundup_server supports SSL via pyopenssl
+- templatable 404 not found messages (sf bug 1403287)
+- Unauthorized email includes a link to the registration page for
+  the tracker
+- config options: control whether author info/email is included in email
+  sent by roundup
+- support for receiving OpenPGP MIME messages (signed or encrypted)
+
+There's also a ton of bugfixes.
 
 If you're upgrading from an older version of Roundup you *must* follow
 the "Software Upgrade" guidelines given in the maintenance documentation.

Modified: tracker/roundup-src/templates/classic/detectors/messagesummary.py
==============================================================================
--- tracker/roundup-src/templates/classic/detectors/messagesummary.py	(original)
+++ tracker/roundup-src/templates/classic/detectors/messagesummary.py	Sun Mar  9 09:26:16 2008
@@ -1,4 +1,4 @@
-#$Id: messagesummary.py,v 1.1 2003/04/17 03:26:38 richard Exp $
+#$Id: messagesummary.py,v 1.2 2007/04/03 06:47:21 a1s Exp $
 
 from roundup.mailgw import parseContent
 
@@ -8,7 +8,7 @@
     if newvalues.has_key('summary') or not newvalues.has_key('content'):
         return
 
-    summary, content = parseContent(newvalues['content'], 1, 1)
+    summary, content = parseContent(newvalues['content'], config=db.config)
     newvalues['summary'] = summary
 
 

Modified: tracker/roundup-src/templates/classic/detectors/userauditor.py
==============================================================================
--- tracker/roundup-src/templates/classic/detectors/userauditor.py	(original)
+++ tracker/roundup-src/templates/classic/detectors/userauditor.py	Sun Mar  9 09:26:16 2008
@@ -18,27 +18,77 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 #
-#$Id: userauditor.py,v 1.3 2006/09/18 03:24:38 tobias-herp Exp $
+#$Id: userauditor.py,v 1.9 2007/09/12 21:11:13 jpend Exp $
+
+import re
+
+# regular expression thanks to: http://www.regular-expressions.info/email.html
+# this is the "99.99% solution for syntax only".
+email_regexp = (r"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*", r"(localhost|(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9]))")
+email_rfc = re.compile('^' + email_regexp[0] + '@' + email_regexp[1] + '$', re.IGNORECASE)
+email_local = re.compile('^' + email_regexp[0] + '$', re.IGNORECASE)
+
+def valid_address(address):
+    ''' If we see an @-symbol in the address then check against the full
+        RFC syntax. Otherwise it is a local-only address so only check
+        the local part of the RFC syntax.
+    '''
+    if '@' in address:
+        return email_rfc.match(address)
+    else:
+        return email_local.match(address)
+
+def get_addresses(user):
+    ''' iterate over all known addresses in a newvalues dict
+        this takes of the address/alterate_addresses handling
+    '''
+    if user.has_key('address'):
+        yield user['address']
+    if user.get('alternate_addresses', None):
+        for address in user['alternate_addresses'].split('\n'):
+            yield address
 
 def audit_user_fields(db, cl, nodeid, newvalues):
     ''' Make sure user properties are valid.
 
-        - email address has no spaces in it
+        - email address is syntactically valid
+        - email address is unique
         - roles specified exist
+        - timezone is valid
     '''
-    if newvalues.has_key('address') and ' ' in newvalues['address']:
-        raise ValueError, 'Email address must not contain spaces'
 
-    if newvalues.has_key('roles') and newvalues['roles']:
-        roles = [x.lower().strip() for x in newvalues['roles'].split(',')]
-        for rolename in roles:
-            if not db.security.role.has_key(rolename):
+    for address in get_addresses(newvalues):
+        if not valid_address(address):
+            raise ValueError, 'Email address syntax is invalid'
+
+        check_main = db.user.stringFind(address=address)
+        # make sure none of the alts are owned by anyone other than us (x!=nodeid)
+        check_alts = [x for x in db.user.filter(None, {'alternate_addresses' : address}) if x != nodeid]
+        if check_main or check_alts:
+            raise ValueError, 'Email address %s already in use' % address
+
+    for rolename in [r.lower().strip() for r in newvalues.get('roles', '').split(',')]:
+            if rolename and not db.security.role.has_key(rolename):
                 raise ValueError, 'Role "%s" does not exist'%rolename
 
+    tz = newvalues.get('timezone', None)
+    if tz:
+        # if they set a new timezone validate the timezone by attempting to
+        # use it before we store it to the db.
+        import roundup.date
+        import datetime
+        try:
+            TZ = roundup.date.get_timezone(tz)
+            dt = datetime.datetime.now()
+            local = TZ.localize(dt).utctimetuple()
+        except IOError:
+            raise ValueError, 'Timezone "%s" does not exist' % tz
+        except ValueError:
+            raise ValueError, 'Timezone "%s" exceeds valid range [-23...23]' % tz
 
 def init(db):
     # fire before changes are made
     db.user.audit('set', audit_user_fields)
     db.user.audit('create', audit_user_fields)
 
-# vim: set filetype=python ts=4 sw=4 et si
+# vim: sts=4 sw=4 et si

Modified: tracker/roundup-src/templates/classic/html/help_controls.js
==============================================================================
--- tracker/roundup-src/templates/classic/html/help_controls.js	(original)
+++ tracker/roundup-src/templates/classic/html/help_controls.js	Sun Mar  9 09:26:16 2008
@@ -1,4 +1,4 @@
-// initial values for either Nosy, Superseder, Topic and Waiting On,
+// initial values for either Nosy, Superseder, Keyword and Waiting On,
 // depending on which has called
 original_field = form[field].value;
 

Modified: tracker/roundup-src/templates/classic/html/issue.index.html
==============================================================================
--- tracker/roundup-src/templates/classic/html/issue.index.html	(original)
+++ tracker/roundup-src/templates/classic/html/issue.index.html	Sun Mar  9 09:26:16 2008
@@ -1,4 +1,4 @@
-<!-- $Id: issue.index.html,v 1.27 2006/11/09 01:26:28 richard Exp $ -->
+<!-- $Id: issue.index.html,v 1.29 2007/09/18 17:44:26 jpend Exp $ -->
 <tal:block metal:use-macro="templates/page/macros/icing">
 <title metal:fill-slot="head_title" >
   <span tal:omit-tag="true" i18n:translate="" >List of issues</span>
@@ -29,7 +29,7 @@
    <th tal:condition="request/show/creation" i18n:translate="">Creation</th>
    <th tal:condition="request/show/activity" i18n:translate="">Activity</th>
    <th tal:condition="request/show/actor" i18n:translate="">Actor</th>
-   <th tal:condition="request/show/topic" i18n:translate="">Topic</th>
+   <th tal:condition="request/show/keyword" i18n:translate="">Keyword</th>
    <th tal:condition="request/show/title" i18n:translate="">Title</th>
    <th tal:condition="request/show/status" i18n:translate="">Status</th>
    <th tal:condition="request/show/creator" i18n:translate="">Creator</th>
@@ -40,7 +40,7 @@
       tal:condition="python:group and batch.propchanged(*group)">
    <th tal:attributes="colspan python:len(request.columns)" class="group">
     <tal:block tal:repeat="g group">
-     <tal:block tal:content="python:str(i[g]) or '(no %s set)'%g"/>
+     <tal:block i18n:translate="" tal:content="python:str(i[g]) or '(no %s set)'%g"/>
     </tal:block>
    </th>
   </tr>
@@ -55,13 +55,14 @@
        tal:content="i/activity/reldate">&nbsp;</td>
    <td class="date" tal:condition="request/show/actor"
        tal:content="python:i.actor.plain() or default">&nbsp;</td>
-   <td tal:condition="request/show/topic"
-       tal:content="python:i.topic.plain() or default">&nbsp;</td>
+   <td tal:condition="request/show/keyword"
+       tal:content="python:i.keyword.plain() or default">&nbsp;</td>
    <td tal:condition="request/show/title">
     <a tal:attributes="href string:issue${i/id}"
 		tal:content="python:str(i.title.plain(hyperlink=0)) or '[no title]'">title</a>
    </td>
    <td tal:condition="request/show/status"
+       i18n:translate=""
        tal:content="python:i.status.plain() or default">&nbsp;</td>
    <td tal:condition="request/show/creator"
        tal:content="python:i.creator.plain() or default">&nbsp;</td>

Modified: tracker/roundup-src/templates/classic/html/issue.item.html
==============================================================================
--- tracker/roundup-src/templates/classic/html/issue.item.html	(original)
+++ tracker/roundup-src/templates/classic/html/issue.item.html	Sun Mar  9 09:26:16 2008
@@ -75,10 +75,10 @@
 <tr>
  <th i18n:translate="">Assigned To</th>
  <td tal:content="structure context/assignedto/menu">assignedto menu</td>
- <th i18n:translate="">Topics</th>
+ <th i18n:translate="">Keywords</th>
  <td>
-  <span tal:replace="structure context/topic/field" />
-  <span tal:condition="context/is_edit_ok" tal:replace="structure python:db.keyword.classhelp(property='topic')" />
+  <span tal:replace="structure context/keyword/field" />
+  <span tal:condition="context/is_edit_ok" tal:replace="structure python:db.keyword.classhelp(property='keyword')" />
  </td>
 </tr>
 

Modified: tracker/roundup-src/templates/classic/html/issue.search.html
==============================================================================
--- tracker/roundup-src/templates/classic/html/issue.search.html	(original)
+++ tracker/roundup-src/templates/classic/html/issue.search.html	Sun Mar  9 09:26:16 2008
@@ -50,11 +50,14 @@
   <td>&nbsp;</td>
 </tr>
 
-<tr tal:define="name string:topic;
+<tr tal:define="name string:keyword;
                 db_klass string:keyword;
                 db_content string:name;">
-  <th i18n:translate="">Topic:</th>
-  <td metal:use-macro="search_select"></td>
+  <th i18n:translate="">Keyword:</th>
+  <td metal:use-macro="search_select">
+    <option metal:fill-slot="extra_options" value="-1" i18n:translate=""
+            tal:attributes="selected python:value == '-1'">not selected</option>
+  </td>
   <td metal:use-macro="column_input"></td>
   <td metal:use-macro="sort_input"></td>
   <td metal:use-macro="group_input"></td>

Modified: tracker/roundup-src/templates/classic/html/query.edit.html
==============================================================================
--- tracker/roundup-src/templates/classic/html/query.edit.html	(original)
+++ tracker/roundup-src/templates/classic/html/query.edit.html	Sun Mar  9 09:26:16 2008
@@ -57,8 +57,8 @@
  </tal:block>
 </tr>
 
-<tr tal:define="queries python:db.query.filter(filterspec={'private_for':uid})"
-     tal:repeat="query queries">
+<tr tal:repeat="query mine">
+ <tal:block condition="not:query/is_retired">
  <td><a tal:attributes="href string:${query/klass}?${query/url}"
         tal:content="query/name">query</a></td>
 
@@ -79,10 +79,12 @@
   <input type="button" value="Delete" i18n:attributes="value"
   tal:attributes="onClick python:'''retire('%s')'''%query.id">
   </td>
+  </tal:block>
 </tr>
 
 <tr tal:define="queries python:db.query.filter(filterspec={'private_for':None})"
      tal:repeat="query queries">
+ <tal:block condition="python: query.creator != uid">
  <td><a tal:attributes="href string:${query/klass}?${query/url}"
         tal:content="query/name">query</a></td>
 
@@ -93,7 +95,7 @@
  </td>
  <td tal:condition="not:query/is_edit_ok" colspan="3"
     i18n:translate="">[not yours to edit]</td>
-
+ </tal:block>
 </tr>
 
 <tr><td colspan="5">

Modified: tracker/roundup-src/templates/classic/html/user.item.html
==============================================================================
--- tracker/roundup-src/templates/classic/html/user.item.html	(original)
+++ tracker/roundup-src/templates/classic/html/user.item.html	Sun Mar  9 09:26:16 2008
@@ -43,6 +43,7 @@
 <div tal:condition="context/is_view_ok">
 
 <form method="POST"
+      name="itemSynopsis"
       tal:define="required python:'username address'.split()"
       enctype="multipart/form-data"
       tal:attributes="action context/designator;

Modified: tracker/roundup-src/templates/classic/html/user_utils.js
==============================================================================
--- tracker/roundup-src/templates/classic/html/user_utils.js	(original)
+++ tracker/roundup-src/templates/classic/html/user_utils.js	Sun Mar  9 09:26:16 2008
@@ -29,6 +29,9 @@
         case 'realname':
             realname=val
             break
+        case 'firstname':
+        case 'lastname':
+           return
         default:
             alert('Ooops - unknown name field '+that.name+'!')
             return
@@ -38,7 +41,7 @@
     function field_empty(name) {
         return the_form[name].value == ''
     }
-    
+
     // no break statements - on purpose!
     switch (that.name) {
         case 'address':

Modified: tracker/roundup-src/templates/classic/schema.py
==============================================================================
--- tracker/roundup-src/templates/classic/schema.py	(original)
+++ tracker/roundup-src/templates/classic/schema.py	Sun Mar  9 09:26:16 2008
@@ -71,7 +71,7 @@
 #   superseder = Multilink("issue")
 issue = IssueClass(db, "issue",
                 assignedto=Link("user"),
-                topic=Multilink("keyword"),
+                keyword=Multilink("keyword"),
                 priority=Link("priority"),
                 status=Link("status"))
 

Modified: tracker/roundup-src/templates/minimal/detectors/userauditor.py
==============================================================================
--- tracker/roundup-src/templates/minimal/detectors/userauditor.py	(original)
+++ tracker/roundup-src/templates/minimal/detectors/userauditor.py	Sun Mar  9 09:26:16 2008
@@ -18,27 +18,77 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 #
-#$Id: userauditor.py,v 1.2 2003/11/11 22:25:37 richard Exp $
+#$Id: userauditor.py,v 1.8 2007/09/12 21:11:14 jpend Exp $
+
+import re
+
+# regular expression thanks to: http://www.regular-expressions.info/email.html
+# this is the "99.99% solution for syntax only".
+email_regexp = (r"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*", r"(localhost|(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9]))")
+email_rfc = re.compile('^' + email_regexp[0] + '@' + email_regexp[1] + '$', re.IGNORECASE)
+email_local = re.compile('^' + email_regexp[0] + '$', re.IGNORECASE)
+
+def valid_address(address):
+    ''' If we see an @-symbol in the address then check against the full
+        RFC syntax. Otherwise it is a local-only address so only check
+        the local part of the RFC syntax.
+    '''
+    if '@' in address:
+        return email_rfc.match(address)
+    else:
+        return email_local.match(address)
+
+def get_addresses(user):
+    ''' iterate over all known addresses in a newvalues dict
+        this takes of the address/alterate_addresses handling
+    '''
+    if user.has_key('address'):
+        yield user['address']
+    if user.get('alternate_addresses', None):
+        for address in user['alternate_addresses'].split('\n'):
+            yield address
 
 def audit_user_fields(db, cl, nodeid, newvalues):
     ''' Make sure user properties are valid.
 
-        - email address has no spaces in it
+        - email address is syntactically valid
+        - email address is unique
         - roles specified exist
+        - timezone is valid
     '''
-    if newvalues.has_key('address') and ' ' in newvalues['address']:
-        raise ValueError, 'Email address must not contain spaces'
 
-    if newvalues.has_key('roles'):
-        roles = [x.lower().strip() for x in newvalues['roles'].split(',')]
-        for rolename in roles:
-            if not db.security.role.has_key(rolename):
+    for address in get_addresses(newvalues):
+        if not valid_address(address):
+            raise ValueError, 'Email address syntax is invalid'
+
+        check_main = db.user.stringFind(address=address)
+        # make sure none of the alts are owned by anyone other than us (x!=nodeid)
+        check_alts = [x for x in db.user.filter(None, {'alternate_addresses' : address}) if x != nodeid]
+        if check_main or check_alts:
+            raise ValueError, 'Email address %s already in use' % address
+
+    for rolename in [r.lower().strip() for r in newvalues.get('roles', '').split(',')]:
+            if rolename and not db.security.role.has_key(rolename):
                 raise ValueError, 'Role "%s" does not exist'%rolename
 
+    tz = newvalues.get('timezone', None)
+    if tz:
+        # if they set a new timezone validate the timezone by attempting to
+        # use it before we store it to the db.
+        import roundup.date
+        import datetime
+        try:
+            TZ = roundup.date.get_timezone(tz)
+            dt = datetime.datetime.now()
+            local = TZ.localize(dt).utctimetuple()
+        except IOError:
+            raise ValueError, 'Timezone "%s" does not exist' % tz
+        except ValueError:
+            raise ValueError, 'Timezone "%s" exceeds valid range [-23...23]' % tz
 
 def init(db):
     # fire before changes are made
     db.user.audit('set', audit_user_fields)
     db.user.audit('create', audit_user_fields)
 
-# vim: set filetype=python ts=4 sw=4 et si
+# vim: sts=4 sw=4 et si

Modified: tracker/roundup-src/templates/minimal/html/help_controls.js
==============================================================================
--- tracker/roundup-src/templates/minimal/html/help_controls.js	(original)
+++ tracker/roundup-src/templates/minimal/html/help_controls.js	Sun Mar  9 09:26:16 2008
@@ -1,4 +1,4 @@
-// initial values for either Nosy, Superseder, Topic and Waiting On,
+// initial values for either Nosy, Superseder, Keyword and Waiting On,
 // depending on which has called
 original_field = form[field].value;
 

Modified: tracker/roundup-src/templates/minimal/html/user.item.html
==============================================================================
--- tracker/roundup-src/templates/minimal/html/user.item.html	(original)
+++ tracker/roundup-src/templates/minimal/html/user.item.html	Sun Mar  9 09:26:16 2008
@@ -32,12 +32,18 @@
 
 <td class="content" metal:fill-slot="content">
 
-<p tal:condition="not:context/is_view_ok" i18n:translate="">You are not
-    allowed to view this page.</p>
+<p tal:condition="python:not (context.is_view_ok()
+ or request.user.hasRole('Anonymous'))" i18n:translate="">
+ You are not allowed to view this page.</p>
+
+<p tal:condition="python:not context.is_view_ok()
+ and request.user.hasRole('Anonymous')" i18n:translate="">
+ Please login with your username and password.</p>
 
 <div tal:condition="context/is_view_ok">
 
 <form method="POST"
+      name="itemSynopsis"
       tal:define="required python:'username address'.split()"
       enctype="multipart/form-data"
       tal:attributes="action context/designator;

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	Sun Mar  9 09:26:16 2008
@@ -15,13 +15,14 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-# $Id: db_test_base.py,v 1.82 2006/11/11 03:21:12 richard Exp $
+# $Id: db_test_base.py,v 1.96 2008/02/07 03:28:34 richard Exp $
 
-import unittest, os, shutil, errno, imp, sys, time, pprint, sets
+import unittest, os, shutil, errno, imp, sys, time, pprint, sets, base64, os.path
 
 from roundup.hyperdb import String, Password, Link, Multilink, Date, \
     Interval, DatabaseError, Boolean, Number, Node
-from roundup import date, password, init, instance, configuration
+from roundup.mailer import Mailer
+from roundup import date, password, init, instance, configuration, support
 
 from mocknull import MockNull
 
@@ -54,7 +55,8 @@
     except OSError, error:
         if error.errno not in (errno.ENOENT, errno.ESRCH): raise
     # create the instance
-    init.install(dirname, 'templates/classic')
+    init.install(dirname, os.path.join(os.path.dirname(__file__), '..',
+        'templates/classic'))
     init.write_select_db(dirname, backend)
     config.save(os.path.join(dirname, 'config.ini'))
     tracker = instance.open(dirname)
@@ -69,8 +71,8 @@
     priority = module.Class(db, "priority", name=String(), order=String())
     priority.setkey("name")
     user = module.Class(db, "user", username=String(), password=Password(),
-        assignable=Boolean(), age=Number(), roles=String(),
-        supervisor=Link('user'))
+        assignable=Boolean(), age=Number(), roles=String(), address=String(),
+        supervisor=Link('user'),realname=String())
     user.setkey("username")
     file = module.FileClass(db, "file", name=String(), type=String(),
         comment=String(indexme="yes"), fooz=Password())
@@ -82,14 +84,18 @@
     stuff = module.Class(db, "stuff", stuff=String())
     session = module.Class(db, 'session', title=String())
     msg = module.FileClass(db, "msg", date=Date(),
-                           author=Link("user", do_journal='no'))
+                           author=Link("user", do_journal='no'),
+                           files=Multilink('file'), inreplyto=String(),
+                           messageid=String(),
+                           recipients=Multilink("user", do_journal='no')
+                           )
     session.disableJournalling()
     db.post_init()
     if create:
         user.create(username="admin", roles='Admin',
             password=password.Password('sekrit'))
         user.create(username="fred", roles='User',
-            password=password.Password('sekrit'))
+            password=password.Password('sekrit'), address='fred at example.com')
         status.create(name="unread")
         status.create(name="in-progress")
         status.create(name="testing")
@@ -253,6 +259,42 @@
             m = self.db.issue.get(nid, "nosy"); m.sort()
             self.assertEqual(l, m)
 
+            # verify that when we pass None to an Multilink it sets
+            # it to an empty list
+            self.db.issue.set(nid, nosy=None)
+            if commit: self.db.commit()
+            self.assertEqual(self.db.issue.get(nid, "nosy"), [])
+
+    def testMultilinkChangeIterable(self):
+        for commit in (0,1):
+            # invalid nosy value assertion
+            self.assertRaises(IndexError, self.db.issue.create, title='spam',
+                nosy=['foo%s'%commit])
+            # invalid type for nosy create
+            self.assertRaises(TypeError, self.db.issue.create, title='spam',
+                nosy=1)
+            u1 = self.db.user.create(username='foo%s'%commit)
+            u2 = self.db.user.create(username='bar%s'%commit)
+            # try a couple of the built-in iterable types to make
+            # sure that we accept them and handle them properly
+            # try a set as input for the multilink
+            nid = self.db.issue.create(title="spam", nosy=set(u1))
+            if commit: self.db.commit()
+            self.assertEqual(self.db.issue.get(nid, "nosy"), [u1])
+            self.assertRaises(TypeError, self.db.issue.set, nid,
+                nosy='invalid type')
+            # test with a tuple
+            self.db.issue.set(nid, nosy=tuple())
+            if commit: self.db.commit()
+            self.assertEqual(self.db.issue.get(nid, "nosy"), [])
+            # make sure we accept a frozen set
+            self.db.issue.set(nid, nosy=frozenset([u1,u2]))
+            if commit: self.db.commit()
+            l = [u1,u2]; l.sort()
+            m = self.db.issue.get(nid, "nosy"); m.sort()
+            self.assertEqual(l, m)
+       
+
 # XXX one day, maybe...
 #    def testMultilinkOrdering(self):
 #        for i in range(10):
@@ -277,6 +319,15 @@
             if commit: self.db.commit()
             self.assertNotEqual(a, b)
             self.assertNotEqual(b, date.Date('1970-1-1.00:00:00'))
+            # The 1970 date will fail for metakit -- it is used
+            # internally for storing NULL. The others would, too
+            # because metakit tries to convert date.timestamp to an int
+            # for storing and fails with an overflow.
+            for d in [date.Date (x) for x in '2038', '1970', '0033', '9999']:
+                self.db.issue.set(nid, deadline=d)
+                if commit: self.db.commit()
+                c = self.db.issue.get(nid, "deadline")
+                self.assertEqual(c, d)
 
     def testDateUnset(self):
         for commit in (0,1):
@@ -498,6 +549,15 @@
         name2 = self.db.user.get('1', 'username')
         self.assertEqual(name1, name2)
 
+    def testDestroyBlob(self):
+        # destroy an uncommitted blob
+        f1 = self.db.file.create(content='hello', type="text/plain")
+        self.db.commit()
+        fn = self.db.filename('file', f1)
+        self.db.file.destroy(f1)
+        self.db.commit()
+        self.assertEqual(os.path.exists(fn), False)
+
     def testDestroyNoJournalling(self):
         self.innerTestDestroy(klass=self.db.session)
 
@@ -870,6 +930,47 @@
         self.assertEquals(self.db.indexer.search(['flebble'], self.db.issue),
             {'1': {}})
 
+    def testIndexingOnImport(self):
+        # import a message
+        msgcontent = 'Glrk'
+        msgid = self.db.msg.import_list(['content', 'files', 'recipients'],
+                                        [repr(msgcontent), '[]', '[]'])
+        msg_filename = self.db.filename(self.db.msg.classname, msgid,
+                                        create=1)
+        support.ensureParentsExist(msg_filename)
+        msg_file = open(msg_filename, 'w')
+        msg_file.write(msgcontent)
+        msg_file.close()
+
+        # import a file
+        filecontent = 'Brrk'
+        fileid = self.db.file.import_list(['content'], [repr(filecontent)])
+        file_filename = self.db.filename(self.db.file.classname, fileid,
+                                         create=1)
+        support.ensureParentsExist(file_filename)
+        file_file = open(file_filename, 'w')
+        file_file.write(filecontent)
+        file_file.close()
+
+        # import an issue
+        title = 'Bzzt'
+        nodeid = self.db.issue.import_list(['title', 'messages', 'files',
+            'spam', 'nosy', 'superseder'], [repr(title), repr([msgid]),
+            repr([fileid]), '[]', '[]', '[]'])
+        self.db.commit()
+
+        # Content of title attribute is indexed
+        self.assertEquals(self.db.indexer.search([title], self.db.issue),
+            {str(nodeid):{}})
+        # Content of message is indexed
+        self.assertEquals(self.db.indexer.search([msgcontent], self.db.issue),
+            {str(nodeid):{'messages':[str(msgid)]}})
+        # Content of file is indexed
+        self.assertEquals(self.db.indexer.search([filecontent], self.db.issue),
+            {str(nodeid):{'files':[str(fileid)]}})
+
+
+
     #
     # searching tests follow
     #
@@ -1087,11 +1188,21 @@
         ae(filt(None, {'deadline': '2002'}), [])
         ae(filt(None, {'deadline': '2003'}), ['1', '2', '3'])
         ae(filt(None, {'deadline': '2004'}), ['4'])
-        ae(filt(None, {'deadline': '2003-02'}), ['1', '3'])
-        ae(filt(None, {'deadline': '2003-03'}), [])
         ae(filt(None, {'deadline': '2003-02-16'}), ['1'])
         ae(filt(None, {'deadline': '2003-02-17'}), [])
 
+    def testFilteringRangeMonths(self):
+        ae, filt = self.filteringSetup()
+        for month in range(1, 13):
+            for n in range(1, month+1):
+                i = self.db.issue.create(title='%d.%d'%(month, n),
+                    deadline=date.Date('2001-%02d-%02d.00:00'%(month, n)))
+        self.db.commit()
+
+        for month in range(1, 13):
+            r = filt(None, dict(deadline='2001-%02d'%month))
+            assert len(r) == month, 'month %d != length %d'%(month, len(r))
+
     def testFilteringRangeInterval(self):
         ae, filt = self.filteringSetup()
         ae(filt(None, {'foo': 'from 0:50 to 2:00'}), ['1'])
@@ -1150,8 +1261,8 @@
 
     def testFilteringMultilinkSort(self):
         # 1: []                 Reverse:  1: []
-        # 2: []                           2: []              
-        # 3: ['admin','fred']             3: ['fred','admin']       
+        # 2: []                           2: []
+        # 3: ['admin','fred']             3: ['fred','admin']
         # 4: ['admin','bleep','fred']     4: ['fred','bleep','admin']
         # Note the sort order for the multilink doen't change when
         # reversing the sort direction due to the re-sorting of the
@@ -1442,7 +1553,7 @@
             ['1', '2', '3', '4', '5', '8', '6', '7'])
         ae(filt(None, {}, [('+','messages.author'), ('+','messages')]),
             ['6', '7', '8', '5', '4', '3', '1', '2'])
-        # The following will sort by 
+        # The following will sort by
         # author.supervisor.username and then by
         # author.username
         # I've resited the tempation to implement recursive orderprop
@@ -1475,15 +1586,33 @@
     def testImportExport(self):
         # use the filtering setup to create a bunch of items
         ae, filt = self.filteringSetup()
+        # Get some stuff into the journal for testing import/export of
+        # journal data:
+        self.db.user.set('4', password = password.Password('xyzzy'))
+        self.db.user.set('4', age = 3)
+        self.db.user.set('4', assignable = True)
+        self.db.issue.set('1', title = 'i1', status = '3')
+        self.db.issue.set('1', deadline = date.Date('2007'))
+        self.db.issue.set('1', foo = date.Interval('1:20'))
+        p = self.db.priority.create(name = 'some_prio_without_order')
+        self.db.commit()
+        self.db.user.set('4', password = password.Password('123xyzzy'))
+        self.db.user.set('4', assignable = False)
+        self.db.priority.set(p, order = '4711')
+        self.db.commit()
+
         self.db.user.retire('3')
         self.db.issue.retire('2')
 
         # grab snapshot of the current database
         orig = {}
+        origj = {}
         for cn,klass in self.db.classes.items():
             cl = orig[cn] = {}
+            jn = origj[cn] = {}
             for id in klass.list():
                 it = cl[id] = {}
+                jn[id] = self.db.getjournal(cn, id)
                 for name in klass.getprops().keys():
                     it[name] = klass.get(id, name)
 
@@ -1522,11 +1651,13 @@
                     maxid = max(maxid, id)
                 self.db.setid(cn, str(maxid+1))
                 klass.import_journals(journals[cn])
+            # This is needed, otherwise journals won't be there for anydbm
+            self.db.commit()
         finally:
             shutil.rmtree('_test_export')
 
         # compare with snapshot of the database
-        for cn, items in orig.items():
+        for cn, items in orig.iteritems():
             klass = self.db.classes[cn]
             propdefs = klass.getprops(1)
             # ensure retired items are retired :)
@@ -1546,6 +1677,19 @@
                             raise
                         # don't get hung up on rounding errors
                         assert not l.__cmp__(value, int_seconds=1)
+        for jc, items in origj.iteritems():
+            for id, oj in items.iteritems():
+                rj = self.db.getjournal(jc, id)
+                # Both mysql and postgresql have some minor issues with
+                # rounded seconds on export/import, so we compare only
+                # the integer part.
+                for j in oj:
+                    j[1].second = float(int(j[1].second))
+                for j in rj:
+                    j[1].second = float(int(j[1].second))
+                oj.sort()
+                rj.sort()
+                ae(oj, rj)
 
         # make sure the retired items are actually imported
         ae(self.db.user.get('4', 'username'), 'blop')
@@ -1600,6 +1744,38 @@
             'nosy', 'priority', 'spam', 'status', 'superseder'])
         self.assertEqual(self.db.issue.list(), ['1'])
 
+    def testNosyMail(self) :
+        """Creates one issue with two attachments, one smaller and one larger
+           than the set max_attachment_size.
+        """
+        db = self.db
+        db.config.NOSY_MAX_ATTACHMENT_SIZE = 4096
+        res = dict(mail_to = None, mail_msg = None)
+        def dummy_snd(s, to, msg, res=res) :
+            res["mail_to"], res["mail_msg"] = to, msg
+        backup, Mailer.smtp_send = Mailer.smtp_send, dummy_snd
+        try :
+            f1 = db.file.create(name="test1.txt", content="x" * 20)
+            f2 = db.file.create(name="test2.txt", content="y" * 5000)
+            m  = db.msg.create(content="one two", author="admin",
+                files = [f1, f2])
+            i  = db.issue.create(title='spam', files = [f1, f2],
+                messages = [m], nosy = [db.user.lookup("fred")])
+
+            db.issue.nosymessage(i, m, {})
+            mail_msg = res["mail_msg"].getvalue()
+            self.assertEqual(res["mail_to"], ["fred at example.com"])
+            self.failUnless("From: admin" in mail_msg)
+            self.failUnless("Subject: [issue1] spam" in mail_msg)
+            self.failUnless("New submission from admin" in mail_msg)
+            self.failUnless("one two" in mail_msg)
+            self.failIf("File 'test1.txt' not attached" in mail_msg)
+            self.failUnless(base64.b64encode("xxx") in mail_msg)
+            self.failUnless("File 'test2.txt' not attached" in mail_msg)
+            self.failIf(base64.b64encode("yyy") in mail_msg)
+        finally :
+            Mailer.smtp_send = backup
+
 class ROTest(MyTestCase):
     def setUp(self):
         # remove previous test, ignore errors
@@ -1716,8 +1892,10 @@
         self.assertEqual(self.db.a.get(aid, 'name'), 'apple')
         self.assertEqual(self.db.a.get(aid, 'newstr'), None)
         self.assertEqual(self.db.a.get(aid, 'newint'), None)
-        self.assertEqual(self.db.a.get(aid, 'newnum'), None)
-        self.assertEqual(self.db.a.get(aid, 'newbool'), None)
+        # hack - metakit can't return None for missing values, and we're not
+        # really checking for that behavior here anyway
+        self.assert_(not self.db.a.get(aid, 'newnum'))
+        self.assert_(not self.db.a.get(aid, 'newbool'))
         self.assertEqual(self.db.a.get(aid, 'newdate'), None)
         self.assertEqual(self.db.b.get(aid, 'name'), 'bear')
         aid2 = self.db.a.create(name='aardvark', newstr='booz')
@@ -1876,12 +2054,15 @@
 
         # check the basics of the schema and initial data set
         l = db.priority.list()
+        l.sort()
         ae(l, ['1', '2', '3', '4', '5'])
         l = db.status.list()
+        l.sort()
         ae(l, ['1', '2', '3', '4', '5', '6', '7', '8'])
         l = db.keyword.list()
         ae(l, [])
         l = db.user.list()
+        l.sort()
         ae(l, ['1', '2'])
         l = db.msg.list()
         ae(l, [])

Modified: tracker/roundup-src/test/test_actions.py
==============================================================================
--- tracker/roundup-src/test/test_actions.py	(original)
+++ tracker/roundup-src/test/test_actions.py	Sun Mar  9 09:26:16 2008
@@ -225,6 +225,83 @@
 
         self.assertLoginLeavesMessages([], 'foo', 'right')
 
+class EditItemActionTestCase(ActionTestCase):
+    def setUp(self):
+        ActionTestCase.setUp(self)
+        self.result = []
+        class AppendResult:
+            def __init__(inner_self, name):
+                inner_self.name = name
+            def __call__(inner_self, *args, **kw):
+                self.result.append((inner_self.name, args, kw))
+                if inner_self.name == 'set':
+                    return kw
+                return '17'
+
+        self.client.db.security.hasPermission = true
+        self.client.classname = 'issue'
+        self.client.base = 'http://tracker/'
+        self.client.nodeid = '4711'
+        self.client.template = 'item'
+        self.client.db.classes.create = AppendResult('create')
+        self.client.db.classes.set = AppendResult('set')
+        self.client.db.classes.getprops = lambda: \
+            ({'messages':hyperdb.Multilink('msg')
+             ,'content':hyperdb.String()
+             ,'files':hyperdb.Multilink('file')
+             })
+        self.action = EditItemAction(self.client)
+
+    def testMessageAttach(self):
+        expect = \
+            [ ('create',(),{'content':'t'})
+            , ('set',('4711',), {'messages':['23','42','17']})
+            ]
+        self.client.db.classes.get = lambda a, b:['23','42']
+        self.client.parsePropsFromForm = lambda: \
+            ( {('msg','-1'):{'content':'t'},('issue','4711'):{}}
+            , [('issue','4711','messages',[('msg','-1')])]
+            )
+        try :
+            self.action.handle()
+        except Redirect, msg:
+            pass
+        self.assertEqual(expect, self.result)
+
+    def testFileAttach(self):
+        expect = \
+            [('create',(),{'content':'t','type':'text/plain','name':'t.txt'})
+            ,('set',('4711',),{'files':['23','42','17']})
+            ]
+        self.client.db.classes.get = lambda a, b:['23','42']
+        self.client.parsePropsFromForm = lambda: \
+            ( {('file','-1'):{'content':'t','type':'text/plain','name':'t.txt'}
+              ,('issue','4711'):{}
+              }
+            , [('issue','4711','messages',[('msg','-1')])
+              ,('issue','4711','files',[('file','-1')])
+              ,('msg','-1','files',[('file','-1')])
+              ]
+            )
+        try :
+            self.action.handle()
+        except Redirect, msg:
+            pass
+        self.assertEqual(expect, self.result)
+
+    def testLinkExisting(self):
+        expect = [('set',('4711',),{'messages':['23','42','1']})]
+        self.client.db.classes.get = lambda a, b:['23','42']
+        self.client.parsePropsFromForm = lambda: \
+            ( {('issue','4711'):{},('msg','1'):{}}
+            , [('issue','4711','messages',[('msg','1')])]
+            )
+        try :
+            self.action.handle()
+        except Redirect, msg:
+            pass
+        self.assertEqual(expect, self.result)
+
 def test_suite():
     suite = unittest.TestSuite()
     suite.addTest(unittest.makeSuite(RetireActionTestCase))
@@ -233,6 +310,7 @@
     suite.addTest(unittest.makeSuite(ShowActionTestCase))
     suite.addTest(unittest.makeSuite(CollisionDetectionTestCase))
     suite.addTest(unittest.makeSuite(LoginTestCase))
+    suite.addTest(unittest.makeSuite(EditItemActionTestCase))
     return suite
 
 if __name__ == '__main__':

Modified: tracker/roundup-src/test/test_cgi.py
==============================================================================
--- tracker/roundup-src/test/test_cgi.py	(original)
+++ tracker/roundup-src/test/test_cgi.py	Sun Mar  9 09:26:16 2008
@@ -8,12 +8,13 @@
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 #
-# $Id: test_cgi.py,v 1.29 2006/12/11 23:36:15 richard Exp $
+# $Id: test_cgi.py,v 1.33 2007/10/05 03:07:14 richard Exp $
 
 import unittest, os, shutil, errno, sys, difflib, cgi, re
 
-from roundup.cgi import client
+from roundup.cgi import client, actions, exceptions
 from roundup.cgi.exceptions import FormError
+from roundup.cgi.templating import HTMLItem
 from roundup.cgi.form_parser import FormParser
 from roundup import init, instance, password, hyperdb, date
 
@@ -69,7 +70,7 @@
         self.db = self.instance.open('admin')
         self.db.user.create(username='Chef', address='chef at bork.bork.bork',
             realname='Bork, Chef', roles='User')
-        self.db.user.create(username='mary', address='mary at test',
+        self.db.user.create(username='mary', address='mary at test.test',
             roles='User', realname='Contrary, Mary')
 
         test = self.instance.backend.Class(self.db, "test",
@@ -191,6 +192,42 @@
         self.assertEqual(self.parseForm({'title': ' '}, 'issue', nodeid),
             ({('issue', nodeid): {'title': None}}, []))
 
+    def testStringLinkId(self):
+        self.db.status.set('1', name='2')
+        self.db.status.set('2', name='1')
+        issue = self.db.issue.create(title='i1-status1', status='1')
+        self.assertEqual(self.db.issue.get(issue,'status'),'1')
+        self.assertEqual(self.db.status.lookup('1'),'2')
+        self.assertEqual(self.db.status.lookup('2'),'1')
+        form = cgi.FieldStorage()
+        cl = client.Client(self.instance, None, {'PATH_INFO':'/'}, form)
+        cl.classname = 'issue'
+        cl.nodeid = issue
+        cl.db = self.db
+        item = HTMLItem(cl, 'issue', issue)
+        self.assertEqual(item.status.id, '1')
+        self.assertEqual(item.status.name, '2')
+
+    def testStringMultilinkId(self):
+        id = self.db.keyword.create(name='2')
+        self.assertEqual(id,'1')
+        id = self.db.keyword.create(name='1')
+        self.assertEqual(id,'2')
+        issue = self.db.issue.create(title='i1-status1', keyword=['1'])
+        self.assertEqual(self.db.issue.get(issue,'keyword'),['1'])
+        self.assertEqual(self.db.keyword.lookup('1'),'2')
+        self.assertEqual(self.db.keyword.lookup('2'),'1')
+        form = cgi.FieldStorage()
+        cl = client.Client(self.instance, None, {'PATH_INFO':'/'}, form)
+        cl.classname = 'issue'
+        cl.nodeid = issue
+        cl.db = self.db
+        cl.userid = '1'
+        item = HTMLItem(cl, 'issue', issue)
+        for keyword in item.keyword:
+            self.assertEqual(keyword.id, '1')
+            self.assertEqual(keyword.name, '2')
+
     def testFileUpload(self):
         file = FileUpload('foo', 'foo.txt')
         self.assertEqual(self.parseForm({'content': file}, 'file'),
@@ -558,6 +595,41 @@
             'name': 'foo.txt', 'type': 'text/plain'}},
             [('issue', None, 'files', [('file', '-1')])]))
 
+    #
+    # SECURITY
+    #
+    # XXX test all default permissions
+    def _make_client(self, form, classname='user', nodeid='2', userid='2'):
+        cl = client.Client(self.instance, None, {'PATH_INFO':'/'},
+            makeForm(form))
+        cl.classname = 'user'
+        cl.nodeid = '1'
+        cl.db = self.db
+        cl.userid = '2'
+        return cl
+
+    def testClassPermission(self):
+        cl = self._make_client(dict(username='bob'))
+        self.failUnlessRaises(exceptions.Unauthorised,
+            actions.EditItemAction(cl).handle)
+        cl.nodeid = '1'
+        self.assertRaises(exceptions.Unauthorised,
+            actions.EditItemAction(cl).handle)
+
+    def testCheckAndPropertyPermission(self):
+        self.db.security.permissions = {}
+        def own_record(db, userid, itemid): return userid == itemid
+        p = self.db.security.addPermission(name='Edit', klass='user',
+            check=own_record, properties=("password", ))
+        self.db.security.addPermissionToRole('User', p)
+
+        cl = self._make_client(dict(username='bob'))
+        self.assertRaises(exceptions.Unauthorised,
+            actions.EditItemAction(cl).handle)
+        cl = self._make_client({'password':'bob', '@confirm at password':'bob'})
+        self.failUnlessRaises(exceptions.Unauthorised,
+            actions.EditItemAction(cl).handle)
+
 def test_suite():
     suite = unittest.TestSuite()
     suite.addTest(unittest.makeSuite(FormTestCase))

Modified: tracker/roundup-src/test/test_dates.py
==============================================================================
--- tracker/roundup-src/test/test_dates.py	(original)
+++ tracker/roundup-src/test/test_dates.py	Sun Mar  9 09:26:16 2008
@@ -15,14 +15,20 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-# $Id: test_dates.py,v 1.39 2006/05/06 17:21:34 a1s Exp $
+# $Id: test_dates.py,v 1.44 2007/12/23 00:23:23 richard Exp $
 from __future__ import nested_scopes
 
-import unittest, time
+import unittest
+import time
+import datetime
+import calendar
+
+from roundup.date import Date, Interval, Range, fixTimeOverflow, \
+    get_timezone
 
-from roundup.date import Date, Interval, Range, fixTimeOverflow, get_timezone
 
 class DateTestCase(unittest.TestCase):
+
     def testDateInterval(self):
         ae = self.assertEqual
         date = Date("2000-06-26.00:34:02 + 2d")
@@ -59,11 +65,13 @@
         ae(str(date), '%s-%02d-%02d.08:47:11'%(y, m, d))
         ae(str(Date('2003')), '2003-01-01.00:00:00')
         ae(str(Date('2004-06')), '2004-06-01.00:00:00')
+        ae(str(Date('1900-02-01')), '1900-02-01.00:00:00')
+        ae(str(Date('1800-07-15')), '1800-07-15.00:00:00')
 
     def testDateError(self):
         self.assertRaises(ValueError, Date, "12")
-        # Date cannot handle dates before UNIX epoch
-        self.assertRaises(ValueError, Date, (1, 1, 1, 0, 0, 0.0, 0, 1, -1))
+        # Date cannot handle dates before year 1
+        self.assertRaises(ValueError, Date, (0, 1, 1, 0, 0, 0.0, 0, 1, -1))
         self.assertRaises(ValueError, Date, "1/1/06")
 
     def testOffset(self):
@@ -128,6 +136,8 @@
         ae(str(date), '2000-02-29.00:00:00')
         date = Date('2001-02-28.22:58:59') + Interval('00:00:3661')
         ae(str(date), '2001-03-01.00:00:00')
+        date = Date('2001-03-01.00:00:00') + Interval('150y')
+        ae(str(date), '2151-03-01.00:00:00')
 
     def testOffsetSub(self):
         ae = self.assertEqual
@@ -162,6 +172,8 @@
         ae(str(date), '2000-02-28.22:58:59')
         date = Date('2001-03-01.00:00:00') - Interval('00:00:3661')
         ae(str(date), '2001-02-28.22:58:59')
+        date = Date('2001-03-01.00:00:00') - Interval('150y')
+        ae(str(date), '1851-03-01.00:00:00')
 
     def testDateLocal(self):
         ae = self.assertEqual
@@ -269,6 +281,10 @@
         # force the transition over a year boundary
         i = Date('2003-01-01.00:00:00') - Date('2002-01-01.00:00:00')
         self.assertEqual(i, Interval('365d'))
+        i = Date('1952-01-01') - Date('1953-01-01')
+        self.assertEqual(i, Interval('-366d'))
+        i = Date('1953-01-01') - Date('1952-01-01')
+        self.assertEqual(i, Interval('366d'))
 
     def testIntervalAdd(self):
         ae = self.assertEqual
@@ -346,6 +362,7 @@
         ae(str(Date('2003-1-1.23:00', add_granularity=1)), '2003-01-01.23:00:59')
         ae(str(Date('2003', add_granularity=1)), '2003-12-31.23:59:59')
         ae(str(Date('2003-5', add_granularity=1)), '2003-05-31.23:59:59')
+        ae(str(Date('2003-12', add_granularity=1)), '2003-12-31.23:59:59')
         ae(str(Interval('+1w', add_granularity=1)), '+ 14d')
         ae(str(Interval('-2m 3w', add_granularity=1)), '- 2m 14d')
 
@@ -383,12 +400,10 @@
         ae('-2y', '2 years ago')
 
     def testPyDatetime(self):
-        try:
-            import datetime
-        except:
-            return
         d = datetime.datetime.now()
         Date(d)
+        toomuch = datetime.MAXYEAR + 1
+        self.assertRaises(ValueError, Date, (toomuch, 1, 1, 0, 0, 0, 0, 1, -1))
 
     def testSimpleTZ(self):
         ae = self.assertEqual
@@ -404,6 +419,27 @@
         date = Date(date, 2)
         ae(str(date), '2006-04-04.10:00:00')
 
+    def testTimestamp(self):
+        ae = self.assertEqual
+        date = Date('2038')
+        ae(date.timestamp(), 2145916800)
+        date = Date('1902')
+        ae(date.timestamp(), -2145916800)
+        date = Date(time.gmtime(0))
+        ae(date.timestamp(), 0)
+        ae(str(date), '1970-01-01.00:00:00')
+        date = Date(time.gmtime(0x7FFFFFFF))
+        ae(date.timestamp(), 2147483647)
+        ae(str(date), '2038-01-19.03:14:07')
+        date = Date('1901-12-13.20:45:52')
+        ae(date.timestamp(), -0x80000000L)
+        ae(str(date), '1901-12-13.20:45:52')
+        date = Date('9999')
+        ae (date.timestamp(), 253370764800.0)
+        date = Date('0033')
+        ae (date.timestamp(), -61125753600.0)
+        ae(str(date), '0033-01-01.00:00:00')
+
 class TimezoneTestCase(unittest.TestCase):
 
     def testTZ(self):
@@ -435,9 +471,25 @@
         date = Date(date, tz)
         ae(str(date), '2006-01-01.11:00:00')
 
+
+class RangeTestCase(unittest.TestCase):
+
+    def testRange(self):
+        ae = self.assertEqual
+        r = Range('2006', Date)
+        ae(str(r.from_value), '2006-01-01.00:00:00')
+        ae(str(r.to_value), '2006-12-31.23:59:59')
+        for i in range(1, 13):
+            r = Range('2006-%02d'%i, Date)
+            ae(str(r.from_value), '2006-%02d-01.00:00:00'%i)
+            ae(str(r.to_value), '2006-%02d-%02d.23:59:59'%(i,
+                calendar.mdays[i]))
+
+
 def test_suite():
     suite = unittest.TestSuite()
     suite.addTest(unittest.makeSuite(DateTestCase))
+    suite.addTest(unittest.makeSuite(RangeTestCase))
     try:
         import pytz
     except ImportError:

Modified: tracker/roundup-src/test/test_mailgw.py
==============================================================================
--- tracker/roundup-src/test/test_mailgw.py	(original)
+++ tracker/roundup-src/test/test_mailgw.py	Sun Mar  9 09:26:16 2008
@@ -8,7 +8,7 @@
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 #
-# $Id: test_mailgw.py,v 1.79 2006/10/05 23:08:21 richard Exp $
+# $Id: test_mailgw.py,v 1.93 2008/02/07 03:55:14 richard Exp $
 
 # TODO: test bcc
 
@@ -21,7 +21,7 @@
 SENDMAILDEBUG = os.environ['SENDMAILDEBUG']
 
 from roundup.mailgw import MailGW, Unauthorized, uidFromAddress, \
-    parseContent, IgnoreLoop, IgnoreBulk, MailUsageError
+    parseContent, IgnoreLoop, IgnoreBulk, MailUsageError, MailUsageHelp
 from roundup import init, instance, password, rfc2822, __version__
 
 import db_test_base
@@ -50,8 +50,9 @@
                     if new[key] != __version__:
                         res.append('  %s: %s != %s' % (key, __version__,
                             new[key]))
-                elif new[key] != old[key]:
-                    res.append('  %s: %s != %s' % (key, old[key], new[key]))
+                elif new.get(key, '') != old.get(key, ''):
+                    res.append('  %s: %s != %s' % (key, old.get(key, ''),
+                        new.get(key, '')))
 
             body_diff = self.compareStrings(new.fp.read(), old.fp.read())
             if body_diff:
@@ -104,11 +105,11 @@
         self.chef_id = self.db.user.create(username='Chef',
             address='chef at bork.bork.bork', realname='Bork, Chef', roles='User')
         self.richard_id = self.db.user.create(username='richard',
-            address='richard at test', roles='User')
-        self.mary_id = self.db.user.create(username='mary', address='mary at test',
+            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',
-            alternate_addresses='jondoe at test\njohn.doe at test', roles='User',
+        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')
 
     def tearDown(self):
@@ -140,7 +141,7 @@
   charset="iso-8859-1"
 From: Chef <chef at bork.bork.bork>
 To: issue_tracker at your.tracker.email.domain.example
-Cc: richard at test
+Cc: richard at test.test
 Reply-To: chef at bork.bork.bork
 Message-Id: <dummy_test_message_id>
 Subject: [issue] Testing...
@@ -154,7 +155,7 @@
   charset="iso-8859-1"
 From: Chef <chef at bork.bork.bork>
 To: issue_tracker at your.tracker.email.domain.example
-Cc: richard at test
+Cc: richard at test.test
 Message-Id: <dummy_test_message_id>
 Subject: [issue] Testing...
 
@@ -175,7 +176,7 @@
   charset="iso-8859-1"
 From: Chef <chef at bork.bork.bork>
 To: issue_tracker at your.tracker.email.domain.example
-Cc: richard at test
+Cc: richard at test.test
 Message-Id: <dummy_test_message_id>
 Subject: [issue] Testing...
 
@@ -189,7 +190,7 @@
     def testAlternateAddress(self):
         self._handle_mail('''Content-Type: text/plain;
   charset="iso-8859-1"
-From: John Doe <john.doe at test>
+From: John Doe <john.doe at test.test>
 To: issue_tracker at your.tracker.email.domain.example
 Message-Id: <dummy_test_message_id>
 Subject: [issue] Testing...
@@ -206,7 +207,7 @@
   charset="iso-8859-1"
 From: Chef <chef at bork.bork.bork>
 To: issue_tracker at your.tracker.email.domain.example
-Cc: richard at test
+Cc: richard at test.test
 Message-Id: <dummy_test_message_id>
 Subject: Testing...
 
@@ -228,16 +229,17 @@
 ''')
         self.compareMessages(self._get_mail(),
 '''FROM: roundup-admin at your.tracker.email.domain.example
-TO: chef at bork.bork.bork, mary at test, richard at test
+TO: chef at bork.bork.bork, mary at test.test, richard at test.test
 Content-Type: text/plain; charset=utf-8
 Subject: [issue1] Testing...
-To: chef at bork.bork.bork, mary at test, richard at test
+To: chef at bork.bork.bork, mary at test.test, richard at test.test
 From: "Bork, Chef" <issue_tracker at your.tracker.email.domain.example>
 Reply-To: Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
 MIME-Version: 1.0
 Message-Id: <dummy_test_message_id>
 X-Roundup-Name: Roundup issue tracker
 X-Roundup-Loop: hello
+X-Roundup-Issue-Status: unread
 Content-Transfer-Encoding: quoted-printable
 
 
@@ -258,20 +260,196 @@
 _______________________________________________________________________
 ''')
 
-    # BUG
-    # def testMultipart(self):
-    #         '''With more than one part'''
-    #        see MultipartEnc tests: but if there is more than one part
-    #        we return a multipart/mixed and the boundary contains
-    #        the ip address of the test machine.
+    def testNewIssueNoAuthorInfo(self):
+        self.db.config.MAIL_ADD_AUTHORINFO = 'no'
+        self._handle_mail('''Content-Type: text/plain;
+  charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Message-Id: <dummy_test_message_id>
+Subject: [issue] Testing... [nosy=mary; assignedto=richard]
 
-    # BUG should test some binary attamchent too.
+This is a test submission of a new issue.
+''')
+        self.compareMessages(self._get_mail(),
+'''FROM: roundup-admin at your.tracker.email.domain.example
+TO: chef at bork.bork.bork, mary at test.test, richard at test.test
+Content-Type: text/plain; charset=utf-8
+Subject: [issue1] Testing...
+To: mary at test.test, richard at test.test
+From: "Bork, Chef" <issue_tracker at your.tracker.email.domain.example>
+Reply-To: Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
+MIME-Version: 1.0
+Message-Id: <dummy_test_message_id>
+X-Roundup-Name: Roundup issue tracker
+X-Roundup-Loop: hello
+X-Roundup-Issue-Status: unread
+Content-Transfer-Encoding: quoted-printable
+
+This is a test submission of a new issue.
+
+----------
+assignedto: richard
+messages: 1
+nosy: Chef, mary, richard
+status: unread
+title: Testing...
+
+_______________________________________________________________________
+Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
+<http://tracker.example/cgi-bin/roundup.cgi/bugs/issue1>
+_______________________________________________________________________
+''')
+
+    def testNewIssueNoAuthorEmail(self):
+        self.db.config.MAIL_ADD_AUTHOREMAIL = 'no'
+        self._handle_mail('''Content-Type: text/plain;
+  charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Message-Id: <dummy_test_message_id>
+Subject: [issue] Testing... [nosy=mary; assignedto=richard]
+
+This is a test submission of a new issue.
+''')
+        self.compareMessages(self._get_mail(),
+'''FROM: roundup-admin at your.tracker.email.domain.example
+TO: chef at bork.bork.bork, mary at test.test, richard at test.test
+Content-Type: text/plain; charset=utf-8
+Subject: [issue1] Testing...
+To: mary at test.test, richard at test.test
+From: "Bork, Chef" <issue_tracker at your.tracker.email.domain.example>
+Reply-To: Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
+MIME-Version: 1.0
+Message-Id: <dummy_test_message_id>
+X-Roundup-Name: Roundup issue tracker
+X-Roundup-Loop: hello
+X-Roundup-Issue-Status: unread
+Content-Transfer-Encoding: quoted-printable
+
+New submission from Bork, Chef:
+
+This is a test submission of a new issue.
+
+----------
+assignedto: richard
+messages: 1
+nosy: Chef, mary, richard
+status: unread
+title: Testing...
+
+_______________________________________________________________________
+Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
+<http://tracker.example/cgi-bin/roundup.cgi/bugs/issue1>
+_______________________________________________________________________
+''')
+
+    multipart_msg = '''Content-Type: text/plain;
+  charset="iso-8859-1"
+From: mary <mary at test.test>
+To: issue_tracker at your.tracker.email.domain.example
+Message-Id: <followup_dummy_id>
+In-Reply-To: <dummy_test_message_id>
+Subject: [issue1] Testing...
+Content-Type: multipart/mixed; boundary="bxyzzy"
+Content-Disposition: inline
+
+
+--bxyzzy
+Content-Type: multipart/alternative; boundary="bCsyhTFzCvuiizWE"
+Content-Disposition: inline
+
+--bCsyhTFzCvuiizWE
+Content-Type: text/plain; charset=us-ascii
+Content-Disposition: inline
+
+test attachment first text/plain
+
+--bCsyhTFzCvuiizWE
+Content-Type: application/octet-stream
+Content-Disposition: attachment; filename="first.dvi"
+Content-Transfer-Encoding: base64
+
+SnVzdCBhIHRlc3QgAQo=
+
+--bCsyhTFzCvuiizWE
+Content-Type: text/plain; charset=us-ascii
+Content-Disposition: inline
+
+test attachment second text/plain
+
+--bCsyhTFzCvuiizWE
+Content-Type: text/html
+Content-Disposition: inline
+
+<html>
+to be ignored.
+</html>
+
+--bCsyhTFzCvuiizWE--
+
+--bxyzzy
+Content-Type: multipart/alternative; boundary="bCsyhTFzCvuiizWF"
+Content-Disposition: inline
+
+--bCsyhTFzCvuiizWF
+Content-Type: text/plain; charset=us-ascii
+Content-Disposition: inline
+
+test attachment third text/plain
+
+--bCsyhTFzCvuiizWF
+Content-Type: application/octet-stream
+Content-Disposition: attachment; filename="second.dvi"
+Content-Transfer-Encoding: base64
+
+SnVzdCBhIHRlc3QK
+
+--bCsyhTFzCvuiizWF--
+
+--bxyzzy--
+'''
+
+    def testMultipartKeepAlternatives(self):
+        self.doNewIssue()
+        self._handle_mail(self.multipart_msg)
+        messages = self.db.issue.get('1', 'messages')
+        messages.sort()
+        msg = self.db.msg.getnode (messages[-1])
+        assert(len(msg.files) == 5)
+        names = {0 : 'first.dvi', 4 : 'second.dvi'}
+        content = {3 : 'test attachment third text/plain\n',
+                   4 : 'Just a test\n'}
+        for n, id in enumerate (msg.files):
+            f = self.db.file.getnode (id)
+            self.assertEqual(f.name, names.get (n, 'unnamed'))
+            if n in content :
+                self.assertEqual(f.content, content [n])
+        self.assertEqual(msg.content, 'test attachment second text/plain')
+
+    def testMultipartDropAlternatives(self):
+        self.doNewIssue()
+        self.db.config.MAILGW_IGNORE_ALTERNATIVES = True
+        self._handle_mail(self.multipart_msg)
+        messages = self.db.issue.get('1', 'messages')
+        messages.sort()
+        msg = self.db.msg.getnode (messages[-1])
+        assert(len(msg.files) == 2)
+        names = {1 : 'second.dvi'}
+        content = {0 : 'test attachment third text/plain\n',
+                   1 : 'Just a test\n'}
+        for n, id in enumerate (msg.files):
+            f = self.db.file.getnode (id)
+            self.assertEqual(f.name, names.get (n, 'unnamed'))
+            if n in content :
+                self.assertEqual(f.content, content [n])
+        self.assertEqual(msg.content, 'test attachment second text/plain')
 
     def testSimpleFollowup(self):
         self.doNewIssue()
         self._handle_mail('''Content-Type: text/plain;
   charset="iso-8859-1"
-From: mary <mary at test>
+From: mary <mary at test.test>
 To: issue_tracker at your.tracker.email.domain.example
 Message-Id: <followup_dummy_id>
 In-Reply-To: <dummy_test_message_id>
@@ -281,10 +459,10 @@
 ''')
         self.compareMessages(self._get_mail(),
 '''FROM: roundup-admin at your.tracker.email.domain.example
-TO: chef at bork.bork.bork, richard at test
+TO: chef at bork.bork.bork, richard at test.test
 Content-Type: text/plain; charset=utf-8
 Subject: [issue1] Testing...
-To: chef at bork.bork.bork, richard at test
+To: chef at bork.bork.bork, richard at test.test
 From: "Contrary, Mary" <issue_tracker at your.tracker.email.domain.example>
 Reply-To: Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
 MIME-Version: 1.0
@@ -292,10 +470,11 @@
 In-Reply-To: <dummy_test_message_id>
 X-Roundup-Name: Roundup issue tracker
 X-Roundup-Loop: hello
+X-Roundup-Issue-Status: chatting
 Content-Transfer-Encoding: quoted-printable
 
 
-Contrary, Mary <mary at test> added the comment:
+Contrary, Mary <mary at test.test> added the comment:
 
 This is a second followup
 
@@ -313,7 +492,7 @@
 
         self._handle_mail('''Content-Type: text/plain;
   charset="iso-8859-1"
-From: richard <richard at test>
+From: richard <richard at test.test>
 To: issue_tracker at your.tracker.email.domain.example
 Message-Id: <followup_dummy_id>
 In-Reply-To: <dummy_test_message_id>
@@ -328,10 +507,10 @@
 
         self.compareMessages(self._get_mail(),
 '''FROM: roundup-admin at your.tracker.email.domain.example
-TO: chef at bork.bork.bork, john at test, mary at test
+TO: chef at bork.bork.bork, john at test.test, mary at test.test
 Content-Type: text/plain; charset=utf-8
 Subject: [issue1] Testing...
-To: chef at bork.bork.bork, john at test, mary at test
+To: chef at bork.bork.bork, john at test.test, mary at test.test
 From: richard <issue_tracker at your.tracker.email.domain.example>
 Reply-To: Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
 MIME-Version: 1.0
@@ -339,10 +518,11 @@
 In-Reply-To: <dummy_test_message_id>
 X-Roundup-Name: Roundup issue tracker
 X-Roundup-Loop: hello
+X-Roundup-Issue-Status: chatting
 Content-Transfer-Encoding: quoted-printable
 
 
-richard <richard at test> added the comment:
+richard <richard at test.test> added the comment:
 
 This is a followup
 
@@ -357,6 +537,50 @@
 _______________________________________________________________________
 ''')
 
+    def testPropertyChangeOnly(self):
+        self.doNewIssue()
+        oldvalues = self.db.getnode('issue', '1').copy()
+        oldvalues['assignedto'] = None
+        self.db.issue.set('1', assignedto=self.chef_id)
+        self.db.commit()
+        self.db.issue.nosymessage('1', None, oldvalues)
+
+        new_mail = ""
+        for line in self._get_mail().split("\n"):
+            if "Message-Id: " in line:
+                continue
+            if "Date: " in line:
+                continue
+            new_mail += line+"\n"
+
+        self.compareMessages(new_mail, """
+FROM: roundup-admin at your.tracker.email.domain.example
+TO: chef at bork.bork.bork, richard at test.test
+Content-Type: text/plain; charset=utf-8
+Subject: [issue1] Testing...
+To: chef at bork.bork.bork, richard at test.test
+From: "Bork, Chef" <issue_tracker at your.tracker.email.domain.example>
+X-Roundup-Name: Roundup issue tracker
+X-Roundup-Loop: hello
+X-Roundup-Issue-Status: unread
+X-Roundup-Version: 1.3.3
+MIME-Version: 1.0
+Reply-To: Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
+Content-Transfer-Encoding: quoted-printable
+
+
+Changes by Bork, Chef <chef at bork.bork.bork>:
+
+
+----------
+assignedto:  -> Chef
+
+_______________________________________________________________________
+Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
+<http://tracker.example/cgi-bin/roundup.cgi/bugs/issue1>
+_______________________________________________________________________
+""")
+
 
     #
     # FOLLOWUP TITLE MATCH
@@ -365,7 +589,7 @@
         self.doNewIssue()
         self._handle_mail('''Content-Type: text/plain;
   charset="iso-8859-1"
-From: richard <richard at test>
+From: richard <richard at test.test>
 To: issue_tracker at your.tracker.email.domain.example
 Message-Id: <followup_dummy_id>
 Subject: Re: Testing... [assignedto=mary; nosy=+john]
@@ -374,10 +598,10 @@
 ''')
         self.compareMessages(self._get_mail(),
 '''FROM: roundup-admin at your.tracker.email.domain.example
-TO: chef at bork.bork.bork, john at test, mary at test
+TO: chef at bork.bork.bork, john at test.test, mary at test.test
 Content-Type: text/plain; charset=utf-8
 Subject: [issue1] Testing...
-To: chef at bork.bork.bork, john at test, mary at test
+To: chef at bork.bork.bork, john at test.test, mary at test.test
 From: richard <issue_tracker at your.tracker.email.domain.example>
 Reply-To: Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
 MIME-Version: 1.0
@@ -385,10 +609,11 @@
 In-Reply-To: <dummy_test_message_id>
 X-Roundup-Name: Roundup issue tracker
 X-Roundup-Loop: hello
+X-Roundup-Issue-Status: chatting
 Content-Transfer-Encoding: quoted-printable
 
 
-richard <richard at test> added the comment:
+richard <richard at test.test> added the comment:
 
 This is a followup
 
@@ -403,12 +628,36 @@
 _______________________________________________________________________
 ''')
 
+    def testFollowupTitleMatchMultiRe(self):
+        nodeid1 = self.doNewIssue()
+        nodeid2 = self._handle_mail('''Content-Type: text/plain;
+  charset="iso-8859-1"
+From: richard <richard at test.test>
+To: issue_tracker at your.tracker.email.domain.example
+Message-Id: <followup_dummy_id>
+Subject: Re: Testing... [assignedto=mary; nosy=+john]
+
+This is a followup
+''')
+
+        nodeid3 = self._handle_mail('''Content-Type: text/plain;
+  charset="iso-8859-1"
+From: richard <richard at test.test>
+To: issue_tracker at your.tracker.email.domain.example
+Message-Id: <followup2_dummy_id>
+Subject: Ang: Re: Testing...
+
+This is a followup
+''')
+        self.assertEqual(nodeid1, nodeid2)
+        self.assertEqual(nodeid1, nodeid3)
+
     def testFollowupTitleMatchNever(self):
         nodeid = self.doNewIssue()
         self.db.config.MAILGW_SUBJECT_CONTENT_MATCH = 'never'
         self.assertNotEqual(self._handle_mail('''Content-Type: text/plain;
   charset="iso-8859-1"
-From: richard <richard at test>
+From: richard <richard at test.test>
 To: issue_tracker at your.tracker.email.domain.example
 Message-Id: <followup_dummy_id>
 Subject: Re: Testing...
@@ -423,7 +672,7 @@
         self.db.config.MAILGW_SUBJECT_CONTENT_MATCH = 'creation 00:00:01'
         self.assertNotEqual(self._handle_mail('''Content-Type: text/plain;
   charset="iso-8859-1"
-From: richard <richard at test>
+From: richard <richard at test.test>
 To: issue_tracker at your.tracker.email.domain.example
 Message-Id: <followup_dummy_id>
 Subject: Re: Testing...
@@ -434,7 +683,7 @@
         self.db.config.MAILGW_SUBJECT_CONTENT_MATCH = 'creation +1d'
         self.assertEqual(self._handle_mail('''Content-Type: text/plain;
   charset="iso-8859-1"
-From: richard <richard at test>
+From: richard <richard at test.test>
 To: issue_tracker at your.tracker.email.domain.example
 Message-Id: <followup_dummy_id>
 Subject: Re: Testing...
@@ -448,7 +697,7 @@
         self.db.config.ADD_AUTHOR_TO_NOSY = 'yes'
         self._handle_mail('''Content-Type: text/plain;
   charset="iso-8859-1"
-From: john at test
+From: john at test.test
 To: issue_tracker at your.tracker.email.domain.example
 Message-Id: <followup_dummy_id>
 In-Reply-To: <dummy_test_message_id>
@@ -459,10 +708,10 @@
 
         self.compareMessages(self._get_mail(),
 '''FROM: roundup-admin at your.tracker.email.domain.example
-TO: chef at bork.bork.bork, richard at test
+TO: chef at bork.bork.bork, richard at test.test
 Content-Type: text/plain; charset=utf-8
 Subject: [issue1] Testing...
-To: chef at bork.bork.bork, richard at test
+To: chef at bork.bork.bork, richard at test.test
 From: John Doe <issue_tracker at your.tracker.email.domain.example>
 Reply-To: Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
 MIME-Version: 1.0
@@ -470,10 +719,11 @@
 In-Reply-To: <dummy_test_message_id>
 X-Roundup-Name: Roundup issue tracker
 X-Roundup-Loop: hello
+X-Roundup-Issue-Status: chatting
 Content-Transfer-Encoding: quoted-printable
 
 
-John Doe <john at test> added the comment:
+John Doe <john at test.test> added the comment:
 
 This is a followup
 
@@ -493,9 +743,9 @@
         self.db.config.ADD_RECIPIENTS_TO_NOSY = 'yes'
         self._handle_mail('''Content-Type: text/plain;
   charset="iso-8859-1"
-From: richard at test
+From: richard at test.test
 To: issue_tracker at your.tracker.email.domain.example
-Cc: john at test
+Cc: john at test.test
 Message-Id: <followup_dummy_id>
 In-Reply-To: <dummy_test_message_id>
 Subject: [issue1] Testing...
@@ -515,10 +765,11 @@
 In-Reply-To: <dummy_test_message_id>
 X-Roundup-Name: Roundup issue tracker
 X-Roundup-Loop: hello
+X-Roundup-Issue-Status: chatting
 Content-Transfer-Encoding: quoted-printable
 
 
-richard <richard at test> added the comment:
+richard <richard at test.test> added the comment:
 
 This is a followup
 
@@ -539,7 +790,7 @@
         self.db.config.MESSAGES_TO_AUTHOR = 'yes'
         self._handle_mail('''Content-Type: text/plain;
   charset="iso-8859-1"
-From: john at test
+From: john at test.test
 To: issue_tracker at your.tracker.email.domain.example
 Message-Id: <followup_dummy_id>
 In-Reply-To: <dummy_test_message_id>
@@ -549,10 +800,10 @@
 ''')
         self.compareMessages(self._get_mail(),
 '''FROM: roundup-admin at your.tracker.email.domain.example
-TO: chef at bork.bork.bork, john at test, richard at test
+TO: chef at bork.bork.bork, john at test.test, richard at test.test
 Content-Type: text/plain; charset=utf-8
 Subject: [issue1] Testing...
-To: chef at bork.bork.bork, john at test, richard at test
+To: chef at bork.bork.bork, john at test.test, richard at test.test
 From: John Doe <issue_tracker at your.tracker.email.domain.example>
 Reply-To: Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
 MIME-Version: 1.0
@@ -560,10 +811,11 @@
 In-Reply-To: <dummy_test_message_id>
 X-Roundup-Name: Roundup issue tracker
 X-Roundup-Loop: hello
+X-Roundup-Issue-Status: chatting
 Content-Transfer-Encoding: quoted-printable
 
 
-John Doe <john at test> added the comment:
+John Doe <john at test.test> added the comment:
 
 This is a followup
 
@@ -583,7 +835,7 @@
         self.instance.config.ADD_AUTHOR_TO_NOSY = 'no'
         self._handle_mail('''Content-Type: text/plain;
   charset="iso-8859-1"
-From: john at test
+From: john at test.test
 To: issue_tracker at your.tracker.email.domain.example
 Message-Id: <followup_dummy_id>
 In-Reply-To: <dummy_test_message_id>
@@ -593,10 +845,10 @@
 ''')
         self.compareMessages(self._get_mail(),
 '''FROM: roundup-admin at your.tracker.email.domain.example
-TO: chef at bork.bork.bork, richard at test
+TO: chef at bork.bork.bork, richard at test.test
 Content-Type: text/plain; charset=utf-8
 Subject: [issue1] Testing...
-To: chef at bork.bork.bork, richard at test
+To: chef at bork.bork.bork, richard at test.test
 From: John Doe <issue_tracker at your.tracker.email.domain.example>
 Reply-To: Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
 MIME-Version: 1.0
@@ -604,10 +856,11 @@
 In-Reply-To: <dummy_test_message_id>
 X-Roundup-Name: Roundup issue tracker
 X-Roundup-Loop: hello
+X-Roundup-Issue-Status: chatting
 Content-Transfer-Encoding: quoted-printable
 
 
-John Doe <john at test> added the comment:
+John Doe <john at test.test> added the comment:
 
 This is a followup
 
@@ -626,9 +879,9 @@
         self.instance.config.ADD_RECIPIENTS_TO_NOSY = 'no'
         self._handle_mail('''Content-Type: text/plain;
   charset="iso-8859-1"
-From: richard at test
+From: richard at test.test
 To: issue_tracker at your.tracker.email.domain.example
-Cc: john at test
+Cc: john at test.test
 Message-Id: <followup_dummy_id>
 In-Reply-To: <dummy_test_message_id>
 Subject: [issue1] Testing...
@@ -648,10 +901,11 @@
 In-Reply-To: <dummy_test_message_id>
 X-Roundup-Name: Roundup issue tracker
 X-Roundup-Loop: hello
+X-Roundup-Issue-Status: chatting
 Content-Transfer-Encoding: quoted-printable
 
 
-richard <richard at test> added the comment:
+richard <richard at test.test> added the comment:
 
 This is a followup
 
@@ -670,7 +924,7 @@
 
         self._handle_mail('''Content-Type: text/plain;
   charset="iso-8859-1"
-From: richard <richard at test>
+From: richard <richard at test.test>
 To: issue_tracker at your.tracker.email.domain.example
 Message-Id: <followup_dummy_id>
 In-Reply-To: <dummy_test_message_id>
@@ -690,7 +944,7 @@
 
         self._handle_mail('''Content-Type: text/plain;
   charset="iso-8859-1"
-From: richard <richard at test>
+From: richard <richard at test.test>
 To: issue_tracker at your.tracker.email.domain.example
 Message-Id: <followup_dummy_id>
 In-Reply-To: <dummy_test_message_id>
@@ -710,7 +964,7 @@
 
         self._handle_mail('''Content-Type: text/plain;
   charset="iso-8859-1"
-From: richard <richard at test>
+From: richard <richard at test.test>
 To: issue_tracker at your.tracker.email.domain.example
 Message-Id: <followup_dummy_id>
 In-Reply-To: <dummy_test_message_id>
@@ -742,7 +996,48 @@
 
 This is a test submission of a new issue.
 '''
-        self.assertRaises(Unauthorized, self._handle_mail, message)
+        try:
+            self._handle_mail(message)
+        except Unauthorized, value:
+            body_diff = self.compareMessages(str(value), """
+You are not a registered user.
+
+Unknown address: fubar at bork.bork.bork
+""")
+
+            assert not body_diff, body_diff
+
+        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
+
+        try:
+            self._handle_mail(message)
+        except Unauthorized, value:
+            body_diff = self.compareMessages(str(value), """
+You are not a registered user. Please register at:
+
+http://tracker.example/cgi-bin/roundup.cgi/bugs/user?template=register
+
+...before sending mail to the tracker.
+
+Unknown address: fubar at bork.bork.bork
+""")
+
+            assert not body_diff, body_diff
+
+        else:
+            raise AssertionError, "Unathorized not raised when handling mail"
+
+        # Make sure list of users is the same as before.
         m = self.db.user.list()
         m.sort()
         self.assertEqual(l, m)
@@ -762,7 +1057,7 @@
         self.doNewIssue()
         self._handle_mail('''Content-Type: text/plain;
   charset="iso-8859-1"
-From: mary <mary at test>
+From: mary <mary at test.test>
 To: issue_tracker at your.tracker.email.domain.example
 Message-Id: <followup_dummy_id>
 In-Reply-To: <dummy_test_message_id>
@@ -776,10 +1071,10 @@
 ''')
         self.compareMessages(self._get_mail(),
 '''FROM: roundup-admin at your.tracker.email.domain.example
-TO: chef at bork.bork.bork, richard at test
+TO: chef at bork.bork.bork, richard at test.test
 Content-Type: text/plain; charset=utf-8
 Subject: [issue1] Testing...
-To: chef at bork.bork.bork, richard at test
+To: chef at bork.bork.bork, richard at test.test
 From: "Contrary, Mary" <issue_tracker at your.tracker.email.domain.example>
 Reply-To: Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
 MIME-Version: 1.0
@@ -787,10 +1082,11 @@
 In-Reply-To: <dummy_test_message_id>
 X-Roundup-Name: Roundup issue tracker
 X-Roundup-Loop: hello
+X-Roundup-Issue-Status: chatting
 Content-Transfer-Encoding: quoted-printable
 
 
-Contrary, Mary <mary at test> added the comment:
+Contrary, Mary <mary at test.test> added the comment:
 
 A message with encoding (encoded oe =C3=B6)
 
@@ -808,7 +1104,7 @@
         self.doNewIssue()
         self._handle_mail('''Content-Type: text/plain;
   charset="iso-8859-1"
-From: mary <mary at test>
+From: mary <mary at test.test>
 To: issue_tracker at your.tracker.email.domain.example
 Message-Id: <followup_dummy_id>
 In-Reply-To: <dummy_test_message_id>
@@ -829,10 +1125,10 @@
 ''')
         self.compareMessages(self._get_mail(),
 '''FROM: roundup-admin at your.tracker.email.domain.example
-TO: chef at bork.bork.bork, richard at test
+TO: chef at bork.bork.bork, richard at test.test
 Content-Type: text/plain; charset=utf-8
 Subject: [issue1] Testing...
-To: chef at bork.bork.bork, richard at test
+To: chef at bork.bork.bork, richard at test.test
 From: "Contrary, Mary" <issue_tracker at your.tracker.email.domain.example>
 Reply-To: Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
 MIME-Version: 1.0
@@ -840,10 +1136,11 @@
 In-Reply-To: <dummy_test_message_id>
 X-Roundup-Name: Roundup issue tracker
 X-Roundup-Loop: hello
+X-Roundup-Issue-Status: chatting
 Content-Transfer-Encoding: quoted-printable
 
 
-Contrary, Mary <mary at test> added the comment:
+Contrary, Mary <mary at test.test> added the comment:
 
 A message with first part encoded (encoded oe =C3=B6)
 
@@ -860,7 +1157,7 @@
         self.doNewIssue()
         self._handle_mail('''Content-Type: text/plain;
   charset="iso-8859-1"
-From: mary <mary at test>
+From: mary <mary at test.test>
 To: issue_tracker at your.tracker.email.domain.example
 Message-Id: <followup_dummy_id>
 In-Reply-To: <dummy_test_message_id>
@@ -878,22 +1175,24 @@
 --bCsyhTFzCvuiizWE
 Content-Type: application/octet-stream
 Content-Disposition: attachment; filename="main.dvi"
+Content-Transfer-Encoding: base64
 
-xxxxxx
+SnVzdCBhIHRlc3QgAQo=
 
 --bCsyhTFzCvuiizWE--
 ''')
         messages = self.db.issue.get('1', 'messages')
         messages.sort()
-        file = self.db.msg.get(messages[-1], 'files')[0]
-        self.assertEqual(self.db.file.get(file, 'name'), 'main.dvi')
+        file = self.db.file.getnode (self.db.msg.get(messages[-1], 'files')[0])
+        self.assertEqual(file.name, 'main.dvi')
+        self.assertEqual(file.content, 'Just a test \001\n')
 
     def testFollowupStupidQuoting(self):
         self.doNewIssue()
 
         self._handle_mail('''Content-Type: text/plain;
   charset="iso-8859-1"
-From: richard <richard at test>
+From: richard <richard at test.test>
 To: issue_tracker at your.tracker.email.domain.example
 Message-Id: <followup_dummy_id>
 In-Reply-To: <dummy_test_message_id>
@@ -914,10 +1213,11 @@
 In-Reply-To: <dummy_test_message_id>
 X-Roundup-Name: Roundup issue tracker
 X-Roundup-Loop: hello
+X-Roundup-Issue-Status: chatting
 Content-Transfer-Encoding: quoted-printable
 
 
-richard <richard at test> added the comment:
+richard <richard at test.test> added the comment:
 
 This is a followup
 
@@ -952,7 +1252,7 @@
 
         self._handle_mail('''Content-Type: text/plain;
   charset="iso-8859-1"
-From: richard <richard at test>
+From: richard <richard at test.test>
 To: issue_tracker at your.tracker.email.domain.example
 Message-Id: <followup_dummy_id>
 In-Reply-To: <dummy_test_message_id>
@@ -1005,7 +1305,7 @@
   charset="iso-8859-1"
 From: Chef <chef at bork.bork.bork>
 To: issue_tracker at your.tracker.email.domain.example
-Cc: richard at test
+Cc: richard at test.test
 Message-Id: <dummy_test_message_id>
 Subject: Re: Complete your registration to Roundup issue tracker
  -- key %s
@@ -1018,7 +1318,7 @@
         self.db.keyword.create(name='Foo')
         self._handle_mail('''Content-Type: text/plain;
   charset="iso-8859-1"
-From: richard <richard at test>
+From: richard <richard at test.test>
 To: issue_tracker at your.tracker.email.domain.example
 Message-Id: <followup_dummy_id>
 In-Reply-To: <dummy_test_message_id>
@@ -1031,9 +1331,9 @@
         nodeid = self._handle_mail('''Content-Type: text/plain;
   charset="iso-8859-1"
 From: Chef <chef at bork.bork.bork>
-Resent-From: mary <mary at test>
+Resent-From: mary <mary at test.test>
 To: issue_tracker at your.tracker.email.domain.example
-Cc: richard at test
+Cc: richard at test.test
 Message-Id: <dummy_test_message_id>
 Subject: [issue] Testing...
 
@@ -1052,7 +1352,7 @@
 From: Chef <chef at bork.bork.bork>
 X-Roundup-Loop: hello
 To: issue_tracker at your.tracker.email.domain.example
-Cc: richard at test
+Cc: richard at test.test
 Message-Id: <dummy_test_message_id>
 Subject: Re: [issue] Testing...
 
@@ -1066,7 +1366,7 @@
 From: Chef <chef at bork.bork.bork>
 Precedence: bulk
 To: issue_tracker at your.tracker.email.domain.example
-Cc: richard at test
+Cc: richard at test.test
 Message-Id: <dummy_test_message_id>
 Subject: Re: [issue] Testing...
 
@@ -1079,11 +1379,11 @@
   charset="iso-8859-1"
 From: Chef <chef at bork.bork.bork>
 To: issue_tracker at your.tracker.email.domain.example
-Cc: richard at test
+Cc: richard at test.test
 Message-Id: <dummy_test_message_id>
 Subject: Re: [issue] Out of office AutoReply: Back next week
 
-Hi, I'm back in the office next week
+Hi, I am back in the office next week
 ''')
 
     def testNoSubject(self):
@@ -1092,7 +1392,7 @@
   charset="iso-8859-1"
 From: Chef <chef at bork.bork.bork>
 To: issue_tracker at your.tracker.email.domain.example
-Cc: richard at test
+Cc: richard at test.test
 Reply-To: chef at bork.bork.bork
 Message-Id: <dummy_test_message_id>
 
@@ -1108,7 +1408,7 @@
 From: Chef <chef at bork.bork.bork>
 To: issue_tracker at your.tracker.email.domain.example
 Subject: [frobulated] testing
-Cc: richard at test
+Cc: richard at test.test
 Reply-To: chef at bork.bork.bork
 Message-Id: <dummy_test_message_id>
 
@@ -1119,7 +1419,7 @@
 From: Chef <chef at bork.bork.bork>
 To: issue_tracker at your.tracker.email.domain.example
 Subject: [issue12345] testing
-Cc: richard at test
+Cc: richard at test.test
 Reply-To: chef at bork.bork.bork
 Message-Id: <dummy_test_message_id>
 
@@ -1132,7 +1432,23 @@
 From: Chef <chef at bork.bork.bork>
 To: issue_tracker at your.tracker.email.domain.example
 Subject: [frobulated] testing
-Cc: richard at test
+Cc: richard at test.test
+Reply-To: chef at bork.bork.bork
+Message-Id: <dummy_test_message_id>
+
+''')
+        assert not os.path.exists(SENDMAILDEBUG)
+        self.assertEqual(self.db.issue.get(nodeid, 'title'),
+            '[frobulated] testing')
+
+    def testInvalidClassLooseReply(self):
+        self.instance.config.MAILGW_SUBJECT_PREFIX_PARSING = 'loose'
+        nodeid = self._handle_mail('''Content-Type: text/plain;
+  charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Subject: Re: [frobulated] testing
+Cc: richard at test.test
 Reply-To: chef at bork.bork.bork
 Message-Id: <dummy_test_message_id>
 
@@ -1148,7 +1464,7 @@
 From: Chef <chef at bork.bork.bork>
 To: issue_tracker at your.tracker.email.domain.example
 Subject: [issue1234] testing
-Cc: richard at test
+Cc: richard at test.test
 Reply-To: chef at bork.bork.bork
 Message-Id: <dummy_test_message_id>
 
@@ -1165,7 +1481,7 @@
 From: Chef <chef at bork.bork.bork>
 To: issue_tracker at your.tracker.email.domain.example
 Subject: [keyword1] Testing... [name=Bar]
-Cc: richard at test
+Cc: richard at test.test
 Reply-To: chef at bork.bork.bork
 Message-Id: <dummy_test_message_id>
 
@@ -1173,6 +1489,40 @@
         assert not os.path.exists(SENDMAILDEBUG)
         self.assertEqual(self.db.keyword.get('1', 'name'), 'Bar')
 
+    def testClassStrictInvalid(self):
+        self.instance.config.MAILGW_SUBJECT_PREFIX_PARSING = 'strict'
+        self.instance.config.MAILGW_DEFAULT_CLASS = ''
+
+        message = '''Content-Type: text/plain;
+  charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Subject: Testing...
+Cc: richard at test.test
+Reply-To: chef at bork.bork.bork
+Message-Id: <dummy_test_message_id>
+
+'''
+        self.assertRaises(MailUsageError, self._handle_mail, message)
+
+    def testClassStrictValid(self):
+        self.instance.config.MAILGW_SUBJECT_PREFIX_PARSING = 'strict'
+        self.instance.config.MAILGW_DEFAULT_CLASS = ''
+
+        nodeid = self._handle_mail('''Content-Type: text/plain;
+  charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Subject: [issue] Testing...
+Cc: richard at test.test
+Reply-To: chef at bork.bork.bork
+Message-Id: <dummy_test_message_id>
+
+''')
+
+        assert not os.path.exists(SENDMAILDEBUG)
+        self.assertEqual(self.db.issue.get(nodeid, 'title'), 'Testing...')
+
     #
     # TEST FOR INVALID COMMANDS HANDLING
     #
@@ -1183,7 +1533,7 @@
 From: Chef <chef at bork.bork.bork>
 To: issue_tracker at your.tracker.email.domain.example
 Subject: testing [frobulated]
-Cc: richard at test
+Cc: richard at test.test
 Reply-To: chef at bork.bork.bork
 Message-Id: <dummy_test_message_id>
 
@@ -1196,7 +1546,7 @@
 From: Chef <chef at bork.bork.bork>
 To: issue_tracker at your.tracker.email.domain.example
 Subject: testing [frobulated]
-Cc: richard at test
+Cc: richard at test.test
 Reply-To: chef at bork.bork.bork
 Message-Id: <dummy_test_message_id>
 
@@ -1212,7 +1562,7 @@
 From: Chef <chef at bork.bork.bork>
 To: issue_tracker at your.tracker.email.domain.example
 Subject: testing [frobulated]
-Cc: richard at test
+Cc: richard at test.test
 Reply-To: chef at bork.bork.bork
 Message-Id: <dummy_test_message_id>
 
@@ -1228,7 +1578,7 @@
 From: Chef <chef at bork.bork.bork>
 To: issue_tracker at your.tracker.email.domain.example
 Subject: testing [assignedto=mary]
-Cc: richard at test
+Cc: richard at test.test
 Reply-To: chef at bork.bork.bork
 Message-Id: <dummy_test_message_id>
 
@@ -1244,7 +1594,7 @@
 From: Chef <chef at bork.bork.bork>
 To: issue_tracker at your.tracker.email.domain.example
 Subject: testing {assignedto=mary}
-Cc: richard at test
+Cc: richard at test.test
 Reply-To: chef at bork.bork.bork
 Message-Id: <dummy_test_message_id>
 
@@ -1258,7 +1608,7 @@
         self.db.keyword.create(name='Foo')
         self._handle_mail('''Content-Type: text/plain;
   charset="iso-8859-1"
-From: richard <richard at test>
+From: richard <richard at test.test>
 To: issue_tracker at your.tracker.email.domain.example
 Message-Id: <followup_dummy_id>
 In-Reply-To: <dummy_test_message_id>
@@ -1275,7 +1625,7 @@
 From: Chef <chef at bork.bork.bork>
 To: issue_tracker at your.tracker.email.domain.example
 Subject: testing [assignedto=mary]
-Cc: richard at test
+Cc: richard at test.test
 Reply-To: chef at bork.bork.bork
 Message-Id: <dummy_test_message_id>
 
@@ -1285,6 +1635,97 @@
             'testing [assignedto=mary]')
         self.assertEqual(self.db.issue.get(nodeid, 'assignedto'), None)
 
+    def testReplytoMatch(self):
+        self.instance.config.MAILGW_SUBJECT_PREFIX_PARSING = 'loose'
+        nodeid = self.doNewIssue()
+        nodeid2 = self._handle_mail('''Content-Type: text/plain;
+  charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Message-Id: <dummy_test_message_id2>
+In-Reply-To: <dummy_test_message_id>
+Subject: Testing...
+
+Followup message.
+''')
+
+        nodeid3 = self._handle_mail('''Content-Type: text/plain;
+  charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Message-Id: <dummy_test_message_id3>
+In-Reply-To: <dummy_test_message_id2>
+Subject: Testing...
+
+Yet another message in the same thread/issue.
+''')
+
+        self.assertEqual(nodeid, nodeid2)
+        self.assertEqual(nodeid, nodeid3)
+
+    def testHelpSubject(self):
+        message = '''Content-Type: text/plain;
+  charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Message-Id: <dummy_test_message_id2>
+In-Reply-To: <dummy_test_message_id>
+Subject: hElp
+
+
+'''
+        self.assertRaises(MailUsageHelp, self._handle_mail, message)
+
+    def testMaillistSubject(self):
+        self.instance.config.MAILGW_SUBJECT_SUFFIX_DELIMITERS = '[]'
+        self.db.keyword.create(name='Foo')
+        self._handle_mail('''Content-Type: text/plain;
+  charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Subject: [mailinglist-name] [keyword1] Testing.. [name=Bar]
+Cc: richard at test.test
+Reply-To: chef at bork.bork.bork
+Message-Id: <dummy_test_message_id>
+
+''')
+
+        assert not os.path.exists(SENDMAILDEBUG)
+        self.assertEqual(self.db.keyword.get('1', 'name'), 'Bar')
+
+    def testUnknownPrefixSubject(self):
+        self.db.keyword.create(name='Foo')
+        self._handle_mail('''Content-Type: text/plain;
+  charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Subject: VeryStrangeRe: [keyword1] Testing.. [name=Bar]
+Cc: richard at test.test
+Reply-To: chef at bork.bork.bork
+Message-Id: <dummy_test_message_id>
+
+''')
+
+        assert not os.path.exists(SENDMAILDEBUG)
+        self.assertEqual(self.db.keyword.get('1', 'name'), 'Bar')
+
+    def testIssueidLast(self):
+        nodeid1 = self.doNewIssue()
+        nodeid2 = self._handle_mail('''Content-Type: text/plain;
+  charset="iso-8859-1"
+From: mary <mary at test.test>
+To: issue_tracker at your.tracker.email.domain.example
+Message-Id: <followup_dummy_id>
+In-Reply-To: <dummy_test_message_id>
+Subject: New title [issue1]
+
+This is a second followup
+''')
+
+        assert nodeid1 == nodeid2
+        self.assertEqual(self.db.issue.get(nodeid2, 'title'), "Testing...")
+
+
 def test_suite():
     suite = unittest.TestSuite()
     suite.addTest(unittest.makeSuite(MailgwTestCase))

Modified: tracker/roundup-src/test/test_metakit.py
==============================================================================
--- tracker/roundup-src/test/test_metakit.py	(original)
+++ tracker/roundup-src/test/test_metakit.py	Sun Mar  9 09:26:16 2008
@@ -1,83 +0,0 @@
-#
-# Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
-# This module is free software, and you may redistribute it and/or modify
-# under the same terms as Python, so long as this copyright message and
-# disclaimer are retained in their original form.
-#
-# IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
-# DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
-# OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
-# POSSIBILITY OF SUCH DAMAGE.
-#
-# BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
-# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
-# FOR A PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
-# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
-# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
-#
-# $Id: test_metakit.py,v 1.7 2004/11/18 16:33:43 a1s Exp $
-import unittest, os, shutil, time, weakref
-
-from db_test_base import DBTest, ROTest, SchemaTest, ClassicInitTest, config, password
-
-from roundup.backends import get_backend, have_backend
-
-class metakitOpener:
-    if have_backend('metakit'):
-        module = get_backend('metakit')
-        module._instances = weakref.WeakValueDictionary()
-
-    def nuke_database(self):
-        shutil.rmtree(config.DATABASE)
-
-class metakitDBTest(metakitOpener, DBTest):
-    def testBooleanUnset(self):
-        # XXX: metakit can't unset Booleans :(
-        nid = self.db.user.create(username='foo', assignable=1)
-        self.db.user.set(nid, assignable=None)
-        self.assertEqual(self.db.user.get(nid, "assignable"), 0)
-
-    def testNumberUnset(self):
-        # XXX: metakit can't unset Numbers :(
-        nid = self.db.user.create(username='foo', age=1)
-        self.db.user.set(nid, age=None)
-        self.assertEqual(self.db.user.get(nid, "age"), 0)
-
-    def testPasswordUnset(self):
-        # XXX: metakit can't unset Numbers (id's) :(
-        x = password.Password('x')
-        nid = self.db.user.create(username='foo', password=x)
-        self.db.user.set(nid, assignable=None)
-        self.assertEqual(self.db.user.get(nid, "assignable"), 0)
-
-class metakitROTest(metakitOpener, ROTest):
-    pass
-
-class metakitSchemaTest(metakitOpener, SchemaTest):
-    pass
-
-class metakitClassicInitTest(ClassicInitTest):
-    backend = 'metakit'
-
-from session_common import DBMTest
-class metakitSessionTest(metakitOpener, DBMTest):
-    pass
-
-def test_suite():
-    suite = unittest.TestSuite()
-    if not have_backend('metakit'):
-        print 'Skipping metakit tests'
-        return suite
-    print 'Including metakit tests'
-    suite.addTest(unittest.makeSuite(metakitDBTest))
-    suite.addTest(unittest.makeSuite(metakitROTest))
-    suite.addTest(unittest.makeSuite(metakitSchemaTest))
-    suite.addTest(unittest.makeSuite(metakitClassicInitTest))
-    suite.addTest(unittest.makeSuite(metakitSessionTest))
-    return suite
-
-if __name__ == '__main__':
-    runner = unittest.TextTestRunner()
-    unittest.main(testRunner=runner)
-
-# vim: set et sts=4 sw=4 :

Modified: tracker/roundup-src/test/test_multipart.py
==============================================================================
--- tracker/roundup-src/test/test_multipart.py	(original)
+++ tracker/roundup-src/test/test_multipart.py	Sun Mar  9 09:26:16 2008
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: test_multipart.py,v 1.7 2004/01/17 13:49:06 jlgijsbers Exp $ 
+# $Id: test_multipart.py,v 1.8 2007/09/22 07:25:35 jpend Exp $ 
 
 import unittest
 from cStringIO import StringIO
@@ -30,7 +30,7 @@
              'application/pgp-signature': '    name="foo.gpg"\nfoo\n',
              'application/pdf': '    name="foo.pdf"\nfoo\n',
              'message/rfc822': 'Subject: foo\n\nfoo\n'}
-    
+
     def __init__(self, spec):
         """Create a basic MIME message according to 'spec'.
 
@@ -44,10 +44,10 @@
             content_type = line.strip()
             if not content_type:
                 continue
-            
+
             indent = self.getIndent(line)
             if indent:
-                parts.append('--boundary-%s\n' % indent)
+                parts.append('\n--boundary-%s\n' % indent)
             parts.append('Content-type: %s;\n' % content_type)
             parts.append(self.table[content_type] % {'indent': indent + 1})
 
@@ -68,7 +68,7 @@
         w = self.fp.write
         w('Content-Type: multipart/mixed; boundary="foo"\r\n\r\n')
         w('This is a multipart message. Ignore this bit.\r\n')
-        w('--foo\r\n')
+        w('\r\n--foo\r\n')
 
         w('Content-Type: text/plain\r\n\r\n')
         w('Hello, world!\r\n')
@@ -76,26 +76,26 @@
         w('Blah blah\r\n')
         w('foo\r\n')
         w('-foo\r\n')
-        w('--foo\r\n')
+        w('\r\n--foo\r\n')
 
         w('Content-Type: multipart/alternative; boundary="bar"\r\n\r\n')
         w('This is a multipart message. Ignore this bit.\r\n')
-        w('--bar\r\n')
+        w('\r\n--bar\r\n')
 
         w('Content-Type: text/plain\r\n\r\n')
         w('Hello, world!\r\n')
         w('\r\n')
         w('Blah blah\r\n')
-        w('--bar\r\n')
+        w('\r\n--bar\r\n')
 
         w('Content-Type: text/html\r\n\r\n')
         w('<b>Hello, world!</b>\r\n')
-        w('--bar--\r\n')
-        w('--foo\r\n')
+        w('\r\n--bar--\r\n')
+        w('\r\n--foo\r\n')
 
         w('Content-Type: text/plain\r\n\r\n')
         w('Last bit\n')
-        w('--foo--\r\n')
+        w('\r\n--foo--\r\n')
         self.fp.seek(0)
 
     def testMultipart(self):
@@ -185,7 +185,7 @@
         text/plain
         application/pdf
 """, ('foo\n', [('foo.pdf', 'application/pdf', 'foo\n')]))
-    
+
     def testSignedText(self):
         self.TestExtraction("""
 multipart/signed


More information about the Python-checkins mailing list