[Python-checkins] r70388 - in tracker/roundup-src: CHANGES.txt COPYING.txt ChangeLog MANIFEST.in README.txt demo.py detectors/creator_resolution.py doc/FAQ.txt doc/_static doc/_static/style.css doc/_templates doc/_templates/layout.html doc/acknowledgements.txt doc/admin_guide.txt doc/announcement.txt doc/contact.txt doc/customizing.txt doc/default.css doc/design.txt doc/developers.txt doc/index.txt doc/installation.txt doc/license.txt doc/roundup-admin.1 doc/roundup-demo.1 doc/roundup-mailgw.1 doc/roundup-server.1 doc/upgrading.txt doc/user_guide.txt doc/xmlrpc.txt frontends/ZRoundup/ZRoundup.py frontends/ZRoundup/__init__.py frontends/roundup.cgi locale/de.po locale/en.po locale/fr.po locale/it.po locale/lt.po locale/roundup.pot locale/ru.po locale/zh_CN.po locale/zh_TW.po roundup/__init__.py roundup/actions.py roundup/admin.py roundup/anypy roundup/anypy/README.txt roundup/anypy/TODO.txt roundup/anypy/__init__.py roundup/anypy/hashlib_.py roundup/anypy/sets_.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/back_tsearch2.py roundup/backends/blobfiles.py roundup/backends/indexer_common.py roundup/backends/indexer_dbm.py roundup/backends/indexer_rdbms.py roundup/backends/indexer_xapian.py roundup/backends/locking.py roundup/backends/portalocker.py roundup/backends/rdbms_common.py roundup/backends/sessions_dbm.py roundup/backends/sessions_rdbms.py roundup/backends/tsearch2_setup.py roundup/cgi/PageTemplates/GlobalTranslationService.py roundup/cgi/PageTemplates/__init__.py roundup/cgi/TAL/TranslationContext.py roundup/cgi/TranslationService.py roundup/cgi/ZTUtils/Batch.py roundup/cgi/ZTUtils/Iterator.py roundup/cgi/ZTUtils/__init__.py roundup/cgi/actions.py roundup/cgi/apache.py roundup/cgi/cgitb.py roundup/cgi/client.py roundup/cgi/exceptions.py roundup/cgi/form_parser.py roundup/cgi/templating.py roundup/cgi/wsgi_handler.py roundup/configuration.py roundup/date.py roundup/dist roundup/dist/__init__.py roundup/dist/command roundup/dist/command/__init__.py roundup/dist/command/bdist_rpm.py roundup/dist/command/build.py roundup/dist/command/build_doc.py roundup/dist/command/build_py.py roundup/dist/command/build_scripts.py roundup/exceptions.py roundup/hyperdb.py roundup/i18n.py roundup/init.py roundup/install_util.py roundup/instance.py roundup/mailer.py roundup/mailgw.py roundup/password.py roundup/roundupdb.py roundup/scripts/__init__.py roundup/scripts/roundup_admin.py roundup/scripts/roundup_demo.py roundup/scripts/roundup_gettext.py roundup/scripts/roundup_mailgw.py roundup/scripts/roundup_server.py roundup/security.py roundup/token.py roundup/version_check.py roundup/xmlrpc.py run_tests.py scripts/add-issue scripts/copy-user.py scripts/import_sf.py scripts/roundup-reminder scripts/roundup.rc-debian scripts/weekly-report setup.py share share/man share/man/man1 share/man/man1/roundup-admin.1 share/man/man1/roundup-demo.1 share/man/man1/roundup-mailgw.1 share/man/man1/roundup-server.1 sha

martin.v.loewis python-checkins at python.org
Sun Mar 15 22:43:34 CET 2009


Author: martin.v.loewis
Date: Sun Mar 15 22:43:30 2009
New Revision: 70388

Log:
Import roundup 1.4.7 as-is; local changes will be
re-applied.


Added:
   tracker/roundup-src/doc/_static/
   tracker/roundup-src/doc/_static/style.css   (contents, props changed)
   tracker/roundup-src/doc/_templates/
   tracker/roundup-src/doc/_templates/layout.html   (contents, props changed)
   tracker/roundup-src/doc/acknowledgements.txt   (contents, props changed)
   tracker/roundup-src/doc/contact.txt   (contents, props changed)
   tracker/roundup-src/doc/license.txt   (contents, props changed)
   tracker/roundup-src/doc/xmlrpc.txt   (contents, props changed)
   tracker/roundup-src/locale/it.po   (contents, props changed)
   tracker/roundup-src/roundup/actions.py   (contents, props changed)
   tracker/roundup-src/roundup/anypy/
   tracker/roundup-src/roundup/anypy/README.txt   (contents, props changed)
   tracker/roundup-src/roundup/anypy/TODO.txt   (contents, props changed)
   tracker/roundup-src/roundup/anypy/__init__.py   (contents, props changed)
   tracker/roundup-src/roundup/anypy/hashlib_.py   (contents, props changed)
   tracker/roundup-src/roundup/anypy/sets_.py   (contents, props changed)
   tracker/roundup-src/roundup/dist/
   tracker/roundup-src/roundup/dist/__init__.py   (contents, props changed)
   tracker/roundup-src/roundup/dist/command/
   tracker/roundup-src/roundup/dist/command/__init__.py   (contents, props changed)
   tracker/roundup-src/roundup/dist/command/bdist_rpm.py   (contents, props changed)
   tracker/roundup-src/roundup/dist/command/build.py   (contents, props changed)
   tracker/roundup-src/roundup/dist/command/build_doc.py   (contents, props changed)
   tracker/roundup-src/roundup/dist/command/build_py.py   (contents, props changed)
   tracker/roundup-src/roundup/dist/command/build_scripts.py   (contents, props changed)
   tracker/roundup-src/roundup/xmlrpc.py   (contents, props changed)
   tracker/roundup-src/share/
   tracker/roundup-src/share/man/
   tracker/roundup-src/share/man/man1/
   tracker/roundup-src/share/man/man1/roundup-admin.1   (contents, props changed)
   tracker/roundup-src/share/man/man1/roundup-demo.1   (contents, props changed)
   tracker/roundup-src/share/man/man1/roundup-mailgw.1   (contents, props changed)
   tracker/roundup-src/share/man/man1/roundup-server.1   (contents, props changed)
   tracker/roundup-src/share/roundup/
   tracker/roundup-src/share/roundup/templates/
   tracker/roundup-src/share/roundup/templates/classic/
   tracker/roundup-src/share/roundup/templates/classic/TEMPLATE-INFO.txt   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/detectors/
   tracker/roundup-src/share/roundup/templates/classic/detectors/messagesummary.py   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/detectors/nosyreaction.py   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/detectors/statusauditor.py   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/detectors/userauditor.py   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/extensions/
   tracker/roundup-src/share/roundup/templates/classic/extensions/README.txt   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/
   tracker/roundup-src/share/roundup/templates/classic/html/_generic.404.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/_generic.calendar.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/_generic.collision.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/_generic.help-empty.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/_generic.help-list.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/_generic.help-search.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/_generic.help-submit.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/_generic.help.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/_generic.index.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/_generic.item.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/file.index.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/file.item.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/help.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/help_controls.js   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/home.classlist.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/home.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/issue.index.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/issue.item.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/issue.search.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/keyword.item.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/msg.index.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/msg.item.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/page.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/query.edit.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/query.item.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/style.css   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/user.forgotten.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/user.help-search.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/user.help.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/user.index.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/user.item.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/user.register.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/user.rego_progress.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/html/user_utils.js   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/initial_data.py   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/classic/schema.py   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/minimal/
   tracker/roundup-src/share/roundup/templates/minimal/TEMPLATE-INFO.txt   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/minimal/detectors/
   tracker/roundup-src/share/roundup/templates/minimal/detectors/userauditor.py   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/minimal/extensions/
   tracker/roundup-src/share/roundup/templates/minimal/extensions/README.txt   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/minimal/html/
   tracker/roundup-src/share/roundup/templates/minimal/html/_generic.404.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/minimal/html/_generic.calendar.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/minimal/html/_generic.collision.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/minimal/html/_generic.help.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/minimal/html/_generic.index.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/minimal/html/_generic.item.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/minimal/html/help_controls.js   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/minimal/html/home.classlist.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/minimal/html/home.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/minimal/html/page.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/minimal/html/style.css   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/minimal/html/user.index.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/minimal/html/user.item.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/minimal/html/user.register.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/minimal/html/user.rego_progress.html   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/minimal/initial_data.py   (contents, props changed)
   tracker/roundup-src/share/roundup/templates/minimal/schema.py   (contents, props changed)
   tracker/roundup-src/test/test_anypy_hashlib.py   (contents, props changed)
   tracker/roundup-src/test/test_userauditor.py   (contents, props changed)
   tracker/roundup-src/test/test_xmlrpc.py   (contents, props changed)
Removed:
   tracker/roundup-src/ChangeLog
   tracker/roundup-src/doc/roundup-admin.1
   tracker/roundup-src/doc/roundup-demo.1
   tracker/roundup-src/doc/roundup-mailgw.1
   tracker/roundup-src/doc/roundup-server.1
   tracker/roundup-src/roundup/backends/back_metakit.py
   tracker/roundup-src/templates/
   tracker/roundup-src/test/test_metakit.py
Modified:
   tracker/roundup-src/CHANGES.txt
   tracker/roundup-src/COPYING.txt
   tracker/roundup-src/MANIFEST.in
   tracker/roundup-src/README.txt
   tracker/roundup-src/demo.py
   tracker/roundup-src/detectors/creator_resolution.py
   tracker/roundup-src/doc/FAQ.txt
   tracker/roundup-src/doc/admin_guide.txt
   tracker/roundup-src/doc/announcement.txt
   tracker/roundup-src/doc/customizing.txt
   tracker/roundup-src/doc/default.css
   tracker/roundup-src/doc/design.txt
   tracker/roundup-src/doc/developers.txt
   tracker/roundup-src/doc/index.txt
   tracker/roundup-src/doc/installation.txt
   tracker/roundup-src/doc/upgrading.txt
   tracker/roundup-src/doc/user_guide.txt
   tracker/roundup-src/frontends/ZRoundup/ZRoundup.py
   tracker/roundup-src/frontends/ZRoundup/__init__.py
   tracker/roundup-src/frontends/roundup.cgi
   tracker/roundup-src/locale/de.po
   tracker/roundup-src/locale/en.po
   tracker/roundup-src/locale/fr.po
   tracker/roundup-src/locale/lt.po
   tracker/roundup-src/locale/roundup.pot
   tracker/roundup-src/locale/ru.po
   tracker/roundup-src/locale/zh_CN.po
   tracker/roundup-src/locale/zh_TW.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_mysql.py
   tracker/roundup-src/roundup/backends/back_postgresql.py
   tracker/roundup-src/roundup/backends/back_sqlite.py
   tracker/roundup-src/roundup/backends/back_tsearch2.py
   tracker/roundup-src/roundup/backends/blobfiles.py
   tracker/roundup-src/roundup/backends/indexer_common.py
   tracker/roundup-src/roundup/backends/indexer_dbm.py
   tracker/roundup-src/roundup/backends/indexer_rdbms.py
   tracker/roundup-src/roundup/backends/indexer_xapian.py
   tracker/roundup-src/roundup/backends/locking.py
   tracker/roundup-src/roundup/backends/portalocker.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/backends/tsearch2_setup.py
   tracker/roundup-src/roundup/cgi/PageTemplates/GlobalTranslationService.py
   tracker/roundup-src/roundup/cgi/PageTemplates/__init__.py
   tracker/roundup-src/roundup/cgi/TAL/TranslationContext.py
   tracker/roundup-src/roundup/cgi/TranslationService.py
   tracker/roundup-src/roundup/cgi/ZTUtils/Batch.py
   tracker/roundup-src/roundup/cgi/ZTUtils/Iterator.py
   tracker/roundup-src/roundup/cgi/ZTUtils/__init__.py
   tracker/roundup-src/roundup/cgi/actions.py
   tracker/roundup-src/roundup/cgi/apache.py
   tracker/roundup-src/roundup/cgi/cgitb.py
   tracker/roundup-src/roundup/cgi/client.py
   tracker/roundup-src/roundup/cgi/exceptions.py
   tracker/roundup-src/roundup/cgi/form_parser.py
   tracker/roundup-src/roundup/cgi/templating.py
   tracker/roundup-src/roundup/cgi/wsgi_handler.py
   tracker/roundup-src/roundup/configuration.py
   tracker/roundup-src/roundup/date.py
   tracker/roundup-src/roundup/exceptions.py
   tracker/roundup-src/roundup/hyperdb.py
   tracker/roundup-src/roundup/i18n.py
   tracker/roundup-src/roundup/init.py
   tracker/roundup-src/roundup/install_util.py
   tracker/roundup-src/roundup/instance.py
   tracker/roundup-src/roundup/mailer.py
   tracker/roundup-src/roundup/mailgw.py
   tracker/roundup-src/roundup/password.py
   tracker/roundup-src/roundup/roundupdb.py
   tracker/roundup-src/roundup/scripts/__init__.py
   tracker/roundup-src/roundup/scripts/roundup_admin.py
   tracker/roundup-src/roundup/scripts/roundup_demo.py
   tracker/roundup-src/roundup/scripts/roundup_gettext.py
   tracker/roundup-src/roundup/scripts/roundup_mailgw.py
   tracker/roundup-src/roundup/scripts/roundup_server.py
   tracker/roundup-src/roundup/security.py
   tracker/roundup-src/roundup/token.py
   tracker/roundup-src/roundup/version_check.py
   tracker/roundup-src/run_tests.py
   tracker/roundup-src/scripts/add-issue
   tracker/roundup-src/scripts/copy-user.py
   tracker/roundup-src/scripts/import_sf.py
   tracker/roundup-src/scripts/roundup-reminder
   tracker/roundup-src/scripts/roundup.rc-debian
   tracker/roundup-src/scripts/weekly-report
   tracker/roundup-src/setup.py
   tracker/roundup-src/test/README.txt
   tracker/roundup-src/test/db_test_base.py
   tracker/roundup-src/test/test_anydbm.py
   tracker/roundup-src/test/test_cgi.py
   tracker/roundup-src/test/test_dates.py
   tracker/roundup-src/test/test_hyperdbvals.py
   tracker/roundup-src/test/test_indexer.py
   tracker/roundup-src/test/test_locking.py
   tracker/roundup-src/test/test_mailgw.py
   tracker/roundup-src/test/test_mailsplit.py
   tracker/roundup-src/test/test_multipart.py
   tracker/roundup-src/test/test_mysql.py
   tracker/roundup-src/test/test_postgresql.py
   tracker/roundup-src/test/test_schema.py
   tracker/roundup-src/test/test_security.py
   tracker/roundup-src/test/test_sqlite.py
   tracker/roundup-src/test/test_templating.py
   tracker/roundup-src/test/test_token.py
   tracker/roundup-src/test/test_tsearch2.py
   tracker/roundup-src/tools/load_tracker.py
   tracker/roundup-src/tools/pygettext.py

Modified: tracker/roundup-src/CHANGES.txt
==============================================================================
--- tracker/roundup-src/CHANGES.txt	(original)
+++ tracker/roundup-src/CHANGES.txt	Sun Mar 15 22:43:30 2009
@@ -1,7 +1,78 @@
 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
+2009-03-13 1.4.7 (r4202)
+
+Features:
+- Provide a "no selection" option in web interface selection widgets
+- Debug logging now uses the logging module rather than print
+- Allow CGI frontend to serve XMLRPC requests.
+- Added XMLRPC actions, as well as bridging CGI actions to XMLRPC actions.
+- Optimized large file serving via mod_python / sendfile().
+- Support resuming downloads for (large) files.
+
+Fixes:
+- a number of security issues were discovered by Daniel Diniz
+- EditCSV and ExportCSV altered to include permission checks
+- HTTP POST required on actions which alter data
+- HTML file uploads served as application/octet-stream
+- Handle Unauthorised in file serving correctly
+- New item action reject creation of new users
+- Item retirement was not being controlled
+- Roundup is now compatible with Python 2.6
+- Improved French and German translations
+- Improve consistency of item sorting in HTML interface
+- Various other small bug fixes, robustification and optimisation
+
+
+2008-09-01 1.4.6
+Fixed:
+- Fix bug introduced in 1.4.5 in RDBMS full-text indexing
+- Make URL matching code less matchy
+- Try to clarify mail_domain config setting
+
+
+2008-08-19 1.4.5
+Feature:
+- Add use of username/password stored in ~/.netrc in mailgw (sf patch
+  #1912105)
+
+Fixed:
+- 'Make a Copy' failed with more than one person in nosy list (sf #1906147)
+- xml-rpc security checks and tests across all backends (sf #1907211)
+- Send a Precedence header in email so (well-written) autoresponders don't
+- Fix mailgw total failure bounce message generation (thanks Bradley Dean)
+- Fix for postgres 8.3 compatibility (and bug) (sf patch #2030479 and bug
+  #1959261)
+- Fix for translations (sf patch #2032526)
+- Fire reactors after file storage is all done (sf patch #2001243)
+- Allow negative ids other than -1 for item generation (sf patch #1982481)
+- Better German translation for retiring users (sf #1998701)
+- More improvements to German translation (sf #1919446)
+- Add filter() to XML-RPC interface (sf patch #1966456)
+- Fix IndexError when there are no messages to an issue (sf patch #1894249)
+- Prevent broken pipe errors in csv export (sf patch #1911449)
+- New session API and cleanup thanks anatoly t.
+- Make WSGI handler threadsafe (sf #1968027)
+- Improved URL matching RE (sf #2038858)
+- Allow binary file content submission via XML-RPC (sf #1995623)
+- Don't run old code on newer database (sf #1979556)
+- Fix HTML injection into page title
+- Fix indexer handling of indexed Link properties (sf #1936876)
+
+
+2008-03-01 1.4.4
+Fixed:
+- Security fixes (thanks Roland Meister)
+
+
+2008-02-27 1.4.3
+Fixed:
+- MySQL backend bug introduced in 1.4.2 (TEXT columns need a size when
+  being indexed)
+
+
+2008-02-08 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

Modified: tracker/roundup-src/COPYING.txt
==============================================================================
--- tracker/roundup-src/COPYING.txt	(original)
+++ tracker/roundup-src/COPYING.txt	Sun Mar 15 22:43:30 2009
@@ -1,7 +1,7 @@
 Roundup Licensing
 -----------------
 
-Copyright (c) 2003 Richard Jones (richard at mechanicalcat.net)
+Copyright (c) 2003-2009 Richard Jones (richard at mechanicalcat.net)
 Copyright (c) 2002 eKit.com Inc (http://www.ekit.com/)
 Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
 

Deleted: tracker/roundup-src/ChangeLog
==============================================================================
--- tracker/roundup-src/ChangeLog	Sun Mar 15 22:43:30 2009
+++ (empty file)
@@ -1,926 +0,0 @@
-2006-11-22  Stefan Seefeld  <stefan at codesourcery.com>
-
-	* roundup/cgi/form_parser.py: Allow required fields to be ommitted
-	if user doesn't have edit permission and the value is already set.
-
-2001-08-03 11:54  richard
-
-	* BUILD.txt, CHANGES.txt, README.txt, setup.py,
-	roundup/templates/classic/htmlbase.py: Started stuff off for the
-	0.2.5 release
-
-2001-08-03 11:28  richard
-
-	* roundup-admin, roundup-mailgw, roundup-server,
-	cgi-bin/roundup.cgi, roundup/init.py: Used the much nicer
-	load_package, pointed out by Steve Majewski.
-
-2001-08-03 11:19  richard
-
-	* roundup/templates/classic/: htmlbase.py, html/issue.item,
-	html/style.css: finished of colourising the classic template
-
-2001-08-03 10:59  richard
-
-	* CHANGES.txt: chnages
-
-2001-08-03 10:59  richard
-
-	* roundup-admin, roundup-mailgw, roundup-server,
-	cgi-bin/roundup.cgi, roundup/init.py: Instance import now imports
-	the instance using imp.load_module so that we can have instance
-	homes of "roundup" or other existing python package names.
-
-2001-08-02 20:26  richard
-
-	* README.txt: changes
-
-2001-08-02 16:38  richard
-
-	* roundup/: cgi_client.py, hyperdb.py, roundupdb.py,
-	templates/classic/dbinit.py, templates/classic/instance_config.py,
-	templates/extended/dbinit.py,
-	templates/extended/instance_config.py: Roundupdb now appends
-	"mailing list" information to its messages which include the e-mail
-	address and web interface address. Templates may override this in
-	their db classes to include specific information (support
-	instructions, etc).
-
-2001-08-02 16:00  richard
-
-	* CHANGES.txt: anges
-
-2001-08-02 15:55  richard
-
-	* roundup/cgi_client.py: Web edit messages aren't sent to the
-	person who did the edit any more. No message is generated if they
-	are the only person on the nosy list.
-
-2001-08-02 11:01  richard
-
-	* CHANGES.txt: changes
-
-2001-08-02 11:00  richard
-
-	* BUILD.txt: Added the 'clean' command to the instructions -
-	distutils doesn't seem to always detect when it needs to rebuild
-	when it should.
-
-2001-08-02 10:43  richard
-
-	* roundup/templates/extended/interfaces.py: Even better (more
-	useful) headings
-
-2001-08-02 10:36  richard
-
-	* roundup/templates/extended/interfaces.py: Made all the
-	user-specific link names the same (My Foo)
-
-2001-08-02 10:34  richard
-
-	* roundup/cgi_client.py: bleah syntax error
-
-2001-08-02 10:27  richard
-
-	* CHANGES.txt, roundup/templates/extended/htmlbase.py: changes
-
-2001-08-02 10:27  richard
-
-	* roundup/date.py: Extended the range of intervals that are
-	pretty-printed before actual dates are displayed.
-
-2001-08-02 10:26  richard
-
-	* roundup/cgi_client.py: Changed the order of the information in
-	the message generated by web edits.
-
-2001-08-01 15:15  richard
-
-	* CHANGES.txt: changes
-
-2001-08-01 15:15  richard
-
-	* README.txt, roundup/templates/extended/htmlbase.py,
-	roundup/templates/extended/interfaces.py,
-	roundup/templates/extended/html/issue.index,
-	roundup/templates/extended/html/support.index: Added "My Issues"
-	and "My Support" to extended template.
-
-2001-08-01 15:06  richard
-
-	* CHANGES.txt: changes
-
-2001-08-01 15:06  richard
-
-	* roundup/: templatebuilder.py, templates/classic/htmlbase.py,
-	templates/extended/htmlbase.py: htmlbase doesn't have extraneous
-	$Foo$ in it any more
-
-2001-08-01 14:24  richard
-
-	* roundup/: hyperdb.py, mailgw.py: mailgw was assuming certain
-	properties existed on the issues being created.
-
-2001-08-01 13:52  richard
-
-	* CHANGES.txt, roundup/htmltemplate.py: Checklist was using wrong
-	name.
-
-2001-08-01 13:48  richard
-
-	* README.txt: Just a new idea...
-
-2001-07-31 19:58  richard
-
-	* CHANGES.txt: changes
-
-2001-07-31 19:54  richard
-
-	* roundup/date.py: Fixed the 2.1-specific gmtime() (no arg) call in
-	roundup.date. (Paul Wright)
-
-2001-07-30 18:12  richard
-
-	* CHANGES.txt, roundup-admin, roundup/cgi_client.py,
-	roundup/htmltemplate.py, roundup/templatebuilder.py,
-	roundup/templates/classic/htmlbase.py,
-	roundup/templates/classic/html/file.newitem,
-	roundup/templates/classic/html/issue.item,
-	roundup/templates/extended/htmlbase.py,
-	roundup/templates/extended/interfaces.py: Added time logging and
-	file uploading to the templates.
-
-2001-07-30 18:04  richard
-
-	* roundup/templates/extended/html/: file.newitem, timelog.index,
-	timelog.item: oops
-
-2001-07-30 18:03  richard
-
-	* roundup/templates/extended/html/: issue.item, support.item: Fixes
-	to the uploading stuff (I forgot to put the code in the issue class
-	;)
-
-2001-07-30 17:17  richard
-
-	* setup.py: Just making sure we've got the right version in there
-	for development.
-
-2001-07-30 16:26  richard
-
-	* roundup/cgi_client.py: Added some documentation on how the
-	newblah works.
-
-2001-07-30 16:17  richard
-
-	* roundup/: cgi_client.py, htmltemplate.py: Features:  . Added
-	ability for cgi newblah forms to indicate that the new node   
-	should be linked somewhere.  Fixed:  . Fixed the agument handling
-	for the roundup-admin find command.   . Fixed handling of summary
-	when no note supplied for newblah. Again.   . Fixed detection of no
-	form in htmltemplate Field display.
-
-2001-07-30 13:53  richard
-
-	* CHANGES.txt: chanegs
-
-2001-07-30 13:52  richard
-
-	* roundup-admin: init help now lists templates and backends
-
-2001-07-30 13:52  richard
-
-	* roundup/backends/__init__.py: Checks for ability to import the
-	specific back-end module.
-
-2001-07-30 13:45  richard
-
-	* test/test_db.py: Added more DB to test_db. Can skip tests where
-	imports fail.
-
-2001-07-30 12:38  richard
-
-	* roundup/templates/: classic/htmlbase.py, extended/htmlbase.py:
-	updated htmlbases
-
-2001-07-30 12:38  richard
-
-	* roundup/: hyperdb.py, roundupdb.py: get() now has a default arg -
-	for migration only.
-
-2001-07-30 12:37  richard
-
-	* roundup/htmltemplate.py: Temporary measure until we have decent
-	schema migration.
-
-2001-07-30 12:37  richard
-
-	* roundup/cgi_client.py: Temporary measure until we have decent
-	schema migration...
-
-2001-07-30 12:37  richard
-
-	* roundup-admin: Freshen is really broken. Commented out.
-
-2001-07-30 12:36  richard
-
-	* roundup/backends/: back_bsddb.py, back_bsddb3.py: Handle
-	non-existence of db files in the other backends (code from anydbm).
-
-2001-07-30 12:35  richard
-
-	* roundup/templates/extended/html/issue.item: Should've been
-	supportcall
-
-2001-07-30 11:47  richard
-
-	* roundup/templates/extended/html/issue.item: Forgot to add the
-	support call property to the item page.
-
-2001-07-30 11:41  richard
-
-	* roundup/backends/: back_anydbm.py, back_bsddb.py, back_bsddb3.py:
-	Makes schema changes mucho easier.
-
-2001-07-30 11:32  richard
-
-	* CHANGES.txt, README.txt: noted changes
-
-2001-07-30 11:28  richard
-
-	* roundup-admin: Bugfixes
-
-2001-07-30 11:28  richard
-
-	* roundup/templates/__init__.py: Support for determining the
-	installed tempaltes
-
-2001-07-30 11:27  richard
-
-	* roundup/templates/extended/html/: support.filter, support.index,
-	support.item: Oops - these are the HTML displays for the support
-	class.
-
-2001-07-30 11:26  richard
-
-	* roundup/templates/extended/: dbinit.py, htmlbase.py,
-	interfaces.py, html/issue.filter, html/issue.index,
-	html/issue.item: Big changes:  . split off the support priority
-	into its own class  . added "new support, new user" to the page
-	head  . fixed the display options for the heading links
-
-2001-07-30 11:25  richard
-
-	* roundup/templates/classic/: htmlbase.py, interfaces.py: Changes
-	to reflect cgi_client now implementing this template by default,
-	and not "extended".
-
-2001-07-30 11:25  richard
-
-	* roundup/cgi_client.py: Default implementation is now "classic"
-	rather than "extended" as one would expect.
-
-2001-07-30 11:24  richard
-
-	* roundup/htmltemplate.py: Handles new node display now.
-
-2001-07-30 10:57  richard
-
-	* roundup-admin: Now uses getopt, much improved command-line
-	parsing. Much fuller help. Much better internal structure. It's
-	just BETTER. :)
-
-2001-07-30 10:06  richard
-
-	* roundup/templatebuilder.py: Hrm - had IOError instead of OSError.
-	Not sure why there's two. Ho hum.
-
-2001-07-30 10:05  richard
-
-	* roundup/roundupdb.py: Fixed IssueClass so that superseders links
-	to its classname rather than hard-coded to "issue".
-
-2001-07-30 10:04  richard
-
-	* roundup-admin: Made the "init" prompting more friendly.
-
-2001-07-30 09:34  richard
-
-	* CHANGES.txt, roundup/templates/classic/htmlbase.py,
-	roundup/templates/extended/htmlbase.py: changes
-
-2001-07-30 09:34  richard
-
-	* setup.py: Added unit tests so they're run whenever we
-	package/install/whatever.
-
-2001-07-30 09:32  richard
-
-	* test/test_dates.py: Fixed bug in unit test ;)
-
-2001-07-29 19:43  richard
-
-	* setup.py: Make sure that the htmlbase is up-to-date when we build
-	a source dist.
-
-2001-07-29 19:33  richard
-
-	* CHANGES.txt: changes
-
-2001-07-29 19:31  richard
-
-	* roundup/htmltemplate.py: oops
-
-2001-07-29 19:28  richard
-
-	* roundup/: htmltemplate.py, hyperdb.py: Fixed sorting by clicking
-	on column headings.
-
-2001-07-29 18:37  richard
-
-	* CHANGES.txt, README.txt, setup.py: changes
-
-2001-07-29 18:27  richard
-
-	* roundup/: cgi_client.py, htmltemplate.py, hyperdb.py: Fixed
-	handling of passed-in values in form elements (ie. during a
-	drill-down)
-
-2001-07-29 17:01  richard
-
-	* README.txt, roundup-admin, roundup-mailgw, roundup-server,
-	setup.py, cgi-bin/roundup.cgi, roundup/__init__.py,
-	roundup/cgi_client.py, roundup/cgitb.py, roundup/date.py,
-	roundup/htmltemplate.py, roundup/hyperdb.py, roundup/init.py,
-	roundup/mailgw.py, roundup/roundupdb.py,
-	roundup/templatebuilder.py, roundup/templates/classic/__init__.py,
-	roundup/templates/classic/dbinit.py,
-	roundup/templates/classic/instance_config.py,
-	roundup/templates/classic/interfaces.py,
-	roundup/templates/extended/__init__.py,
-	roundup/templates/extended/dbinit.py,
-	roundup/templates/extended/instance_config.py,
-	roundup/templates/extended/interfaces.py, test/README.txt,
-	test/__init__.py, test/test_dates.py, test/test_db.py,
-	test/test_multipart.py, test/test_schema.py: Added vim command to
-	all source so that we don't get no steenkin' tabs :)
-
-2001-07-29 16:42  richard
-
-	* test/test_dates.py: Added Interval tests.
-
-2001-07-29 15:41  richard
-
-	* CHANGES.txt: changes
-
-2001-07-29 15:36  richard
-
-	* roundup/: htmltemplate.py, hyperdb.py: Cleanup of the link label
-	generation.
-
-2001-07-29 14:11  richard
-
-	* CHANGES.txt: Reverse the entries so most recent is first.
-
-2001-07-29 14:09  richard
-
-	* test/test_db.py: Added the fabricated property "id" to all
-	hyperdb classes.
-
-2001-07-29 14:07  richard
-
-	* roundup/templates/classic/: interfaces.py, html/file.index,
-	html/issue.filter, html/issue.index, html/issue.item,
-	html/msg.index, html/msg.item, html/style.css, html/user.index,
-	html/user.item: Fixed the classic template so it's more like the
-	"advertised" Roundup template.
-
-2001-07-29 14:06  richard
-
-	* roundup/htmltemplate.py: Fixed problem in link display when Link
-	value is None.
-
-2001-07-29 14:05  richard
-
-	* roundup/: hyperdb.py, roundupdb.py: Added the fabricated property
-	"id".
-
-2001-07-29 14:04  richard
-
-	* roundup/cgi_client.py: Moved some code around allowing for
-	subclassing to change behaviour.
-
-2001-07-28 18:17  richard
-
-	* roundup/htmltemplate.py: fixed use of stylesheet
-
-2001-07-28 18:16  richard
-
-	* roundup/cgi_client.py: New issue form handles lack of note better
-	now.
-
-2001-07-28 18:02  richard
-
-	* roundup/templatebuilder.py: commented out print
-
-2001-07-28 17:59  richard
-
-	* roundup/: htmltemplate.py, init.py, templatebuilder.py: Replaced
-	errno integers with their module values.  De-tabbed
-	templatebuilder.py
-
-2001-07-28 17:35  richard
-
-	* README.txt: todo refinement ;)
-
-2001-07-28 16:44  richard
-
-	* CHANGES.txt, README.txt, doc/implementation.txt: Split off
-	implementation notes into separate file in doc directory. Added
-	some todo items to the README
-
-2001-07-28 16:43  richard
-
-	* roundup/mailgw.py, test/__init__.py, test/test_multipart.py:
-	Multipart message class has the getPart method now. Added some
-	tests for it.
-
-2001-07-28 11:56  richard
-
-	* CHANGES.txt, MANIFEST.in: changes
-
-2001-07-28 11:45  richard
-
-	* doc/: overview.html, spec.html, images/edit.gif, images/edit.png,
-	images/hyperdb.gif, images/hyperdb.png, images/logo-acl-medium.gif,
-	images/logo-acl-medium.png, images/logo-codesourcery-medium.gif,
-	images/logo-codesourcery-medium.png,
-	images/logo-software-carpentry-standard.gif,
-	images/logo-software-carpentry-standard.png, images/roundup-1.gif,
-	images/roundup-1.png, images/roundup.gif, images/roundup.png: GIF
-	-> PNG, saving about 100k
-
-2001-07-28 11:40  richard
-
-	* doc/: overview.html, images/edit.gif, images/hyperdb.gif,
-	images/roundup-1.gif, images/roundup.gif: added more documentation
-
-2001-07-28 11:39  richard
-
-	* roundup/__init__.py: Added some documentation to the roundup
-	package.
-
-2001-07-28 10:39  richard
-
-	* CHANGES.txt, setup.py: changes for the 0.2.1 distribution build.
-
-2001-07-28 10:34  richard
-
-	* CHANGES.txt: changes
-
-2001-07-28 10:34  richard
-
-	* roundup/: cgi_client.py, mailgw.py: Fixed some non-string node
-	ids.
-
-2001-07-28 10:31  richard
-
-	* INSTALL.txt, roundup/templatebuilder.py: Fixed some problems with
-	installation.
-
-2001-07-27 17:33  richard
-
-	* INSTALL.txt: more notes for installation
-
-2001-07-27 17:30  richard
-
-	* BUILD.txt: minor notes
-
-2001-07-27 17:27  richard
-
-	* BUILD.txt, README.txt: Added build instructions, changed my
-	e-mail address in the docs to the sourceforge address.
-
-2001-07-27 17:20  richard
-
-	* Makefile, setup.cfg, setup.py: Makefile is now obsolete - setup
-	does what it used to do.
-
-2001-07-27 17:18  richard
-
-	* MANIFEST.in: Added the distutils manifest template (for
-	"documentation", see distutils.filelist).  Has no facility for
-	comments, so no ID or LOG for this baby.
-
-2001-07-27 17:16  richard
-
-	* test/: README.TXT, README.txt: rename for consistency
-
-2001-07-27 17:04  richard
-
-	* INSTALL.TXT, CHANGES.txt, INSTALL.txt, README.TXT, README.txt:
-	name changes to make distutils happy
-
-2001-07-27 16:56  richard
-
-	* setup.cfg, setup.py: Added scripts to the setup and added the
-	config so the default script install dir is /usr/local/bin.
-
-2001-07-27 16:55  richard
-
-	* test/: README.TXT, __init__.py, test_dates.py, test_db.py,
-	test_schema.py: moving tests -> test
-
-2001-07-27 16:25  richard
-
-	* roundup/hyperdb.py: Fixed some of the exceptions so they're the
-	right type.  Removed the str()-ification of node ids so we don't
-	mask oopsy errors any more.
-
-2001-07-27 15:17  richard
-
-	* roundup/hyperdb.py: just some comments
-
-2001-07-26 17:14  richard
-
-	* setup.py: Made setup.py executable, added id and log.
-
-2001-07-26 16:47  richard
-
-	* INSTALL.TXT: Updated for new installation procedure
-
-2001-07-25 14:19  anthonybaxter
-
-	* setup.py: first cut at setup.py - installs the package, but not
-	the bin/cgi-bin yet
-
-2001-07-25 14:09  richard
-
-	* roundup/date.py: Fixed offset handling (shoulda read the spec a
-	little better)
-
-2001-07-25 13:40  richard
-
-	* README.TXT: added note about the spec
-
-2001-07-25 13:39  richard
-
-	* roundup/htmltemplate.py: Hrm - displaying links to classes that
-	don't specify a key property. I've got it defaulting to 'name',
-	then 'title' and then a "random" property (first one returned by
-	getprops().keys().  Needs to be moved onto the Class I think...
-
-2001-07-25 11:23  richard
-
-	* doc/spec.html, doc/images/logo-acl-medium.gif,
-	doc/images/logo-codesourcery-medium.gif,
-	doc/images/logo-software-carpentry-standard.gif,
-	roundup/backends/back_anydbm.py,
-	roundup/templates/extended/dbinit.py: Added the Roundup spec to the
-	new documentation directory.
-
-2001-07-24 21:18  anthonybaxter
-
-	* roundup/init.py: oops. left a print in
-
-2001-07-24 20:54  anthonybaxter
-
-	* roundup/: init.py, templatebuilder.py: oops. Html.
-
-2001-07-24 20:46  anthonybaxter
-
-	* roundup/: init.py, templatebuilder.py, templates/__init__.py,
-	templates/classic/__init__.py, templates/classic/dbinit.py,
-	templates/classic/htmlbase.py, templates/extended/__init__.py,
-	templates/extended/htmlbase.py: Added templatebuilder module. two
-	functions - one to pack up the html base, one to unpack it. Packed
-	up the two standard templates into htmlbases.  Modified __init__ to
-	install them.
-	
-	__init__.py magic was needed for the rather high levels of wierd
-	import magic.  Reducing level of import magic == (good, future)
-
-2001-07-24 14:26  anthonybaxter
-
-	* roundup/backends/back_bsddb3.py: bsddb3 implementation. For now,
-	it's the bsddb implementation with a "3" added in crayon.
-
-2001-07-24 11:07  richard
-
-	* roundup-server: Added command-line arg handling to roundup-server
-	so it's more useful out-of-the-box.
-
-2001-07-24 11:06  richard
-
-	* roundup/templates/classic/dbinit.py: Oops - accidentally duped
-	the keywords class
-
-2001-07-24 09:32  richard
-
-	* INSTALL.TXT: minor edit
-
-2001-07-24 09:28  richard
-
-	* roundup/templates/: README.txt, classic/__init__.py,
-	classic/dbinit.py, classic/instance_config.py,
-	classic/interfaces.py, classic/detectors/__init__.py,
-	classic/detectors/nosyreaction.py, classic/html/file.index,
-	classic/html/issue.filter, classic/html/issue.index,
-	classic/html/issue.item, classic/html/msg.index,
-	classic/html/msg.item, classic/html/style.css,
-	classic/html/user.index, classic/html/user.item: Adding the classic
-	template
-
-2001-07-24 09:20  richard
-
-	* roundup/templates/extended/dbinit.py: forgot to remove the
-	interfaces from the dbinit module ;)
-
-2001-07-24 09:16  richard
-
-	* roundup/templates/extended/: __init__.py, interfaces.py: Split
-	off the interfaces (CGI, mailgw) into a separate file from the DB
-	stuff.
-
-2001-07-23 20:31  richard
-
-	* roundup-server: disabled the reloading until it can be done
-	properly
-
-2001-07-23 18:55  richard
-
-	* CHANGES, INSTALL.TXT, README, README.TXT: renamed the text files
-	so that they're recognised as text files on windows added
-	INSTALL.TXT
-
-2001-07-23 18:53  richard
-
-	* README, roundup-server: Fixed the ROUNDUPS decl in roundup-server
-	Move the installation notes to INSTALL
-
-2001-07-23 18:45  richard
-
-	* roundup-admin, roundup/init.py,
-	roundup/templates/extended/dbinit.py: ok, so now "./roundup-admin
-	init" will ask questions in an attempt to get a workable
-	instance_home set up :) _and_ anydbm has had its first test :)
-
-2001-07-23 18:25  richard
-
-	* roundup/backends/back_bsddb.py: more handling of bad journals
-
-2001-07-23 18:20  richard
-
-	* roundup-admin, roundup/backends/back_anydbm.py,
-	roundup/backends/back_bsddb.py: Moved over to using marshal in the
-	bsddb and anydbm backends.  roundup-admin now has a "freshen"
-	command that'll load/save all nodes (not  retired - mod
-	hyperdb.Class.list() so it lists retired nodes)
-
-2001-07-23 17:56  richard
-
-	* roundup/: date.py, backends/back_bsddb.py: Storing only
-	marshallable data in the db - no nasty pickled class references.
-
-2001-07-23 17:22  richard
-
-	* roundup/backends/: __init__.py, _anydbm.py, _bsddb.py,
-	back_anydbm.py, back_bsddb.py: *sigh* some databases have _foo.so
-	as their underlying implementation.  This time for sure, Rocky.
-
-2001-07-23 17:15  richard
-
-	* roundup/backends/: _anydbm.py, _bsddb.py, bsddb.py: Moved the
-	backends into the backends package. Anydbm hasn't been tested at
-	all.
-
-2001-07-23 17:14  richard
-
-	* roundup/: roundupdb.py, backends/__init__.py,
-	templates/extended/dbinit.py: Moved the database backends off into
-	backends.
-
-2001-07-23 16:25  richard
-
-	* roundup/templates/extended/dbinit.py: relfected the move to
-	roundup/backends
-
-2001-07-23 16:24  richard
-
-	* roundup/backends/__init__.py: made backends a package
-
-2001-07-23 16:23  richard
-
-	* roundup/: hyper_bsddb.py, backends/bsddb.py: moved hyper_bsddb.py
-	to the new backends package as bsddb.py
-
-2001-07-23 14:49  anthonybaxter
-
-	* README: changed the 'snip' lines so they don't look like CVS
-	conflict markers.
-
-2001-07-23 14:47  anthonybaxter
-
-	* cgi-bin/roundup.cgi: renamed ROUNDUPS to ROUNDUP_INSTANCE_HOMES
-	sys.exit(0) if python version wrong.
-
-2001-07-23 14:33  richard
-
-	* cgi-bin/roundup.cgi: brought the CGI instance config dict in line
-	with roundup-server
-
-2001-07-23 14:33  anthonybaxter
-
-	* roundup/templates/extended/: __init__.py, dbinit.py,
-	instance_config.py: split __init__.py into 2. dbinit and
-	instance_config.
-
-2001-07-23 14:31  richard
-
-	* CHANGES, cgi-bin/roundup.cgi: Fixed the roundup CGI script for
-	updates to cgi_client.py
-
-2001-07-23 14:21  richard
-
-	* roundup/templates/extended/: html/file.index, html/issue.filter,
-	html/issue.index, html/issue.item, html/msg.index, html/msg.item,
-	html/style.css, html/user.index, html/user.item, issue.filter,
-	issue.item, msg.item, style.css, user.item: moving HTML templates
-	to their own dir
-
-2001-07-23 14:19  richard
-
-	* roundup/templates/extended/: file.index, issue.index, msg.index,
-	user.index: moving the HTML templates into their own dir
-
-2001-07-23 14:05  anthonybaxter
-
-	* roundup-server: actually quit if python version wrong
-
-2001-07-23 13:56  richard
-
-	* roundup/cgi_client.py: oops, missed a config removal
-
-2001-07-23 13:50  anthonybaxter
-
-	* roundup/templates/extended/: __init__.py, file.index,
-	issue.filter, issue.index, issue.item, msg.index, msg.item,
-	style.css, user.index, user.item, detectors/__init__.py,
-	detectors/nosyreaction.py: moved templates to proper location
-
-2001-07-23 13:46  richard
-
-	* roundup-admin, roundup-mailgw, roundup-server: moving the bin
-	files to facilitate out-of-the-boxness
-
-2001-07-22 22:09  richard
-
-	* roundup/: __init__.py, cgi_client.py, cgitb.py, date.py,
-	htmltemplate.py, hyper_bsddb.py, hyperdb.py, init.py, mailgw.py,
-	roundupdb.py: Final commit of Grande Splite
-
-2001-07-22 21:58  richard
-
-	* roundup/: __init__.py, cgi_client.py, cgitb.py, date.py,
-	htmltemplate.py, hyper_bsddb.py, hyperdb.py, init.py, mailgw.py,
-	roundupdb.py: More Grande Splite
-
-2001-07-22 21:47  richard
-
-	* cgi-bin/roundup.cgi: More Grande Splite
-
-2001-07-22 21:11  richard
-
-	* CHANGES, README, cgitb.py, config.py, date.py, hyperdb.py,
-	hyperdb_bsddb.py, roundup-mailgw.py, roundup.cgi, roundup.py,
-	roundup_cgi.py, roundupdb.py, server.py, style.css, template.py,
-	test.py: Initial commit of the Grande Splite
-
-2001-07-20 22:33  richard
-
-	* server.py: oops ;)
-
-2001-07-20 18:20  richard
-
-	* CHANGES: update for recent chagnes
-
-2001-07-20 18:20  richard
-
-	* README, hyperdb.py: Fixed a bug in the filter - wrong variable
-	names in the error message.  Recognised that the filter has an
-	outstanding bug. Hrm. we need a bug tracker for this project :)
-
-2001-07-20 17:35  richard
-
-	* CHANGES, hyperdb.py, hyperdb_bsddb.py, roundup_cgi.py,
-	roundupdb.py, test.py: largish changes as a start of splitting off
-	bits and pieces to allow more flexible installation / database
-	back-ends
-
-2001-07-20 17:34  richard
-
-	* template.py: Quote the value put in the text input value
-	attribute.
-
-2001-07-20 11:37  richard
-
-	* README: Just registering a new TODO
-
-2001-07-20 10:53  richard
-
-	* roundup_cgi.py: Default index now filters out the resolved issues
-	;)
-
-2001-07-20 10:23  richard
-
-	* CHANGES: update for latest changes
-
-2001-07-20 10:22  richard
-
-	* roundupdb.py: Priority list changes - removed the redundant TODO
-	and added support. See roundup-devel for details.
-
-2001-07-20 10:17  richard
-
-	* roundup_cgi.py: Fixed adding a new issue when there is no __note
-
-2001-07-19 20:43  anthonybaxter
-
-	* config.py, server.py: HTTP_HOST and HTTP_PORT config options.
-
-2001-07-19 16:37  anthonybaxter
-
-	* README: added more todo items
-
-2001-07-19 16:27  anthonybaxter
-
-	* cgitb.py, config.py, date.py, hyperdb.py, roundup-mailgw.py,
-	roundup.py, roundup_cgi.py, roundupdb.py, server.py, template.py:
-	fixing (manually) the (dollarsign)Log(dollarsign) entries caused by
-	my using the magic (dollarsign)Id(dollarsign) and
-	(dollarsign)Log(dollarsign) strings in a commit message. I'm a
-	twonk.
-	
-	Also broke the help string in two.
-
-2001-07-19 16:14  richard
-
-	* Makefile, README, dummy_config.py: minor changes to test the cvs
-	mailout system
-
-2001-07-19 16:08  anthonybaxter
-
-	* roundup.py: fixed typo in usage string because it was bugging me
-	each time I saw it.
-
-2001-07-19 15:52  anthonybaxter
-
-	* cgitb.py, config.py, date.py, hyperdb.py, roundup-mailgw.py,
-	roundup.py, roundup_cgi.py, roundupdb.py, server.py, template.py:
-	Added CVS keywords $Id: ChangeLog,v 1.8 2006/11/23 00:44:48 stefan Exp $ and $Log: ChangeLog,v $
-	Added CVS keywords $Id: ChangeLog,v 1.7 2001/08/03 02:12:07 anthonybaxter Exp $ and Revision 1.8  2006/11/23 00:44:48  stefan
-	Added CVS keywords $Id: ChangeLog,v 1.7 2001/08/03 02:12:07 anthonybaxter Exp $ and Fix sf bug 1599740
-	Added CVS keywords $Id: ChangeLog,v 1.7 2001/08/03 02:12:07 anthonybaxter Exp $ and
-	Added CVS keywords $Id: ChangeLog,v 1.8 2006/11/23 00:44:48 stefan Exp $ and Revision 1.7  2001/08/03 02:12:07  anthonybaxter
-	Added CVS keywords $Id: ChangeLog,v 1.8 2006/11/23 00:44:48 stefan Exp $ and regenerated on Fri Aug  3 12:12:00 EST 2001
-	Added CVS keywords $Id: ChangeLog,v 1.8 2006/11/23 00:44:48 stefan Exp $ and to all python files.
-
-2001-07-19 15:46  anthonybaxter
-
-	* config.py: modified to use localconfig.py (if it exists) and to
-	make the various options (e.g. paths) based on ROUNDUP_HOME &c.
-
-2001-07-19 15:23  richard
-
-	* CHANGES, Makefile, config.py, hyperdb.py, roundup_cgi.py,
-	roundupdb.py, template.py:  . Fixed bug in re generation in the
-	filter (I hadn't finished the code ;)
-	 . Added TODO as a priority (between bug and usability)
-	 . Fixed handling of None String property in grouped list headings
-
-2001-07-19 13:12  richard
-
-	* README: mention config.py in the install instructions, removed a
-	bug
-
-2001-07-19 13:11  richard
-
-	* Makefile, dummy_config.py: Added stuff to help with release
-	generation.   . Makefile has the release tgz builder in it   .
-	dummy_config.py is an empty config file that replaces the config.py
-	in the	   release
-
-2001-07-19 12:16  richard
-
-	* README, date.py, hyperdb.py, roundup.cgi, roundup_cgi.py,
-	roundupdb.py, CHANGES, cgitb.py, config.py, roundup-mailgw.py,
-	roundup.py, server.py, style.css, template.py: Initial revision
-
-2001-07-19 12:16  richard
-
-	* README, date.py, hyperdb.py, roundup.cgi, roundup_cgi.py,
-	roundupdb.py, CHANGES, cgitb.py, config.py, roundup-mailgw.py,
-	roundup.py, server.py, style.css, template.py: Initial import of
-	code - currently version 1.0.2 but with the 1.0.3 changes as given
-	in the CHANGES file. Is about ready for a 1.0.3 release.
-

Modified: tracker/roundup-src/MANIFEST.in
==============================================================================
--- tracker/roundup-src/MANIFEST.in	(original)
+++ tracker/roundup-src/MANIFEST.in	Sun Mar 15 22:43:30 2009
@@ -1,3 +1,4 @@
+recursive-include share *
 recursive-include roundup *.*
 recursive-include frontends *.*
 recursive-include scripts *.* *-*
@@ -6,7 +7,7 @@
 recursive-include doc *.html *.png *.txt *.css *.1 *.example
 recursive-include detectors *.py
 recursive-include templates *.* home* page*
-global-exclude .cvsignore *.pyc *.pyo .DS_Store
+global-exclude .svn .cvsignore *.pyc *.pyo .DS_Store
 include run_tests.py *.txt demo.py MANIFEST.in MANIFEST
 exclude BUILD.txt I18N_PROGRESS.txt TODO.txt
 exclude doc/security.txt doc/templating.txt

Modified: tracker/roundup-src/README.txt
==============================================================================
--- tracker/roundup-src/README.txt	(original)
+++ tracker/roundup-src/README.txt	Sun Mar 15 22:43:30 2009
@@ -2,7 +2,7 @@
 Roundup: an Issue-Tracking System for Knowledge Workers
 =======================================================
 
-Copyright (c) 2003 Richard Jones (richard at mechanicalcat.net)
+Copyright (c) 2003-2009 Richard Jones (richard at mechanicalcat.net)
 Copyright (c) 2002 eKit.com Inc (http://www.ekit.com/)
 Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
 
@@ -31,7 +31,7 @@
 Upgrading
 =========
 For upgrading instructions, please see upgrading.txt in the "doc" directory.
- 
+
 
 Usage and Other Information
 ===========================

Modified: tracker/roundup-src/demo.py
==============================================================================
--- tracker/roundup-src/demo.py	(original)
+++ tracker/roundup-src/demo.py	Sun Mar 15 22:43:30 2009
@@ -2,7 +2,7 @@
 #
 # Copyright (c) 2003 Richard Jones (richard at mechanicalcat.net)
 #
-# $Id: demo.py,v 1.26 2007/08/28 22:37:45 jpend Exp $
+# $Id: demo.py,v 1.26 2007-08-28 22:37:45 jpend Exp $
 
 import errno
 import os
@@ -127,7 +127,7 @@
             backend = sys.argv[-2]
         else:
             backend = 'anydbm'
-        install_demo(home, backend, os.path.join('templates', 'classic'))
+        install_demo(home, backend, os.path.join('share', 'roundup', 'templates', 'classic'))
     run_demo(home)
 
 if __name__ == '__main__':

Modified: tracker/roundup-src/detectors/creator_resolution.py
==============================================================================
--- tracker/roundup-src/detectors/creator_resolution.py	(original)
+++ tracker/roundup-src/detectors/creator_resolution.py	Sun Mar 15 22:43:30 2009
@@ -1,7 +1,7 @@
 # This detector was written by richard at mechanicalcat.net and it's been
 # placed in the Public Domain. Copy and modify to your heart's content.
 
-#$Id: creator_resolution.py,v 1.2 2004/04/07 06:32:54 richard Exp $
+#$Id: creator_resolution.py,v 1.2 2004-04-07 06:32:54 richard Exp $
 
 from roundup.exceptions import Reject
 

Modified: tracker/roundup-src/doc/FAQ.txt
==============================================================================
--- tracker/roundup-src/doc/FAQ.txt	(original)
+++ tracker/roundup-src/doc/FAQ.txt	Sun Mar 15 22:43:30 2009
@@ -2,8 +2,6 @@
 Roundup FAQ
 ===========
 
-:Version: $Revision: 1.23 $
-
 .. contents::
 
 
@@ -204,10 +202,5 @@
 sorting, all items of that Class *must* have a value against the "order"
 property, or sorting will result in random ordering.
 
------------------
-
-Back to `Table of Contents`_
-
-.. _`Table of Contents`: index.html
 .. _`customisation`: customizing.html
 

Added: tracker/roundup-src/doc/_static/style.css
==============================================================================
--- (empty file)
+++ tracker/roundup-src/doc/_static/style.css	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,109 @@
+/* layout*/
+body 
+{
+  font-family: sans-serif, Arial, Helvetica;
+  background-color: white;
+  color: #333;
+  margin:0;
+  padding: 0 3em 0 14em;
+}
+body > .header { margin: 0 0 0 -14em;}
+body > .footer { margin: 0 0 0 -14em; clear:both;}
+body > .navigation 
+{
+  margin-left: -14em;
+  width: 14em;
+  float: left;
+}
+body > .content 
+{
+  width: 100%;
+  margin: 0;
+}
+body > .header > #searchbox { position: absolute; right: 1em; top: 1em;}
+
+/* style */
+
+:link { color: #bb0000; text-decoration: none;}
+:visited { color: #770000; text-decoration: none;}
+a.toc-backref { color: #000000; }
+
+.header h1 { margin-left: 1em; }
+
+body
+{
+  font-family: sans-serif, Arial, Helvetica;
+  background-color: #f5f5f5;
+  color: #333;
+}
+
+.menu 
+{
+  margin-right: 1em;
+  padding: 2pt;
+  border: solid thin #dadada;
+  background-color:#ffffff;
+}
+.menu ul { list-style-type:none; padding: 0;}
+.menu ul ul { padding-left: 1em;}
+.menu li { border-top: solid thin #dadada;}
+.menu li:first-child { border-top: none;}
+
+/* related */
+
+div.related 
+{
+  width: 100%;
+  font-size: 90%;
+}
+div.related-top { border-bottom: solid thin #dadada;}
+div.related-bottom { border-top: solid thin #dadada;}
+
+div.related ul 
+{
+  margin: 0;
+  padding: 0 0 0 10px;
+  list-style: none;
+}
+
+div.related li { display: inline;}
+
+div.related li.right 
+{
+  float: right;
+  margin-right: 5px;
+}
+
+.footer
+{
+  font-size: small;
+  text-align: center;
+  color: lightgrey;
+}
+
+.content
+{ 
+  padding: 1em;
+  border: solid thin #dadada;
+  background-color: #ffffff;
+}
+
+/* This is a little hack to inject a 'news' block into the title
+   page without having to set up a custom directive. */
+#roundup-issue-tracker .note
+{
+  float: right;
+  width: auto;
+  border: solid thin #dadada;
+  background-color:#f5f5f5;
+  padding: 1em;
+  margin: 1em;
+}
+#roundup-issue-tracker .note .admonition-title { display: none; }
+
+table
+{ 
+  border-collapse: collapse;
+  border-spacing: 1px;
+  background-color: #fafafa;
+}

Added: tracker/roundup-src/doc/_templates/layout.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/doc/_templates/layout.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,166 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+{%- macro relbar(class) %}
+    <div class="related {{ class }}">
+      <ul>
+        {%- for rellink in rellinks %}
+        <li class="right" {% if loop.first %}style="margin-right: 10px"{% endif %}>
+          <a href="{{ pathto(rellink[0]) }}" title="{{ rellink[1]|striptags }}"
+             accesskey="{{ rellink[2] }}">{{ rellink[3] }}</a>
+          {%- if not loop.first %}{{ reldelim2 }}{% endif %}</li>
+        {%- endfor %}
+        {%- block rootrellink %}
+        <li><a href="{{ pathto(master_doc) }}">{{ shorttitle|e }}</a>{{ reldelim1 }}</li>
+        {%- endblock %}
+        {%- for parent in parents %}
+          <li><a href="{{ parent.link|e }}" accesskey="U">{{ parent.title }}</a>{{ reldelim1 }}</li>
+        {%- endfor %}
+        {%- block relbaritems %} {% endblock %}
+      </ul>
+    </div>
+{%- endmacro %}
+{%- macro sidebar() %}
+  {%- block sidebartoc %}
+  {%- if display_toc %}
+    <h3><a href="{{ pathto(master_doc) }}">{{ _('Table Of Contents') }}</a></h3>
+    {{ toc }}
+  {%- endif %}
+  {%- endblock %}
+  {%- block sidebarrel %}
+  {%- if prev %}
+    <h4>{{ _('Previous topic') }}</h4>
+    <p class="topless"><a href="{{ prev.link|e }}"
+                          title="{{ _('previous chapter') }}">{{ prev.title }}</a></p>
+  {%- endif %}
+  {%- if next %}
+    <h4>{{ _('Next topic') }}</h4>
+    <p class="topless"><a href="{{ next.link|e }}"
+                          title="{{ _('next chapter') }}">{{ next.title }}</a></p>
+  {%- endif %}
+  {%- endblock %}
+  {%- block sidebarsourcelink %}
+  {%- if show_source and has_source and sourcename %}
+    <h3>{{ _('This Page') }}</h3>
+    <ul class="this-page-menu">
+      <li><a href="{{ pathto('_sources/' + sourcename, true)|e }}"
+             rel="nofollow">{{ _('Show Source') }}</a></li>
+    </ul>
+  {%- endif %}
+  {%- endblock %}
+  {%- block sidebarsearch %}
+  {%- if pagename != "search" %}
+  <div id="searchbox" style="display: none">
+    <h3>{{ _('Quick search') }}</h3>
+      <form class="search" action="{{ pathto('search') }}" method="get">
+        <input type="text" name="q" size="18" />
+        <input type="submit" value="{{ _('Go') }}" />
+        <input type="hidden" name="check_keywords" value="yes" />
+        <input type="hidden" name="area" value="default" />
+      </form>
+      <p style="font-size: 90%">{{ _('Enter search terms or a module, class or function name.') }}</p>
+  </div>
+  <script type="text/javascript">$('#searchbox').show(0);</script>
+  {%- endif %}
+  {%- endblock %}
+{%- endmacro %}
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+    {{ metatags }}
+    {%- if builder != 'htmlhelp' %}
+      {%- set titlesuffix = " &mdash; " + docstitle|e %}
+    {%- endif %}
+    <title>{{ title|striptags }}{{ titlesuffix }}</title>
+    {%- if builder == 'web' %}
+    <link rel="stylesheet" href="{{ pathto('index') }}?do=stylesheet{%
+      if in_admin_panel %}&admin=yes{% endif %}" type="text/css" />
+    {%- for link, type, title in page_links %}
+    <link rel="alternate" type="{{ type|e(true) }}" title="{{ title|e(true) }}" href="{{ link|e(true) }}" />
+    {%- endfor %}
+    {%- else %}
+    <link rel="stylesheet" href="{{ pathto('_static/style.css', 1) }}" type="text/css" />
+    <link rel="stylesheet" href="{{ pathto('_static/pygments.css', 1) }}" type="text/css" />
+    {%- endif %}
+    {%- if builder != 'htmlhelp' %}
+    <script type="text/javascript">
+      var DOCUMENTATION_OPTIONS = {
+          URL_ROOT:    '{{ pathto("", 1) }}',
+          VERSION:     '{{ release|e }}',
+          COLLAPSE_MODINDEX: false,
+          FILE_SUFFIX: '{{ file_suffix }}'
+      };
+    </script>
+    {%- for scriptfile in script_files %}
+    <script type="text/javascript" src="{{ pathto(scriptfile, 1) }}"></script>
+    {%- endfor %}
+    {%- if use_opensearch %}
+    <link rel="search" type="application/opensearchdescription+xml"
+          title="{% trans docstitle=docstitle|e %}Search within {{ docstitle }}{% endtrans %}"
+          href="{{ pathto('_static/opensearch.xml', 1) }}"/>
+    {%- endif %}
+    {%- if favicon %}
+    <link rel="shortcut icon" href="{{ pathto('_static/' + favicon, 1) }}"/>
+    {%- endif %}
+    {%- endif %}
+{%- block linktags %}
+    {%- if hasdoc('about') %}
+    <link rel="author" title="{{ _('About these documents') }}" href="{{ pathto('about') }}" />
+    {%- endif %}
+    <link rel="index" title="{{ _('Index') }}" href="{{ pathto('genindex') }}" />
+    <link rel="search" title="{{ _('Search') }}" href="{{ pathto('search') }}" />
+    {%- if hasdoc('copyright') %}
+    <link rel="copyright" title="{{ _('Copyright') }}" href="{{ pathto('copyright') }}" />
+    {%- endif %}
+    <link rel="top" title="{{ docstitle|e }}" href="{{ pathto('index') }}" />
+    {%- if parents %}
+    <link rel="up" title="{{ parents[-1].title|striptags }}" href="{{ parents[-1].link|e }}" />
+    {%- endif %}
+    {%- if next %}
+    <link rel="next" title="{{ next.title|striptags }}" href="{{ next.link|e }}" />
+    {%- endif %}
+    {%- if prev %}
+    <link rel="prev" title="{{ prev.title|striptags }}" href="{{ prev.link|e }}" />
+    {%- endif %}
+{%- endblock %}
+{%- block extrahead %} {% endblock %}
+  </head>
+  <body>
+    <div class="header"><h1>Roundup</h1>
+      {%- if pagename != "search" %}
+        <div id="searchbox" style="display: none">
+          <form class="search" action="{{ pathto('search') }}" method="get">
+            <input type="text" name="q" size="18" />
+            <input type="submit" value="{{ _('Search') }}" />
+            <input type="hidden" name="check_keywords" value="yes" />
+            <input type="hidden" name="area" value="default" />
+          </form>
+        </div>
+        <script type="text/javascript">$('#searchbox').show(0);</script>
+      {%- endif %}
+    </div>
+    <div class="navigation">
+      <div class="menu">
+       {{ sidebar() }}
+      </div>
+    </div>
+    <div class="content">
+       {{ relbar('related-top') }}
+       {{ body }}
+       {{ relbar('related-bottom') }}
+    </div>
+{%- block footer %}
+    <div class="footer">
+      {%- if hasdoc('copyright') %}
+        {% trans path=pathto('copyright'), copyright=copyright|e %}&copy; <a href="{{ path }}">Copyright</a> {{ copyright }}.{% endtrans %}
+      {%- else %}
+        {% trans copyright=copyright|e %}&copy; Copyright {{ copyright }}.{% endtrans %}
+      {%- endif %}
+      {%- if show_source and has_source and sourcename %}
+        <p class="source"><a href="{{ pathto('_sources/' + sourcename, true)|e }}" rel="nofollow">{{ _('source') }}</a></p>
+      {%- endif %}
+    </div>
+{%- endblock %}
+  </body>
+</html>

Added: tracker/roundup-src/doc/acknowledgements.txt
==============================================================================
--- (empty file)
+++ tracker/roundup-src/doc/acknowledgements.txt	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,132 @@
+Acknowledgements
+================
+
+Go Ping, you rock! Also, go Common Ground, ekit.com and Bizar Software for
+letting me implement this system on their time.
+
+Thanks also to the many people on the mailing list, in the sourceforge
+project and those who just report bugs:
+Thomas Arendsen Hein,
+Nerijus Baliunas,
+Anthony Baxter,
+Marlon van den Berg,
+Bo Berglund,
+Stéphane Bidoul,
+Cameron Blackwood,
+Jeff Blaine,
+Duncan Booth,
+Seb Brezel,
+J Alan Brogan,
+Titus Brown,
+Steve Byan,
+Brett Cannon,
+Godefroid Chapelle,
+Roch'e Compaan,
+Wil Cooley,
+Joe Cooper,
+Kelley Dagley,
+Bruno Damour,
+Bradley Dean,
+Toby Dickenson,
+Paul F. Dubois,
+Eric Earnst,
+Peter Eisentraut,
+Andrew Eland,
+Jeff Epler,
+Tom Epperly,
+Tamer Fahmy,
+Vickenty Fesunov,
+Hernan Martinez Foffani,
+Stuart D. Gathman,
+Martin Geisler,
+Ajit George,
+Frank Gibbons,
+Johannes Gijsbers,
+Gus Gollings,
+Philipp Gortan,
+Dan Grassi,
+Robin Green,
+Jason Grout,
+Charles Groves,
+Engelbert Gruber,
+Bruce Guenter,
+Tamás Gulácsi,
+Thomas Arendsen Hein,
+Juergen Hermann,
+Tobias Herp,
+Uwe Hoffmann,
+Alex Holkner,
+Tobias Hunger,
+Simon Hyde,
+Paul Jimenez,
+Christophe Kalt,
+Timo Kankare,
+Brian Kelley,
+James Kew,
+Sheila King,
+Michael Klatt,
+Bastian Kleineidam,
+Axel Kollmorgen,
+Cedric Krier,
+Detlef Lannert,
+Andrey Lebedev,
+Henrik Levkowetz,
+David Linke,
+Martin v. Löwis,
+Fredrik Lundh,
+Will Maier,
+Ksenia Marasanova,
+Georges Martin,
+Gordon McMillan,
+John F Meinel Jr,
+Roland Meister,
+Ulrik Mikaelsson,
+John Mitchell,
+Ramiro Morales,
+Toni Mueller,
+Stefan Niederhauser,
+Truls E. Næss,
+Patrick Ohly,
+Luke Opperman,
+Eddie Parker,
+Will Partain,
+Ewout Prangsma,
+Marcus Priesch,
+Bernhard Reiter,
+Roy Rapoport,
+John P. Rouillard,
+Luke Ross,
+Ollie Rutherfurd,
+Toby Sargeant,
+Giuseppe Scelsi,
+Ralf Schlatterbeck,
+Gregor Schmid,
+Florian Schulze,
+Klamer Schutte,
+Dougal Scott,
+Stefan Seefeld,
+Jouni K Seppänen,
+Jeffrey P Shell,
+Dan Shidlovsky,
+Joel Shprentz,
+Terrel Shumway,
+Emil Sit,
+Alexander Smishlajev,
+Nathaniel Smith,
+Leonardo Soto,
+Maciej Starzyk,
+Mitchell Surface,
+Anatoly T.,
+Jon C. Thomason
+Mike Thompson,
+Michael Twomey,
+Karl Ulbrich,
+Martin Uzak,
+Darryl VanDorp,
+J Vickroy,
+Timothy J. Warren,
+William (Wilk),
+Tue Wennerberg,
+Matt Wilbert,
+Chris Withers,
+Milan Zamazal.

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 15 22:43:30 2009
@@ -2,8 +2,6 @@
 Administration Guide
 ====================
 
-:Version: $Revision: 1.27 $
-
 .. contents::
 
 What does Roundup install?
@@ -207,7 +205,11 @@
 2. If you're using an RDBMS backend, make a backup of its contents now.
 3. Make a backup of the tracker home itself.
 4. Stop the tracker web and email frontends.
-5. Follow the steps in the `upgrading documentation`_ for the new version of
+5. Install the new version of the software::
+
+    python setup.py install
+
+6. 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
@@ -215,25 +217,7 @@
 
    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::
-
-    PYTHONPATH=. python roundup/scripts/roundup_server.py <normal arguments>
-    PYTHONPATH=. python roundup/scripts/roundup_admin.py <normal arguments>
-    PYTHONPATH=. python roundup/scripts/roundup_mailgw.py <normal arguments>
-
-   Note that on Windows, this would read::
-
-    C:\sources\roundup-0.7.4> SET PYTHONPATH=.
-    C:\sources\roundup-0.7.4> python roundup/scripts/roundup_server.py <normal arguments>
-
-7. Once you're comfortable that the upgrade will work using that copy, you
-   should install the new version of the software::
-
-    python setup.py install
-
-8. Restart your tracker web and email frontends.
+7. Restart your tracker web and email frontends.
 
 If something bad happens, you may reinstate your backup of the tracker and
 reinstall the older version of the sofware using the same install command::
@@ -384,10 +368,5 @@
    of your normal login
 
 
--------------------
-
-Back to `Table of Contents`_
-
-.. _`Table of Contents`: index.html
 .. _`customisation documentation`: customizing.html
 .. _`upgrading documentation`: upgrading.html

Modified: tracker/roundup-src/doc/announcement.txt
==============================================================================
--- tracker/roundup-src/doc/announcement.txt	(original)
+++ tracker/roundup-src/doc/announcement.txt	Sun Mar 15 22:43:30 2009
@@ -1,24 +1,28 @@
-I'm proud to release version 1.4.2 of Roundup.
+I'm proud to release version 1.4.7 of Roundup.
 
-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)
+1.4.7 is primarily a bugfix release which contains important security
+fixes:
+
+- a number of security issues were discovered by Daniel Diniz
+- EditCSV and ExportCSV altered to include permission checks
+- HTTP POST required on actions which alter data
+- HTML file uploads served as application/octet-stream
+- Handle Unauthorised in file serving correctly
+- New item action reject creation of new users
+- Item retirement was not being controlled
+- Roundup is now compatible with Python 2.6
+- Improved French and German translations
+- Improve consistency of item sorting in HTML interface
+- Various other small bug fixes, robustification and optimisation
+
+Though some new features made it in also:
+
+- Provide a "no selection" option in web interface selection widgets
+- Debug logging now uses the logging module rather than print
+- Allow CGI frontend to serve XMLRPC requests.
+- Added XMLRPC actions, as well as bridging CGI actions to XMLRPC actions.
+- Optimized large file serving via mod_python / sendfile().
+- Support resuming downloads for (large) files.
 
 If you're upgrading from an older version of Roundup you *must* follow
 the "Software Upgrade" guidelines given in the maintenance documentation.

Added: tracker/roundup-src/doc/contact.txt
==============================================================================
--- (empty file)
+++ tracker/roundup-src/doc/contact.txt	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,22 @@
+Contact
+=======
+
+For general support enquiries about usage, a mailing list is available:
+
+    roundup-users at sourceforge.net
+
+If you've got a great idea for roundup, or have found a bug, please
+submit an issue to the tracker at: 
+
+    http://sourceforge.net/tracker/?group_id=31577
+
+For discussions about developing or enhancing roundup:
+
+    roundup-devel at sourceforge.net
+
+The admin for this project is Richard Jones:
+
+    richard at users.sourceforge.net
+
+but he should only be contacted directly when none of the
+above avenues of contact are suitable.

Modified: tracker/roundup-src/doc/customizing.txt
==============================================================================
--- tracker/roundup-src/doc/customizing.txt	(original)
+++ tracker/roundup-src/doc/customizing.txt	Sun Mar 15 22:43:30 2009
@@ -1,9 +1,9 @@
+:tocdepth: 2
+
 ===================
 Customising Roundup
 ===================
 
-: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
 
@@ -181,6 +181,12 @@
   LC_MESSAGES, or LANG, in that order of preference.
 
 Section **web**
+ allow_html_file -- ``no``
+  Setting this option enables Roundup to serve uploaded HTML
+  file content *as HTML*. This is a potential security risk
+  and is therefore disabled by default. Set to 'yes' if you
+  trust *all* users uploading content to your tracker.
+
  http_auth -- ``yes``
   Whether to use HTTP Basic Authentication, if present.
   Roundup will use either the REMOTE_USER or HTTP_AUTHORIZATION
@@ -1875,10 +1881,15 @@
    - the current index information (``filterspec``, ``filter`` args,
      ``properties``, etc) parsed out of the form. 
    - methods for easy filterspec link generation
-   - *user*, the current user item as an HTMLItem instance
-   - *form*
+   - "form"
      The current CGI form information as a mapping of form argument name
-     to value
+     to value (specifically a cgi.FieldStorage)
+   - "env" the CGI environment variables
+   - "base" the base URL for this instance
+   - "user" a HTMLItem instance for the current user
+   - "language" as determined by the browser or config
+   - "classname" the current classname (possibly None)
+   - "template" the current template (suffix, also possibly None)
 **config**
   This variable holds all the values defined in the tracker config.ini
   file (eg. TRACKER_NAME, etc.)
@@ -4850,11 +4861,4 @@
 rather than requiring a web server restart.
 
 
--------------------
-
-Back to `Table of Contents`_
-
-.. _`Table of Contents`: index.html
 .. _`design documentation`: design.html
-.. _`admin guide`: admin_guide.html
-

Modified: tracker/roundup-src/doc/default.css
==============================================================================
--- tracker/roundup-src/doc/default.css	(original)
+++ tracker/roundup-src/doc/default.css	Sun Mar 15 22:43:30 2009
@@ -1,7 +1,7 @@
 /*
 :Author: David Goodger
 :Contact: goodger at users.sourceforge.net
-:date: $Date: 2004/06/09 00:25:32 $
+:date: $Date: 2004-06-09 00:25:32 $
 :version: $Revision: 1.13 $
 :copyright: This stylesheet has been placed in the public domain.
 

Modified: tracker/roundup-src/doc/design.txt
==============================================================================
--- tracker/roundup-src/doc/design.txt	(original)
+++ tracker/roundup-src/doc/design.txt	Sun Mar 15 22:43:30 2009
@@ -475,6 +475,8 @@
             match the 'filter' spec, sorted by the group spec and then the
             sort spec.
 
+            "search_matches" is a container type
+
             "filterspec" is {propname: value(s)}
 
             "sort" and "group" are [(dir, prop), ...] where dir is '+', '-'
@@ -482,8 +484,6 @@
             backward-compatibility reasons a single (dir, prop) tuple is
             also allowed.
 
-            "search_matches" is {nodeid: marker}
-
             The filter must match all properties specificed. If the property
             value to match is a list:
 
@@ -599,7 +599,7 @@
     4
     >>> db.issue.create(title="abuse", status=1)
     5
-    >>> hyperdb.Class(db, "user", username=hyperdb.Key(),
+    >>> hyperdb.Class(db, "user", username=hyperdb.String(),
     ... password=hyperdb.String())
     <hyperdb.Class "user">
     >>> db.issue.addprop(fixer=hyperdb.Link("user"))
@@ -1652,10 +1652,5 @@
 - Access Controls
 - Added "actor" property
 
-------------------
-
-Back to `Table of Contents`_
-
-.. _`Table of Contents`: index.html
 .. _customisation: customizing.html
 

Modified: tracker/roundup-src/doc/developers.txt
==============================================================================
--- tracker/roundup-src/doc/developers.txt	(original)
+++ tracker/roundup-src/doc/developers.txt	Sun Mar 15 22:43:30 2009
@@ -2,8 +2,6 @@
 Developing Roundup
 ==================
 
-:Version: $Revision: 1.16 $
-
 .. note::
    The intended audience of this document is the developers of the core
    Roundup code. If you just wish to alter some behaviour of your Roundup
@@ -429,11 +427,6 @@
 At run time, Roundup automatically compiles message catalogs whenever
 `PO`_ file is changed.
 
------------------
-
-Back to `Table of Contents`_
-
-.. _`Table of Contents`: index.html
 .. _`Customising Roundup`: customizing.html
 .. _`Roundup's Design Document`: spec.html
 .. _`implementation notes`: implementation.html

Modified: tracker/roundup-src/doc/index.txt
==============================================================================
--- tracker/roundup-src/doc/index.txt	(original)
+++ tracker/roundup-src/doc/index.txt	Sun Mar 15 22:43:30 2009
@@ -5,185 +5,20 @@
 Contents
 ========
 
-- Features__
-- Installation__ and Upgrading__ existing installs
-- `Frequently Asked Questions`__
-- `User Guide`__
-- `Configuring and Customising Roundup`__
-- `Administering Roundup Trackers`__
-- `Roundup's Design`__ (original__)
-- `Developing Roundup`__
-- `Roundup Tracker Templates`__
-- Contact_
-- Acknowledgements_
-- License_
-
-__ features.html
-__ installation.html
-__ upgrading.html
-__ FAQ.html
-__ user_guide.html
-__ customizing.html
-__ admin_guide.html
-__ design.html
-__ spec.html
-__ developers.html
-__ tracker_templates.html
-
-Contact
-=======
-
-For general support enquiries about usage, a mailing list is available:
-
-    roundup-users at sourceforge.net
-
-If you've got a great idea for roundup, or have found a bug, please
-submit an issue to the tracker at: 
-
-    http://sourceforge.net/tracker/?group_id=31577
-
-For discussions about developing or enhancing roundup:
-
-    roundup-devel at sourceforge.net
-
-The admin for this project is Richard Jones:
-
-    richard at users.sourceforge.net
-
-but he should only be contacted directly when none of the
-above avenues of contact are suitable.
-
-
-Acknowledgements
-================
-
-Go Ping, you rock! Also, go Common Ground, ekit.com and Bizar Software for
-letting me implement this system on their time.
-
-Thanks also to the many people on the mailing list, in the sourceforge
-project and those who just report bugs:
-Thomas Arendsen Hein,
-Nerijus Baliunas,
-Anthony Baxter,
-Marlon van den Berg,
-Bo Berglund,
-Stéphane Bidoul,
-Cameron Blackwood,
-Jeff Blaine,
-Duncan Booth,
-Seb Brezel,
-J Alan Brogan,
-Titus Brown,
-Steve Byan,
-Brett Cannon,
-Godefroid Chapelle,
-Roch'e Compaan,
-Wil Cooley,
-Joe Cooper,
-Kelley Dagley,
-Bruno Damour,
-Toby Dickenson,
-Paul F. Dubois,
-Eric Earnst,
-Andrew Eland,
-Jeff Epler,
-Tom Epperly,
-Tamer Fahmy,
-Vickenty Fesunov,
-Hernan Martinez Foffani,
-Stuart D. Gathman,
-Ajit George,
-Frank Gibbons,
-Johannes Gijsbers,
-Gus Gollings,
-Philipp Gortan,
-Dan Grassi,
-Robin Green,
-Jason Grout,
-Charles Groves,
-Engelbert Gruber,
-Bruce Guenter,
-Tamás Gulácsi,
-Thomas Arendsen Hein,
-Juergen Hermann,
-Tobias Herp,
-Uwe Hoffmann,
-Alex Holkner,
-Tobias Hunger,
-Simon Hyde,
-Paul Jimenez,
-Christophe Kalt,
-Timo Kankare,
-Brian Kelley,
-James Kew,
-Sheila King,
-Michael Klatt,
-Bastian Kleineidam,
-Axel Kollmorgen,
-Detlef Lannert,
-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,
-Truls E. Næss,
-Patrick Ohly,
-Luke Opperman,
-Eddie Parker,
-Will Partain,
-Ewout Prangsma,
-Marcus Priesch,
-Bernhard Reiter,
-Roy Rapoport,
-John P. Rouillard,
-Luke Ross,
-Ollie Rutherfurd,
-Toby Sargeant,
-Giuseppe Scelsi,
-Ralf Schlatterbeck,
-Gregor Schmid,
-Florian Schulze,
-Klamer Schutte,
-Dougal Scott,
-Stefan Seefeld,
-Jouni K Seppänen,
-Jeffrey P Shell,
-Dan Shidlovsky,
-Joel Shprentz,
-Terrel Shumway,
-Emil Sit,
-Alexander Smishlajev,
-Nathaniel Smith,
-Leonardo Soto,
-Maciej Starzyk,
-Mitchell Surface,
-Jon C. Thomason
-Mike Thompson,
-Michael Twomey,
-Karl Ulbrich,
-Martin Uzak,
-Darryl VanDorp,
-J Vickroy,
-Timothy J. Warren,
-William (Wilk),
-Tue Wennerberg,
-Matt Wilbert,
-Chris Withers,
-Milan Zamazal.
-
-
-
-License
-=======
-
-See COPYING.txt in the software distribution for the licensing terms.
+.. toctree::
+   :maxdepth: 2
 
+   features
+   installation
+   upgrading
+   FAQ
+   user_guide
+   customizing
+   admin_guide
+   spec
+   original design <design>
+   developers
+   tracker_templates
+   contact
+   acknowledgements
+   license

Modified: tracker/roundup-src/doc/installation.txt
==============================================================================
--- tracker/roundup-src/doc/installation.txt	(original)
+++ tracker/roundup-src/doc/installation.txt	Sun Mar 15 22:43:30 2009
@@ -2,8 +2,6 @@
 Installing Roundup
 ==================
 
-:Version: $Revision: 1.130 $
-
 .. contents::
    :depth: 2
 
@@ -1010,12 +1008,6 @@
 Roundup -- from installation and scripts.
 
 
--------------------------------------------------------------------------------
-
-Back to `Table of Contents`_
-
-Next: `User Guide`_
-
 .. _`table of contents`: index.html
 .. _`user guide`: user_guide.html
 .. _`roundup specification`: spec.html

Added: tracker/roundup-src/doc/license.txt
==============================================================================
--- (empty file)
+++ tracker/roundup-src/doc/license.txt	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,5 @@
+License
+=======
+
+See COPYING.txt in the software distribution for the licensing terms.
+

Deleted: tracker/roundup-src/doc/roundup-admin.1
==============================================================================
--- tracker/roundup-src/doc/roundup-admin.1	Sun Mar 15 22:43:30 2009
+++ (empty file)
@@ -1,26 +0,0 @@
-.TH ROUNDUP-ADMIN 1 "24 January 2003"
-.SH NAME
-roundup-admin \- administrate roundup trackers
-.SH SYNOPSIS
-\fBroundup-admin\fP [\fIoptions\fP] \fI<command>\fP \fI<arguments>\fP
-.SH OPTIONS
-.TP
-\fB-i\fP \fIinstance home\fP
-specify the issue tracker "home directory" to administer
-.TP
-\fB-u\fP
-the user[:password] to use for commands
-.TP
-\fB-c\fP
-when outputting lists of data, just comma-separate them
-.SH FURTHER HELP
- roundup-admin -h
- roundup-admin help                       -- this help
- roundup-admin help <command>             -- command-specific help
- roundup-admin help all                   -- all available help
-.SH AUTHOR
-This manpage was written by Bastian Kleineidam
-<calvin at debian.org> for the Debian distribution of roundup.
-
-The main author of roundup is Richard Jones
-<richard at users.sourceforge.net>.

Deleted: tracker/roundup-src/doc/roundup-demo.1
==============================================================================
--- tracker/roundup-src/doc/roundup-demo.1	Sun Mar 15 22:43:30 2009
+++ (empty file)
@@ -1,30 +0,0 @@
-.TH ROUNDUP-SERVER 1 "27 July 2004"
-.SH NAME
-roundup-demo \- create a roundup "demo" tracker and launch its web interface
-.SH SYNOPSIS
-\fBroundup-demo\fP [\fIbackend\fP [\fBnuke\fP]]
-.SH OPTIONS
-.TP
-\fBnuke\fP
-Create a fresh demo tracker (deleting the existing one if any). If the
-additional \fIbackend\fP argument is specified, the new demo tracker will
-use the backend named (one of "anydbm", "sqlite", "metakit", "mysql" or
-"postgresql"; subject to availability on your system).
-.TH DESCRIPTION
-This command creates a fresh demo tracker for you to experiment with. The
-email features of Roundup will be turned off (so the nosy feature won't
-send email). It does this by removing the \fInosyreaction.py\fP module
-from the demo tracker's \fIdetectors\fP directory.
-
-If you wish, you may modify the demo tracker by editing its configuration
-files and HTML templates. See the \fIcustomisation\fP manual for
-information about how to do that.
-
-Once you've fiddled with the demo tracker, you may use it as a template for
-creating your real, live tracker. Simply run the \fIroundup-admin\fP
-command to install the tracker from inside the demo tracker home directory,
-and it will be listed as an available template for installation. No data
-will be copied over.
-.SH AUTHOR
-This manpage was written by Richard Jones
-<richard at users.sourceforge.net>.

Deleted: tracker/roundup-src/doc/roundup-mailgw.1
==============================================================================
--- tracker/roundup-src/doc/roundup-mailgw.1	Sun Mar 15 22:43:30 2009
+++ (empty file)
@@ -1,59 +0,0 @@
-.TH ROUNDUP-MAILGW 1 "24 January 2003"
-.SH NAME
-roundup-mailgw \- mail gateway for roundup
-.SH SYNOPSIS
-\fBroundup-mailgw\fP \fI<instance home>\fP [\fImethod\fP]
-.SH OPTIONS
-.TP
-\fB-C\fP \fIhyperdb class\fP
-specify a tracker class - one of msg (the default), issue, file, user - to
-manipulate with -S options
-.TP
-\fB-S\fP \fIproperty=value[;property=value] pairs\fP
-specify the values to set on the class specified by -C using the same
-format as the Subject line property manipulations
-.SH DESCRIPTION
-The roundup mail gateway may be called in one of three ways:
-.IP \(bu
-with an instance home as the only argument,
-.IP \(bu
-with both an instance home and a mail spool file, or
-.IP \(bu
-with both an instance home and a pop server account.
-.PP
-\fBPIPE\fP
-.br
-In the first case, the mail gateway reads a single message from the
-standard input and submits the message to the roundup.mailgw module.
-
-\fBUNIX mailbox\fP
-.br
-In the second case, the gateway reads all messages from the mail spool
-file and submits each in turn to the roundup.mailgw module. The file is
-emptied once all messages have been successfully handled. The file is
-specified as:
- \fImailbox /path/to/mailbox\fP
-
-\fBPOP\fP
-.br
-In the third case, the gateway reads all messages from the POP server
-specified and submits each in turn to the roundup.mailgw module. The
-server is specified as:
- \fIpop username:password at server\fP
-.br
-The username and password may be omitted:
- \fIpop username at server\fP
- \fIpop server\fP
-.br
-are both valid. The username and/or password will be prompted for if
-not supplied on the command-line.
-
-\fBAPOP\fP
-Same as POP, but using Authenticated POP:
- \fIapop username:password at server\fP
-.SH AUTHOR
-This manpage was written by Bastian Kleineidam
-<calvin at debian.org> for the Debian distribution of roundup.
-
-The main author of roundup is Richard Jones
-<richard at users.sourceforge.net>.

Deleted: tracker/roundup-src/doc/roundup-server.1
==============================================================================
--- tracker/roundup-src/doc/roundup-server.1	Sun Mar 15 22:43:30 2009
+++ (empty file)
@@ -1,61 +0,0 @@
-.TH ROUNDUP-SERVER 1 "27 July 2004"
-.SH NAME
-roundup-server \- start roundup web server
-.SH SYNOPSIS
-\fBroundup-server\fP [\fIoptions\fP] [\fBname=\fP\fItracker home\fP]*
-.SH OPTIONS
-.TP
-\fB-C\fP \fIfile\fP
-Use options read from the configuration file (see below).
-.TP
-\fB-n\fP \fIhostname\fP
-Sets the host name.
-.TP
-\fB-p\fP \fIport\fP
-Sets the port to listen on.
-.TP
-\fB-d\fP \fIfile\fP
-Daemonize, and write the server's PID to the nominated file.
-.TP
-\fB-l\fP \fIfile\fP
-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 \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
-This manpage was written by Bastian Kleineidam
-<calvin at debian.org> for the Debian distribution of roundup.
-
-The main author of roundup is Richard Jones
-<richard at users.sourceforge.net>.

Modified: tracker/roundup-src/doc/upgrading.txt
==============================================================================
--- tracker/roundup-src/doc/upgrading.txt	(original)
+++ tracker/roundup-src/doc/upgrading.txt	Sun Mar 15 22:43:30 2009
@@ -13,6 +13,99 @@
 
 .. contents::
 
+
+Migrating from 1.4.x to 1.4.7
+=============================
+
+Several security issues were addressed in this release. Some aspects of your
+trackers may no longer function depending on your local customisations. Core
+functionality that will need to be modified:
+
+Grant the "retire" permission to users for their queries
+--------------------------------------------------------
+
+Users will no longer be able to retire their own queries. To remedy this you
+will need to add the following to your tracker's ``schema.py`` just under the
+line that grants them permission to edit their own queries::
+
+   p = db.security.addPermission(name='Edit', klass='query', check=edit_query,
+      description="User is allowed to edit their queries")
+   db.security.addPermissionToRole('User', p)
+ + p = db.security.addPermission(name='Retire', klass='query', check=edit_query,
+ +    description="User is allowed to retire their queries")
+ + db.security.addPermissionToRole('User', p)
+   p = db.security.addPermission(name='Create', klass='query',
+      description="User is allowed to create queries")
+   db.security.addPermissionToRole('User', p)
+
+The lines marked "+" should be added, minus the "+" sign.
+
+
+Fix the "retire" link in the users list for admin users
+-------------------------------------------------------
+
+The "retire" link found in the file ``html/users.index.html``::
+
+  <td tal:condition="context/is_edit_ok">
+   <a tal:attributes="href string:user${user/id}?@action=retire&@template=index"
+    i18n:translate="">retire</a>
+
+Should be replaced with::
+
+  <td tal:condition="context/is_retire_ok">
+     <form style="padding:0"
+           tal:attributes="action string:user${user/id}">
+      <input type="hidden" name="@template" value="index">
+      <input type="hidden" name="@action" value="retire">
+      <input type="submit" value="retire" i18n:attributes="value">
+     </form>
+
+
+Fix for Python 2.6+ users
+-------------------------
+
+If you use Python 2.6 you should edit your tracker's
+``detectors/nosyreaction.py`` file to change::
+
+   import sets
+
+at the top to::
+
+   from roundup.anypy.sets_ import set
+
+and then all instances of ``sets.Set()`` to ``set()`` in the later code.
+
+
+
+Trackers currently allowing HTML file uploading
+-----------------------------------------------
+
+Trackers which wish to continue to allow uploading of HTML content against issues
+will need to set a new configuration variable in the ``[web]`` section of the
+tracker's ``config.ini`` file:
+
+   # Setting this option enables Roundup to serve uploaded HTML
+   # file content *as HTML*. This is a potential security risk
+   # and is therefore disabled by default. Set to 'yes' if you
+   # trust *all* users uploading content to your tracker.
+   # Allowed values: yes, no
+   # Default: no
+   allow_html_file = no
+
+
+
+Migrating from 1.4.2 to 1.4.3
+=============================
+
+If you are using the MySQL backend you will need to replace some indexes
+that may have been created by version 1.4.2.
+
+You should to access your MySQL database directly and remove any indexes
+with a name ending in "_key_retired_idx". You should then re-add them with
+the same spec except the key column name needs a size. So an index on
+"_user (__retired, _name)" should become "_user (__retired, _name(255))".
+
+
 Migrating from 1.4.x to 1.4.2
 =============================
 

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 15 22:43:30 2009
@@ -2,8 +2,6 @@
 User Guide
 ==========
 
-:Version: $Revision: 1.37 $
-
 .. contents::
 
 .. hint::
@@ -479,8 +477,13 @@
 
 If the sender of an e-mail is unknown to Roundup (looking up both user
 primary e-mail addresses and their alternate addresses) then a new user
-will be created. The new user will have their username set to the "user"
-part of "user at domain" in their e-mail address. Their password will be
+may be created, depending on tracker configuration (see the `Admin
+Guide`_ section "Users and Security" for configuration details.)
+
+.. _`Admin Guide`: admin_guide.html
+
+The new user will have their username set to the "user" part of
+"user at domain" in their e-mail address. Their password will be
 completely randomised, and they'll have to visit the web interface to
 have it changed. Some sites don't allow web access by users who register
 via e-mail like this.
@@ -799,9 +802,3 @@
 Remember the roundup commands that accept multiple designators accept
 them ',' separated so using '-dc' is almost always required.
 
------------------
-
-Back to `Table of Contents`_
-
-.. _`Table of Contents`: index.html
-.. _`customisation documentation`: customizing.html

Added: tracker/roundup-src/doc/xmlrpc.txt
==============================================================================
--- (empty file)
+++ tracker/roundup-src/doc/xmlrpc.txt	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,88 @@
+=========================
+XML-RPC access to Roundup
+=========================
+
+.. contents::
+
+Introduction
+------------
+Version 1.4 of Roundup includes an XML-RPC frontend. Some installations find
+that roundup-admins requirement of local access to the tracker instance
+limiting. The XML-RPC frontend provides the ability to execute a limited subset
+of commands similar to those found in roundup-admin from remote machines. 
+
+roundup-xmlrpc-server
+---------------------
+The Roundup XML-RPC server must be started before remote clients can access the
+tracker via XML-RPC. ``roundup-xmlrpc-server`` is installed in the scripts
+directory alongside ``roundup-server`` and roundup-admin``. When invoked, the
+location of the tracker instance must be specified.
+
+	roundup-xmlrpc-server -i ``/path/to/tracker``
+
+The default port is ``8000``. An alternative port can be specified with the
+``--port`` switch.
+
+security consideration
+======================
+Note that the current ``roundup-xmlrpc-server`` implementation does not
+support SSL. This means that usernames and passwords will be passed in
+cleartext unless the server is being proxied behind another server (such as
+Apache or lighttpd) that provide SSL.
+
+client API
+----------
+The server currently implements four methods. Each method requires that the
+user provide a username and password in the HTTP authorization header in order
+to authenticate the request against the tracker.
+
+======= ====================================================================
+Command Description
+======= ====================================================================
+list    arguments: *classname, [property_name]*
+
+        List all elements of a given ``classname``. If ``property_name`` is
+        specified, that is the property that will be displayed for each
+        element. If ``property_name`` is not specified the default label
+        property will be used.
+
+display arguments: *designator, [property_1, ..., property_N]*
+
+        Display a single item in the tracker as specified by ``designator``
+        (e.g. issue20 or user5). The default is to display all properties
+        for the item. Alternatively, a list of properties to display can be
+        specified.
+
+create  arguments: *classname, arg_1 ... arg_N*
+
+        Create a new instance of ``classname`` with ``arg_1`` through
+        ``arg_N`` as the values of the new instance. The arguments are
+        name=value pairs (e.g. ``status='3'``).
+
+set     arguments: *designator, arg_1 ... arg_N*
+
+        Set the values of an existing item in the tracker as specified by
+        ``designator``. The new values are specified in ``arg_1`` through
+        ``arg_N``. The arguments are name=value pairs (e.g. ``status='3'``).
+======= ====================================================================
+
+sample python client
+====================
+::
+
+        >>> import xmlrpclib
+        >>> roundup_server = xmlrpclib.ServerProxy('http://username:password@localhost:8000')
+        >>> roundup_server.list('user')
+        ['admin', 'anonymous', 'demo']
+        >>> roundup_server.list('issue', 'id')
+        ['1']
+        >>> roundup_server.display('issue1')
+        {'assignedto' : None, 'files' : [], 'title' = 'yes, ..... }
+        >>> roundup_server.display('issue1', 'priority', 'status')
+        {'priority' : '1', 'status' : '2'}
+        >>> roundup_server.set('issue1', 'status=3')
+        >>> roundup_server.display('issue1', 'status')
+        {'status' : '3' }
+        >>> roundup_server.create('issue', "title='another bug'", "status=2")
+        '2'
+

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 15 22:43:30 2009
@@ -14,7 +14,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: ZRoundup.py,v 1.23 2008/02/07 01:03:39 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
 

Modified: tracker/roundup-src/frontends/ZRoundup/__init__.py
==============================================================================
--- tracker/roundup-src/frontends/ZRoundup/__init__.py	(original)
+++ tracker/roundup-src/frontends/ZRoundup/__init__.py	Sun Mar 15 22:43:30 2009
@@ -14,7 +14,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: __init__.py,v 1.5 2006/08/11 00:04:29 richard Exp $
+# $Id: __init__.py,v 1.5 2006-08-11 00:04:29 richard Exp $
 #
 __version__='1.1'
 

Modified: tracker/roundup-src/frontends/roundup.cgi
==============================================================================
--- tracker/roundup-src/frontends/roundup.cgi	(original)
+++ tracker/roundup-src/frontends/roundup.cgi	Sun Mar 15 22:43:30 2009
@@ -16,7 +16,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: roundup.cgi,v 1.2 2006/12/11 23:36:15 richard Exp $
+# $Id: roundup.cgi,v 1.2 2006-12-11 23:36:15 richard Exp $
 
 # python version check
 from roundup import version_check

Modified: tracker/roundup-src/locale/de.po
==============================================================================
--- tracker/roundup-src/locale/de.po	(original)
+++ tracker/roundup-src/locale/de.po	Sun Mar 15 22:43:30 2009
@@ -1,40 +1,54 @@
 # German message file for Roundup Issue Tracker
-# Tobias Herp <tobias.herp at gmx.de>, 2006
 # Initial work by Stefan Niederhauser <stefan.niederhauser at unibas.ch>, 2004.
 # updated by Toni Mueller <toni at debian.org>
+# updated by Tobias Herp <tobias.herp at gmx.de>
 #
-# $Id: de.po,v 1.5 2006/12/18 03:51:44 richard Exp $
-#
-# roundup.pot revision 1.8
+# $Id: de.po,v 1.9 2008-08-19 01:51:10 richard Exp $
 #
 msgid ""
 msgstr ""
-"Project-Id-Version: Roundup 1.2.1\n"
+"Project-Id-Version: Roundup 1.4.6\n"
 "Report-Msgid-Bugs-To: roundup-devel at lists.sourceforge.net\n"
-"POT-Creation-Date: 2004-12-08 10:25+0200\n"
-"PO-Revision-Date: 2006-10-07 22:45Westeuropäische Normalzeit\n"
-"Last-Translator: Toni Mueller <toni at debian.org>\n"
+"POT-Creation-Date: 2009-03-12 11:58+0200\n"
+"PO-Revision-Date: 2009-03-12 18:05Westeuropäische Normalzeit\n"
+"Last-Translator: Tobias Herp <tobias.herp at gmx.de>\n"
 "Language-Team: German Translators <roundup-devel at lists.sourceforge.net>\n"
 "MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=ISO-8859-1\n"
+"Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Poedit-Bookmarks: 75,-1,-1,-1,-1,-1,-1,-1,-1,-1\n"
+
+#: ../roundup/actions.py:53
+#: ../roundup/cgi/actions.py:120
+msgid "You may not retire the admin or anonymous user"
+msgstr "Sie können den Administrator oder den Gast-Benutzer nicht löschen"
+
+#: ../roundup/actions.py:66
+#: ../roundup/cgi/actions.py:57
+#, python-format
+msgid "You do not have permission to %(action)s the %(classname)s class."
+msgstr "Sie sind nicht berechtigt, die Aktion(en) %(action)s auf die Klasse %(classname)s anzuwenden."
 
-# ../roundup/admin.py:83 :949 :998 :1020
-#: ../roundup/admin.py:84 ../roundup/admin.py:954 ../roundup/admin.py:1003
-#: ../roundup/admin.py:1025
+#: ../roundup/admin.py:83
+#: ../roundup/admin.py:986
+#: ../roundup/admin.py:1037
+#: ../roundup/admin.py:1060
+#: ../roundup/admin.py:83:986
+#: :1037:1060
 #, python-format
 msgid "no such class \"%(classname)s\""
 msgstr "Die Klasse \"%(classname)s\" existiert nicht"
 
 # ../roundup/admin.py:93 :97
-#: ../roundup/admin.py:94 ../roundup/admin.py:98
+#: ../roundup/admin.py:93
+#: ../roundup/admin.py:97
+#: ../roundup/admin.py:93:97
 #, python-format
 msgid "argument \"%(arg)s\" not propname=value"
-msgstr ""
-"Der Parameter \"%(arg)s\" entspricht nicht dem Format Eigenschaft=Wert"
+msgstr "Der Parameter \"%(arg)s\" entspricht nicht dem Format Eigenschaft=Wert"
 
-#: ../roundup/admin.py:111
+#: ../roundup/admin.py:110
 #, python-format
 msgid ""
 "Problem: %(message)s\n"
@@ -43,14 +57,13 @@
 "Problem: %(message)s\n"
 "\n"
 
-#: ../roundup/admin.py:113
+#: ../roundup/admin.py:111
 #, 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"
@@ -72,38 +85,38 @@
 "%(message)sVerwendung: roundup-admin [Optionen] [<Befehl> <Parameter>]\n"
 "\n"
 "Optionen:\n"
-" -i <Instanzverzeichnis> -- Tracker-Instanz zur Administration auswählen\n"
-" -u                -- Benutzer[:Paßwort] für das Ausführen von Befehlen\n"
+" -i <Instanzverzeichnis> -- Tracker-Instanz zur Administration auswählen\n"
+" -u                -- Benutzer[:Passwort] für das Ausführen von Befehlen\n"
 " -d                -- lange Bezeichner statt Klassen-Ids anzeigen\n"
 " -c                -- Komma-getrennte Listenausgabe (CSV).\n"
 "                      Analog zu '-S \",\"'.\n"
 " -S <Zeichenkette> -- Trennzeichen bei der Listenausgabe.\n"
 " -s                -- Leerzeichen als Trennzeichen verwenden.\n"
 "                      Analog zu '-S \" \"'.\n"
-" -V                -- ausführliche Ausgaben (\"verbose\") beim Import\n"
+" -V                -- ausführliche Ausgaben (\"verbose\") beim Import\n"
 " -v                -- Roundup- und Python-Version ausgeben (und beenden)\n"
 "\n"
-" Nur eine der Optionen -s, -c or -S kann gewählt werden.\n"
+" Nur eine der Optionen -s, -c or -S kann gewählt werden.\n"
 "\n"
 "Hilfe:\n"
 " roundup-admin -h\n"
 " roundup-admin help                       -- diese Kurzhilfe anzeigen\n"
 " roundup-admin help <Befehl>              -- Hilfe zu einem Befehl anzeigen\n"
-" roundup-admin help all                   -- sämtliche Hilfen anzeigen\n"
+" roundup-admin help all                   -- sämtliche Hilfen anzeigen\n"
 
-#: ../roundup/admin.py:137
+#: ../roundup/admin.py:138
 msgid "Commands:"
 msgstr "Befehle:"
 
-#: ../roundup/admin.py:144
+#: ../roundup/admin.py:145
 msgid ""
 "Commands may be abbreviated as long as the abbreviation\n"
 "matches only one command, e.g. l == li == lis == list."
 msgstr ""
-"Befehle können abgekürzt werden, solange sie eindeutig bleiben, \n"
+"Befehle können abgekürzt werden, solange sie eindeutig bleiben, \n"
 "z.B. l == li == lis == list."
 
-#: ../roundup/admin.py:174
+#: ../roundup/admin.py:175
 msgid ""
 "\n"
 "All commands (except help) require a tracker specifier. This is just\n"
@@ -138,8 +151,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"
@@ -169,30 +182,30 @@
 "Command help:\n"
 msgstr ""
 "\n"
-"Sie müssen für sämtliche Befehle - außer für die Hilfe - das Verzeichnis\n"
+"Sie müssen für sämtliche Befehle - außer für die Hilfe - das Verzeichnis\n"
 "einer Tracker-Instanz angeben. Dort wird die Konfiguration gespeichert und\n"
-" - je nach Datenbank - auch die Daten. Das Tracker-Verzeichnis kann über\n"
+" - je nach Datenbank - auch die Daten. Das Tracker-Verzeichnis kann über\n"
 "die Umgebungsvariable TRACKER_HOME oder die Option \"-i Verzeichnis\"\n"
 "angegeben werden.\n"
 "\n"
 "Ein Bezeichner besteht aus einem Klassennamen und einer ID, zum Beispiel\n"
 "\"issue12\"\n"
 "\n"
-"Eigenschaften werden als Zeichenketten übergeben und angezeigt.\n"
+"Eigenschaften werden als Zeichenketten übergeben und angezeigt.\n"
 " . Eine Zeichenkette (\"String\") wird direkt ausgegeben.\n"
-" . Datumswerte werden als vollständiges Datum in der lokalen Zeitzone\n"
-"   ausgegeben und können im vollständigen Format oder in einem Teilformat\n"
+" . Datumswerte werden als vollständiges Datum in der lokalen Zeitzone\n"
+"   ausgegeben und können im vollständigen Format oder in einem Teilformat\n"
 "   eingeben werden (siehe unten).\n"
-" . Links zu anderen Einträgen werden mit dem Bezeichner dargestellt.\n"
-"   Bei der Eingabe wird entweder der Bezeichner oder nur der Schlüssel\n"
+" . Links zu anderen Einträgen werden mit dem Bezeichner dargestellt.\n"
+"   Bei der Eingabe wird entweder der Bezeichner oder nur der Schlüssel\n"
 "   angegeben.\n"
 " . Bei Mehrfach-Links werden die verlinkten Bezeichner mit Kommata getrennt\n"
-"   ausgegeben. Bei der Eingabe können Bezeichner oder Schlüssel\n"
+"   ausgegeben. Bei der Eingabe können Bezeichner oder Schlüssel\n"
 "   mit Kommata getrennt eingegeben werden.\n"
 "\n"
-"Falls Eigenschaften Leerzeichen enthalten, müssen die Werte in\n"
-"\"Anführungszeichen\" eingeschlossen werden. Leerzeichen können auch mit\n"
-"einem \\Backslash geschützt werden. Ebenso müssen Anführungszeichen im Wert\n"
+"Falls Eigenschaften Leerzeichen enthalten, müssen die Werte in\n"
+"\"Anführungszeichen\" eingeschlossen werden. Leerzeichen können auch mit\n"
+"einem \\Backslash geschützt werden. Ebenso müssen Anführungszeichen im Wert\n"
 "mit einem Backslash versehen werden, einfache ' wie doppelte \".\n"
 "Beispiele:\n"
 "           Hallo Welt          (2 Werte: Hallo, Welt)\n"
@@ -201,22 +214,23 @@
 "           Alfons\\' Welt      (2 Werte: Alfons', Welt)\n"
 "           Adresse=\"1 2 3\"   (1 Wert: Address=1 2 3)\n"
 "           \\\\                (1 Wert: \\)\n"
-"           \\n\\r\\t           (1 Wert: Zeilenumbruch + CR + Tab)\n"
+"           \\n"
+"\\r\\t           (1 Wert: Zeilenumbruch + CR + Tab)\n"
 "\n"
-"Wenn bei einer Abfrage oder einer Änderung mehrere Einträge angegeben\n"
-"werden, so werden die gewünschten Eigenschaften aller Einträge angezeigt\n"
-"bzw. geändert.\n"
+"Wenn bei einer Abfrage oder einer Änderung mehrere Einträge angegeben\n"
+"werden, so werden die gewünschten Eigenschaften aller Einträge angezeigt\n"
+"bzw. geändert.\n"
 "\n"
-"Wenn ein Befehl \"get\" oder \"find\" mehrere Einträge zurückgibt, so \n"
-"werden diese Zeile für Zeile oder (mit der Option -c) kommagetrennt\n"
+"Wenn ein Befehl \"get\" oder \"find\" mehrere Einträge zurückgibt, so \n"
+"werden diese Zeile für Zeile oder (mit der Option -c) kommagetrennt\n"
 "ausgegeben.\n"
 "\n"
-"Bei Änderungen wird ein Benutzername und ein Paßwort benötigt.\n"
-"Diese Angaben können in der Umgebungsvariable ROUNDUP_LOGIN oder mit der\n"
+"Bei Änderungen wird ein Benutzername und ein Passwort benötigt.\n"
+"Diese Angaben können in der Umgebungsvariable ROUNDUP_LOGIN oder mit der\n"
 "Option -u gemacht werden, entweder als \"Benutzername\" oder als\n"
-"\"benutzername:paßwort\".\n"
+"\"benutzername:passwort\".\n"
 "\n"
-"Beispiele für Datumsformate:\n"
+"Beispiele für Datumsformate:\n"
 "  \"2000-04-17.03:45\" ergibt <Date 2000-04-17.08:45:00>\n"
 "  \"2000-04-17\" ergibt <Date 2000-04-17.00:00:00>\n"
 "  \"01-25\" ergibt <Date yyyy-01-25.00:00:00>\n"
@@ -228,12 +242,12 @@
 "\n"
 "Befehlshilfe:\n"
 
-#: ../roundup/admin.py:237
+#: ../roundup/admin.py:238
 #, python-format
 msgid "%s:"
 msgstr "%s:"
 
-#: ../roundup/admin.py:242
+#: ../roundup/admin.py:243
 msgid ""
 "Usage: help topic\n"
 "        Give help about topic.\n"
@@ -245,41 +259,45 @@
 "        "
 msgstr ""
 "Verwendung: help Thema\n"
-"        Zeigt die Hilfe für ein Thema ein.\n"
+"        Zeigt die Hilfe für ein Thema ein.\n"
 "\n"
 "        commands  -- Befehle auflisten\n"
 "        <command> -- Hilfe zu einem bestimmten Befehl\n"
 "        initopts  -- Optionen zur Initialisierung\n"
-"        all       -- sämtlichen Hilfetext anzeigen\n"
+"        all       -- sämtlichen Hilfetext anzeigen\n"
 "        "
 
-#: ../roundup/admin.py:265
+#: ../roundup/admin.py:266
 #, python-format
 msgid "Sorry, no help for \"%(topic)s\""
 msgstr "Zum Thema \"%(topic)s\" existiert leider kein Hilfetext"
 
 # ../roundup/admin.py:336 :382
-#: ../roundup/admin.py:337 ../roundup/admin.py:386
+#: ../roundup/admin.py:343
+#: ../roundup/admin.py:399
+#: ../roundup/admin.py:343:399
 msgid "Templates:"
 msgstr "Vorlagen:"
 
 # ../roundup/admin.py:339 :393
-#: ../roundup/admin.py:340 ../roundup/admin.py:397
+#: ../roundup/admin.py:346
+#: ../roundup/admin.py:410
+#: ../roundup/admin.py:346:410
 msgid "Back ends:"
 msgstr "Datenbanken:"
 
-#: ../roundup/admin.py:343
+#: ../roundup/admin.py:349
 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"
@@ -293,26 +311,26 @@
 "        See also initopts help.\n"
 "        "
 msgstr ""
-"Verwendung: install [Vorlage [Datenbanktyp [Administratorpasswort]]]\n"
+"Verwendung: install [Vorlage [Datenbanktyp [Admin-Paßwort [opt=wert[,...]]]]]\n"
 "        Installiert einen neuen Roundup-Tracker.\n"
 "\n"
-"        Sie werden aufgefordert, ein Tracker-Verzeichnis zu wählen\n"
+"        Sie werden aufgefordert, ein Tracker-Verzeichnis zu wählen\n"
 "        (falls Sie keines mit TRACKER_HOME oder -i angegeben haben),\n"
 "        sowie eine Vorlage, den Datenbanktyp und das Administrations-\n"
-"        paßwort anzugeben.\n"
-"        Sie können auch die Vorlage, den Datenbanktyp und das Paßwort\n"
+"        passwort anzugeben.\n"
+"        Sie können auch die Vorlage, den Datenbanktyp und das Passwort\n"
 "        in dieser Reihenfolge auf der Kommandozeile angegen.\n"
 "\n"
 "        Das letzte Argument erlaubt die Angabe von Konfigurations-Optionen.\n"
 "        So wird zum Beispiel durch Angabe von\n"
 "           \"web_http_auth=no,rdbms_user=dinsdale\"\n"
 "        die Option http_auth in der Sektion [web] ausgeschaltet und der\n"
-"        Name des Datenbank-Benutzers in der Sektion [rdbms] geändert.\n"
-"        Vorsicht bitte mit Leerzeichen! Wenn sie Leerzeichen angeben müssen,\n"
-"        schließen Sie das ganze Argument in Gänsefüßchen ein.\n"
+"        Name des Datenbank-Benutzers in der Sektion [rdbms] geändert.\n"
+"        Vorsicht bitte mit Leerzeichen! Wenn sie Leerzeichen angeben müssen,\n"
+"        schließen Sie das ganze Argument in Gänsefüßchen ein.\n"
 "\n"
-"        Nach der Installation müssen Sie die Datenbank mit dem Befehl \n"
-"        \"initialise\" einrichten. Zuvor können Sie in der Datei\n"
+"        Nach der Installation müssen Sie die Datenbank mit dem Befehl \n"
+"        \"initialise\" einrichten. Zuvor können Sie in der Datei\n"
 "        \"dbinit.py\" die Funktion \"init()\" einen Anfangsbestand an\n"
 "        Daten programmieren.\n"
 "\n"
@@ -321,21 +339,40 @@
 
 # ../roundup/admin.py:358 :483 :562 :612 :682 :703 :731 :802 :869 :940 :988
 # :1010 :1037 :1098 :1156
-#: ../roundup/admin.py:359 ../roundup/admin.py:441 ../roundup/admin.py:502
-#: ../roundup/admin.py:581 ../roundup/admin.py:631 ../roundup/admin.py:687
-#: ../roundup/admin.py:708 ../roundup/admin.py:736 ../roundup/admin.py:807
-#: ../roundup/admin.py:874 ../roundup/admin.py:945 ../roundup/admin.py:993
-#: ../roundup/admin.py:1015 ../roundup/admin.py:1042 ../roundup/admin.py:1104
-#: ../roundup/admin.py:1170
+#: ../roundup/admin.py:372
+#: ../roundup/admin.py:469
+#: ../roundup/admin.py:530
+#: ../roundup/admin.py:609
+#: ../roundup/admin.py:660
+#: ../roundup/admin.py:718
+#: ../roundup/admin.py:739
+#: ../roundup/admin.py:767
+#: ../roundup/admin.py:839
+#: ../roundup/admin.py:906
+#: ../roundup/admin.py:977
+#: ../roundup/admin.py:1027
+#: ../roundup/admin.py:1050
+#: ../roundup/admin.py:1081
+#: ../roundup/admin.py:1177
+#: ../roundup/admin.py:1250
+#: ../roundup/admin.py:372:469
+#: :1027:1050
+#: :1081:1177
+#: :1250
+#: :530:609
+#: :660:718
+#: :739:767
+#: :839:906
+#: :977
 msgid "Not enough arguments supplied"
-msgstr "Zu wenig Parameter übergeben"
+msgstr "Zu wenig Parameter übergeben"
 
-#: ../roundup/admin.py:365
+#: ../roundup/admin.py:378
 #, python-format
 msgid "Instance home parent directory \"%(parent)s\" does not exist"
 msgstr "Das angegebene Tracker-Verzeichnis \"%(parent)s\" existiert nicht"
 
-#: ../roundup/admin.py:373
+#: ../roundup/admin.py:386
 #, python-format
 msgid ""
 "WARNING: There appears to be a tracker in \"%(tracker_home)s\"!\n"
@@ -343,27 +380,27 @@
 "Erase it? Y/N: "
 msgstr ""
 "WARNUNG: Im Verzeichnis \"%(tracker_home)s\" scheint bereits ein Tracker\n"
-"installiert zu sein! Eine erneute Installation löscht sämtliche Daten!\n"
-"Wirklich löschen? Y/N: "
+"installiert zu sein! Eine erneute Installation löscht sämtliche Daten!\n"
+"Wirklich löschen? Y/N: "
 
-#: ../roundup/admin.py:388
+#: ../roundup/admin.py:401
 msgid "Select template [classic]: "
-msgstr "Template auswählen [classic]:"
+msgstr "Template auswählen [classic]:"
 
-#: ../roundup/admin.py:399
+#: ../roundup/admin.py:412
 msgid "Select backend [anydbm]: "
-msgstr "Datenbank auswählen [anydbm]"
+msgstr "Datenbank auswählen [anydbm]"
 
-#: ../roundup/admin.py:419
+#: ../roundup/admin.py:422
 #, python-format
 msgid "Error in configuration settings: \"%s\""
-msgstr ""
-"Fehler in der Konfiguration: \"%s\""
+msgstr "Fehler in der Konfiguration: \"%s\""
 
-#: ../roundup/admin.py:408
-#, python-format
+#: ../roundup/admin.py:431
+#, fuzzy, python-format
 msgid ""
 "\n"
+"---------------------------------------------------------------------------\n"
 " You should now edit the tracker configuration file:\n"
 "   %(config_file)s"
 msgstr ""
@@ -371,11 +408,11 @@
 " Sie sollten nun die Konfigurationsdatei des Trackers bearbeiten:\n"
 "   %(config_file)s"
 
-#: ../roundup/admin.py:417
+#: ../roundup/admin.py:441
 msgid " ... at a minimum, you must set following options:"
 msgstr " ... passen sie zumindest folgende Optionen an:"
 
-#: ../roundup/admin.py:422
+#: ../roundup/admin.py:446
 #, python-format
 msgid ""
 "\n"
@@ -393,14 +430,14 @@
 "\n"
 " Um das Datenbank-Schema anzupassen, bearbeiten Sie die Datei:\n"
 "   %(database_config_file)s\n"
-" Sie können zudem auch den anfänglichen Datenbestand ändern:\n"
+" Sie können zudem auch den anfänglichen Datenbestand ändern:\n"
 "   %(database_init_file)s\n"
-" ... die Online-Dokumentation enthält ein eigenes Kapitel über Anpassungen.\n"
+" ... die Online-Dokumentation enthält ein eigenes Kapitel über Anpassungen.\n"
 "\n"
-" Anschließend MÜSSEN Sie \"roundup-admin initialise\" ausführen.\n"
+" Anschließend MÜSSEN Sie \"roundup-admin initialise\" ausführen.\n"
 "---------------------------------------------------------------------------\n"
 
-#: ../roundup/admin.py:436
+#: ../roundup/admin.py:464
 msgid ""
 "Usage: genconfig <filename>\n"
 "        Generate a new tracker config file (ini style) with default values\n"
@@ -413,7 +450,7 @@
 "        "
 
 #. password
-#: ../roundup/admin.py:446
+#: ../roundup/admin.py:474
 msgid ""
 "Usage: initialise [adminpw]\n"
 "        Initialise a new Roundup tracker.\n"
@@ -423,7 +460,7 @@
 "        Execute the tracker's initialisation function dbinit.init()\n"
 "        "
 msgstr ""
-"Verwendung: initialise [Administrationspasswort]\n"
+"Verwendung: initialise [Admin-Paßwort]\n"
 "        Initialisieren eines neuen Roundup-Trackers.\n"
 "\n"
 "        Der Administrator-Benutzer wird eingerichtet.\n"
@@ -431,33 +468,33 @@
 "        Die Funktion dbinit.init() wird aufgerufen\n"
 "        "
 
-#: ../roundup/admin.py:460
+#: ../roundup/admin.py:488
 msgid "Admin Password: "
-msgstr "Administrator-Paßwort: "
+msgstr "Administratorpasswort: "
 
-#: ../roundup/admin.py:461
+#: ../roundup/admin.py:489
 msgid "       Confirm: "
 msgstr "  Wiederholen: "
 
-#: ../roundup/admin.py:465
+#: ../roundup/admin.py:493
 msgid "Instance home does not exist"
 msgstr "Tracker-Verzeichnis existiert nicht"
 
-#: ../roundup/admin.py:469
+#: ../roundup/admin.py:497
 msgid "Instance has not been installed"
 msgstr "Tracker-Instanz wurde nicht installiert"
 
-#: ../roundup/admin.py:474
+#: ../roundup/admin.py:502
 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 ""
 "WARNUNG: Die Datenbank ist schon initialisiert!\n"
-"Eine erneute Initialisierung löscht sämtliche Daten!\n"
-"Wirklich löschen? Y/N: "
+"Eine erneute Initialisierung löscht sämtliche Daten!\n"
+"Wirklich löschen? Y/N: "
 
-#: ../roundup/admin.py:495
+#: ../roundup/admin.py:523
 msgid ""
 "Usage: get property designator[,designator]*\n"
 "        Get the given property of one or more designator(s).\n"
@@ -467,36 +504,37 @@
 "        "
 msgstr ""
 "Verwendung: get Eigenschaft Bezeichner[,Bezeichner]*\n"
-"        Gibt die Eigenschaft eines oder mehrerer Einträge zurück.\n"
+"        Gibt die Eigenschaft eines oder mehrerer Einträge zurück.\n"
 "\n"
 "        Diese Funktion zeigt Ihnen die Werte einer bestimmten\n"
-"        Eigenschaft der gewünschten Einträge an.\n"
+"        Eigenschaft der gewünschten Einträge an.\n"
 "        "
 
 # ../roundup/admin.py:516 :531
-#: ../roundup/admin.py:535 ../roundup/admin.py:550
+#: ../roundup/admin.py:563
+#: ../roundup/admin.py:578
+#: ../roundup/admin.py:563:578
 #, python-format
 msgid "property %s is not of type Multilink or Link so -d flag does not apply."
-msgstr ""
-"Die Eigenschaft %s ist kein Multilink oder Link; die Option -d wird "
-"deshalb hier nicht ausgewertet."
+msgstr "Die Eigenschaft %s ist kein Multilink oder Link; die Option -d wird deshalb hier nicht ausgewertet."
 
 # ../roundup/admin.py:539 :951 :1000 :1022
-#: ../roundup/admin.py:558 ../roundup/admin.py:956 ../roundup/admin.py:1005
-#: ../roundup/admin.py:1027
+#: ../roundup/admin.py:586
+#: ../roundup/admin.py:988
+#: ../roundup/admin.py:1039
+#: ../roundup/admin.py:1062
+#: ../roundup/admin.py:586:988
+#: :1039:1062
 #, python-format
 msgid "no such %(classname)s node \"%(nodeid)s\""
-msgstr ""
-"Es existiert kein Eintrag der Klasse %(classname)s mit der ID \"%(nodeid)s\""
+msgstr "Es existiert kein Eintrag der Klasse %(classname)s mit der ID \"%(nodeid)s\""
 
-#: ../roundup/admin.py:560
+#: ../roundup/admin.py:588
 #, python-format
 msgid "no such %(classname)s property \"%(propname)s\""
-msgstr ""
-"Die Eigenschaft \"%(propname)s\" ist für die Klasse \"%"
-"(classname)s\" nicht definiert"
+msgstr "Die Eigenschaft \"%(propname)s\" ist für die Klasse \"%(classname)s\" nicht definiert"
 
-#: ../roundup/admin.py:569
+#: ../roundup/admin.py:597
 msgid ""
 "Usage: set items property=value property=value ...\n"
 "        Set the given properties of one or more items(s).\n"
@@ -505,26 +543,25 @@
 "        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 ""
-"Verwendung: set Einträge Eigenschaft=Wert Eigenschaft=Wert ...\n"
-"        Bearbeitet den Eigenschaftswert eines oder mehrerer Einträge.\n"
+"Verwendung: set Einträge Eigenschaft=Wert Eigenschaft=Wert ...\n"
+"        Bearbeitet den Eigenschaftswert eines oder mehrerer Einträge.\n"
 "\n"
-"        Für \"Einträge\" können Sie eine Klasse angeben oder eine Liste\n"
-"        von einem oder mehreren mit Kommata getrennten Bezeichnern aufführen\n"
+"        Für \"Einträge\" können Sie eine Klasse angeben oder eine Liste\n"
+"        von einem oder mehreren mit Kommata getrennten Bezeichnern aufführen\n"
 "        (\"Bezeichner[,Bezeichner]*\").\n"
 "\n"
-"        Der Wert der Eigenschaft wird für alle angegebenen Einträge gesetzt.\n"
-"        Wenn der Wert fehlt (Eigenschaft=), wird die Eigenschaft gelöscht.\n"
+"        Der Wert der Eigenschaft wird für alle angegebenen Einträge gesetzt.\n"
+"        Wenn der Wert fehlt (Eigenschaft=), wird die Eigenschaft gelöscht.\n"
 "        Wenn die Eigenschaft ein Link/Multilink ist, werden die verlinkten\n"
-"        Einträge als mit Kommata getrennte ID-Nummern angegeben (\"1,2,3\").\n"
+"        Einträge als mit Kommata getrennte ID-Nummern angegeben (\"1,2,3\").\n"
 "        "
 
-#: ../roundup/admin.py:623
+#: ../roundup/admin.py:652
 msgid ""
 "Usage: find classname propname=value ...\n"
 "        Find the nodes of the given class with a given link property value.\n"
@@ -535,21 +572,25 @@
 "        "
 msgstr ""
 "Verwendung: find Klassenname Eigenschaft=Wert ...\n"
-"        Findet Einträge, welche die angegebene Verlinkung aufweisen.\n"
+"        Findet Einträge, welche die angegebene Verlinkung aufweisen.\n"
 "\n"
-"        Findet sämtliche Einträge einer Klasse, bei welchen die Link-\n"
-"        Eigenschaft den angegebenen Wert enthält. Der Wert kann entweder\n"
+"        Findet sämtliche Einträge einer Klasse, bei welchen die Link-\n"
+"        Eigenschaft den angegebenen Wert enthält. Der Wert kann entweder\n"
 "        als ID oder als Bezeichner (\"msg23\") spezifiziert werden.\n"
 "        "
 
 # ../roundup/admin.py:631 :669 :822 :834 :888
-#: ../roundup/admin.py:674 ../roundup/admin.py:827 ../roundup/admin.py:839
-#: ../roundup/admin.py:893
+#: ../roundup/admin.py:705
+#: ../roundup/admin.py:859
+#: ../roundup/admin.py:871
+#: ../roundup/admin.py:925
+#: ../roundup/admin.py:705:859
+#: :871:925
 #, python-format
 msgid "%(classname)s has no property \"%(propname)s\""
 msgstr "Die Klasse \"%(classname)s\" hat keine Eigenschaft \"%(propname)s\""
 
-#: ../roundup/admin.py:681
+#: ../roundup/admin.py:712
 msgid ""
 "Usage: specification classname\n"
 "        Show the properties for a classname.\n"
@@ -560,20 +601,22 @@
 "Verwendung: specification Klassenname\n"
 "        Gibt die Attribute der Klasse aus.\n"
 "\n"
-"        Zeigt sämtliche Eigenschaften der Klasse auf.\n"
+"        Zeigt sämtliche Eigenschaften der Klasse auf.\n"
 "        "
 
-#: ../roundup/admin.py:696
+#: ../roundup/admin.py:727
 #, python-format
 msgid "%(key)s: %(value)s (key property)"
-msgstr "%(key)s: %(value)s (Schlüsseleigenschaft)"
+msgstr "%(key)s: %(value)s (Schlüsseleigenschaft)"
 
-#: ../roundup/admin.py:698
+#: ../roundup/admin.py:729
+#: ../roundup/admin.py:756
+#: ../roundup/admin.py:729:756
 #, python-format
 msgid "%(key)s: %(value)s"
 msgstr "%(key)s: %(value)s"
 
-#: ../roundup/admin.py:701
+#: ../roundup/admin.py:732
 msgid ""
 "Usage: display designator[,designator]*\n"
 "        Show the property values for the given node(s).\n"
@@ -585,23 +628,17 @@
 "Verwendung: display Bezeichner[,Bezeichner]*\n"
 "        Zeigt alle Eigenschaften eines oder mehrerer Eintrage an.\n"
 "\n"
-"        Der Befehl zeigt die Eigenschaften und Ihre Werte des\n"
-"        gewählten Eintrags an.\n"
+"        Der Befehl zeigt die Eigenschaften und ihre Werte des\n"
+"        gewählten Eintrags an.\n"
 "        "
 
-#: ../roundup/admin.py:725
-#, python-format
-msgid "%(key)s: %(value)r"
-msgstr "%(key)s: %(value)r"
-
-#: ../roundup/admin.py:728
+#: ../roundup/admin.py:759
 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 ""
@@ -612,31 +649,31 @@
 "        werden mit den Werten initialisiert\n"
 "        "
 
-#: ../roundup/admin.py:755
+#: ../roundup/admin.py:786
 #, python-format
 msgid "%(propname)s (Password): "
-msgstr "%(propname)s (Paßwort):"
+msgstr "%(propname)s (Passwort):"
 
-#: ../roundup/admin.py:757
+#: ../roundup/admin.py:788
 #, python-format
 msgid "   %(propname)s (Again): "
 msgstr "   %(propname)s (Wiederholen):"
 
-#: ../roundup/admin.py:759
+#: ../roundup/admin.py:790
 msgid "Sorry, try again..."
 msgstr "Bitte erneut versuchen..."
 
-#: ../roundup/admin.py:763
+#: ../roundup/admin.py:794
 #, python-format
 msgid "%(propname)s (%(proptype)s): "
 msgstr "%(propname)s (%(proptype)s): "
 
-#: ../roundup/admin.py:781
+#: ../roundup/admin.py:812
 #, python-format
 msgid "you must provide the \"%(propname)s\" property."
-msgstr "Sie müssen einen Wert für \"%(propname)s\" angeben."
+msgstr "Sie müssen einen Wert für \"%(propname)s\" angeben."
 
-#: ../roundup/admin.py:792
+#: ../roundup/admin.py:824
 msgid ""
 "Usage: list classname [property]\n"
 "        List the instances of a class.\n"
@@ -652,31 +689,30 @@
 "        "
 msgstr ""
 "Usage: list Klassenname [Eigenschaft]\n"
-"        Listet sämtliche Einträge einer Klasse auf.\n"
+"        Listet sämtliche Einträge einer Klasse auf.\n"
 "\n"
-"        Es werden sämtliche Einträge der Klasse ausgegeben. Wird keine\n"
+"        Es werden sämtliche Einträge der Klasse ausgegeben. Wird keine\n"
 "        Eigenschaft angegeben, so wird ein Bezeichner aus der folgenden\n"
-"        Liste generiert, mit absteigender Priorität:\n"
-"        Schlüsselfeld, ein Feld namens \"name\" oder \"title\". Falls\n"
+"        Liste generiert, mit absteigender Priorität:\n"
+"        Schlüsselfeld, ein Feld namens \"name\" oder \"title\". Falls\n"
 "        auch diese Felder nicht existieren, wird das \n"
 "        erste Feld alphabetisch sortiert angezeigt.\n"
 "\n"
 "        Mit den Optionen -c, -S or -s wird eine Liste von IDs ausgegeben,\n"
 "        falls keine Eigenschaft angegeben wird. Sonst werden die Werte\n"
-"        dieser Eigenschaften sämtlicher Instanzen dieser Klasse "
-"aufgelistet.\n"
+"        dieser Eigenschaften sämtlicher Instanzen dieser Klasse aufgelistet.\n"
 "        "
 
-#: ../roundup/admin.py:805
+#: ../roundup/admin.py:837
 msgid "Too many arguments supplied"
-msgstr "Sie haben zuviele Argumente übergeben"
+msgstr "Sie haben zuviele Argumente übergeben"
 
-#: ../roundup/admin.py:841
+#: ../roundup/admin.py:873
 #, python-format
 msgid "%(nodeid)4s: %(value)s"
 msgstr "%(nodeid)4s: %(value)s"
 
-#: ../roundup/admin.py:845
+#: ../roundup/admin.py:877
 msgid ""
 "Usage: table classname [property[,property]*]\n"
 "        List the instances of a class in tabular form.\n"
@@ -708,12 +744,12 @@
 "        "
 msgstr ""
 "Verwendung: table Klassenname [Eigenschaft[,Eigenschaft]*]\n"
-"        Listet die Einträge einer Klasse in tabellarischer Form.\n"
+"        Listet die Einträge einer Klasse in tabellarischer Form.\n"
 "\n"
-"        Dieser Befehl gibt eine Liste sämtlicher Instanzen einer Klasse aus.\n"
+"        Dieser Befehl gibt eine Liste sämtlicher Instanzen einer Klasse aus.\n"
 "        Werden die Eigenschaften nicht explizit angegeben, so werden\n"
 "        alle angezeigt. Die Spaltenbreite wird automatisch nach dem \n"
-"        grössten Wert jeder Spalte berechnet, oder sie kann explizit\n"
+"        grössten Wert jeder Spalte berechnet, oder sie kann explizit\n"
 "        als \"Eigenschaft:Breite\" angegeben werden.\n"
 "        Beispiel:\n"
 "\n"
@@ -724,7 +760,7 @@
 "          3  usability\n"
 "          4  feature\n"
 "\n"
-"        Um die Spaltenbreite auf die Grösse des Spaltentitels zu bechränken,\n"
+"        Um die Spaltenbreite auf die Grösse des Spaltentitels zu bechränken,\n"
 "        lassen Sie die Breitenangabe hinter dem Doppelpunkt weg.\n"
 "        Beispiel:\n"
 "\n"
@@ -737,18 +773,17 @@
 "\n"
 "        "
 
-#: ../roundup/admin.py:889
+#: ../roundup/admin.py:921
 #, python-format
 msgid "\"%(spec)s\" not name:width"
 msgstr "\"%(spec)s\" entspricht nicht dem Format Eigenschaft:Breite"
 
-#: ../roundup/admin.py:939
+#: ../roundup/admin.py:971
 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 ""
 "Verwendung: history Bezeichner\n"
@@ -758,7 +793,7 @@
 "        Bezeichner auf.\n"
 "        "
 
-#: ../roundup/admin.py:960
+#: ../roundup/admin.py:992
 msgid ""
 "Usage: commit\n"
 "        Commit changes made to the database during an interactive session.\n"
@@ -772,17 +807,17 @@
 "        "
 msgstr ""
 "Verwendung: commit\n"
-"        Speichern der Datenbank-Änderungen.\n"
+"        Speichern der Datenbank-Änderungen.\n"
 "\n"
-"        Falls die Datenbank Transaktionen unterstützt, werden Änderungen\n"
-"        während einer Bearbeitungs-Session erst nach einem \"commit\" an die\n"
-"        Datenbank übermittelt.\n"
+"        Falls die Datenbank Transaktionen unterstützt, werden Änderungen\n"
+"        während einer Bearbeitungs-Session erst nach einem \"commit\" an die\n"
+"        Datenbank übermittelt.\n"
 "\n"
-"        Einzelbefehle über die Kommandozeile werden sofort in die Datenbank\n"
+"        Einzelbefehle über die Kommandozeile werden sofort in die Datenbank\n"
 "        geschrieben.\n"
 "        "
 
-#: ../roundup/admin.py:974
+#: ../roundup/admin.py:1007
 msgid ""
 "Usage: rollback\n"
 "        Undo all changes that are pending commit to the database.\n"
@@ -794,14 +829,14 @@
 "        "
 msgstr ""
 "Verwendung: rollback\n"
-"        Sämtliche nicht gespeicherte Änderungen werden verworfen.\n"
+"        Sämtliche nicht gespeicherte Änderungen werden verworfen.\n"
 "\n"
-"        Falls die Datenbank Transaktionen unterstützt, werden dadurch\n"
-"        sämtliche noch nicht gespeicherte Änderungen (siehe \"commit\")\n"
+"        Falls die Datenbank Transaktionen unterstützt, werden dadurch\n"
+"        sämtliche noch nicht gespeicherte Änderungen (siehe \"commit\")\n"
 "        verworfen.\n"
 "        "
 
-#: ../roundup/admin.py:986
+#: ../roundup/admin.py:1020
 msgid ""
 "Usage: retire designator[,designator]*\n"
 "        Retire the node specified by designator.\n"
@@ -811,14 +846,14 @@
 "        "
 msgstr ""
 "Verwendung: retire Bezeichner[,Bezeichner]*\n"
-"        Verbirgt einen oder mehrere Einträge.\n"
+"        Verbirgt einen oder mehrere Einträge.\n"
 "\n"
-"        Das Verbergen eines Eintrags bewirkt, daß dieser bei einer Suche\n"
-"        nicht mehr angezeigt wird. Der Schlüssel des verborgenen Eintrags\n"
+"        Das Verbergen eines Eintrags bewirkt, dass dieser bei einer Suche\n"
+"        nicht mehr angezeigt wird. Der Schlüssel des verborgenen Eintrags\n"
 "        kann zudem wiederverwendet werden.\n"
 "        "
 
-#: ../roundup/admin.py:1009
+#: ../roundup/admin.py:1044
 msgid ""
 "Usage: restore designator[,designator]*\n"
 "        Restore the retired node specified by designator.\n"
@@ -827,19 +862,22 @@
 "        "
 msgstr ""
 "Verwendung: restore Bezeichner[,Bezeichner]*\n"
-"        Ein oder mehrere verborgene Einträge werden wiederhergestellt.\n"
+"        Ein oder mehrere verborgene Einträge werden wiederhergestellt.\n"
 "\n"
 "        Ein verborgener Eintrag wird wiederhergestellt und ist danach\n"
-"        für die Benutzer wieder sichtbar.\n"
+"        für die Benutzer wieder sichtbar.\n"
 "        "
 
 #. grab the directory to export to
-#: ../roundup/admin.py:1031
+#: ../roundup/admin.py:1067
 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"
@@ -848,14 +886,44 @@
 msgstr ""
 "Verwendung: export [Klasse[,Klasse]] Exportverzeichnis\n"
 "        Exportiert die Datenbank in ein Verzeichnis mit CSV-Dateien.\n"
+"        Um die im Dateisystem abgelegten Daten fortzulassen (z. B.\n"
+"        die Klassen msg und file), verwenden Sie \"exporttables\".\n"
 "\n"
-"        Optional kann der Export auf gewisse Klassen beschränkt werden.\n"
+"        Wenn Sie Klassennamen übergeben, wird der Export auf diese beschränkt\n"
+"        bzw. (wenn der ersten Klasse ein '-' vorgestellt wird) diese fortgelassen.\n"
 "\n"
 "        Die Daten werden als kommagetrennte Dateien in das angegebene\n"
 "        Exportverzeichnis geschrieben.\n"
 "        "
 
-#: ../roundup/admin.py:1084
+#: ../roundup/admin.py:1142
+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 ""
+"Verwendung: exporttables [Klasse[,Klasse]] Exportverzeichnis\n"
+"        Exportiert die Datenbank in ein Verzeichnis mit CSV-Dateien,\n"
+"        unter Fortlassung der im Dateisystem unter $TRACKER_HOME/db/files\n"
+"        abgelegten Daten; um diese mitzuexportieren, verwenden Sie \"export\".\n"
+"\n"
+"        Wenn Sie Klassennamen übergeben, wird der Export auf diese beschränkt\n"
+"        bzw. (wenn der ersten Klasse ein '-' vorgestellt wird) diese fortgelassen.\n"
+"\n"
+"        Die Daten werden als kommagetrennte Dateien in das angegebene\n"
+"        Exportverzeichnis geschrieben.\n"
+"        "
+
+#: ../roundup/admin.py:1157
 msgid ""
 "Usage: import import_dir\n"
 "        Import a database from the directory containing CSV files,\n"
@@ -878,36 +946,35 @@
 "        "
 msgstr ""
 "Verwendung: import Importverzeichnis\n"
-"        Importiert Datensätze aus einem Verzeichnis mit CSV-Dateien\n"
+"        Importiert Datensätze aus einem Verzeichnis mit CSV-Dateien\n"
 "\n"
 "        Folgende Dateien werden beim Import verwendet:\n"
 "\n"
 "        <Klasse>.csv\n"
-"          In dieser Datei sind die Daten zu den Einträgen einer Klasse.\n"
-"          Für sämtliche Eigenschaften der Klasse muß eine Spalte \n"
+"          In dieser Datei sind die Daten zu den Einträgen einer Klasse.\n"
+"          Für sämtliche Eigenschaften der Klasse muss eine Spalte \n"
 "          exisitieren. In der ersten Zeile stehen die Eigenschaftsnamen.\n"
 "        <Klasse>-journals.csv\n"
-"          In dieser Datei wird der Bearbeitungs-Verlauf der Einträge\n"
+"          In dieser Datei wird der Bearbeitungs-Verlauf der Einträge\n"
 "          beschrieben.\n"
 "\n"
-"        Importierte Einträge übernehmen die IDs, welche in den Dateien\n"
-"        definiert sind. Existierende Einträge mit denselben IDs werden\n"
-"        überschrieben.\n"
-"        Die Einträge werden in die existierende Datenbank geschrieben.\n"
-"        Falls eine neue, leere Datenbank verwendet werden soll, so müssen\n"
-"        Sie diese zuerst erstellen (oder sämtliche bestehenden Inhalte \n"
+"        Importierte Einträge übernehmen die IDs, welche in den Dateien\n"
+"        definiert sind. Existierende Einträge mit denselben IDs werden\n"
+"        überschrieben.\n"
+"        Die Einträge werden in die existierende Datenbank geschrieben.\n"
+"        Falls eine neue, leere Datenbank verwendet werden soll, so müssen\n"
+"        Sie diese zuerst erstellen (oder sämtliche bestehenden Inhalte \n"
 "        verbergen).\n"
 "        "
 
-#: ../roundup/admin.py:1152
+#: ../roundup/admin.py:1232
 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"
@@ -923,26 +990,25 @@
 "Verwendung: pack Periode | Datum\n"
 "        Entfernt den Bearbeitungsverlauf ab einem gewissen Datum.\n"
 "\n"
-"        Das Datum kann als rückläufige Periode spezifiziert werden:\n"
-"           \"y\", \"m\", and \"d\".         wobei \"w\" (Woche) für 7 Tage "
-"steht.\n"
+"        Das Datum kann als rückläufige Periode spezifiziert werden:\n"
+"           \"y\", \"m\", and \"d\".         wobei \"w\" (Woche) für 7 Tage steht.\n"
 "\n"
 "        Beispiele:\n"
-"              \"3y\" steht für 3 Jahre\n"
-"              \"2y 1m\" steht für 2 Jahre und ein Monat\n"
-"              \"1m 25d\" steht für 1 Monat und 25 Tage\n"
-"              \"2w 3d\" steht für 2 Wochen und 3 Tage\n"
+"              \"3y\" steht für 3 Jahre\n"
+"              \"2y 1m\" steht für 2 Jahre und ein Monat\n"
+"              \"1m 25d\" steht für 1 Monat und 25 Tage\n"
+"              \"2w 3d\" steht für 2 Wochen und 3 Tage\n"
 "\n"
 "        Das Datumsformat lautet \"JJJJ-MM-TT\", z.B:\n"
 "            2001-06-27\n"
 "\n"
 "        "
 
-#: ../roundup/admin.py:1180
+#: ../roundup/admin.py:1260
 msgid "Invalid format"
-msgstr "Ungültiges Format"
+msgstr "Ungültiges Format"
 
-#: ../roundup/admin.py:1190
+#: ../roundup/admin.py:1271
 msgid ""
 "Usage: reindex [classname|designator]*\n"
 "        Re-generate a tracker's search indexes.\n"
@@ -958,12 +1024,12 @@
 "        normalerweise automatisch.\n"
 "        "
 
-#: ../roundup/admin.py:1204
+#: ../roundup/admin.py:1285
 #, python-format
 msgid "no such item \"%(designator)s\""
 msgstr "Der Eintrag \"%(designator)s\" existiert nicht"
 
-#: ../roundup/admin.py:1214
+#: ../roundup/admin.py:1295
 msgid ""
 "Usage: security [Role name]\n"
 "        Display the Permissions available to one or all Roles.\n"
@@ -973,77 +1039,129 @@
 "        Zeigt die Berechtigungen einer oder aller Rollen an.\n"
 "        "
 
-#: ../roundup/admin.py:1222
+#: ../roundup/admin.py:1303
 #, python-format
 msgid "No such Role \"%(role)s\""
 msgstr "Die Rolle \"%(role)s\" existiert nicht "
 
-#: ../roundup/admin.py:1228
+#: ../roundup/admin.py:1309
 #, python-format
 msgid "New Web users get the Roles \"%(role)s\""
 msgstr "Neue Web-Benutzer erhalten die Rollen \"%(role)s\""
 
-#: ../roundup/admin.py:1230
+#: ../roundup/admin.py:1311
 #, python-format
 msgid "New Web users get the Role \"%(role)s\""
 msgstr "Neue Web-Benutzer erhalten die Rolle \"%(role)s\""
 
-#: ../roundup/admin.py:1233
+#: ../roundup/admin.py:1314
 #, python-format
 msgid "New Email users get the Roles \"%(role)s\""
-msgstr "Neue Email-Benutzer erhalten die Rollen \"%(role)s\""
+msgstr "Neue E-Mail-Benutzer erhalten die Rollen \"%(role)s\""
 
-#: ../roundup/admin.py:1235
+#: ../roundup/admin.py:1316
 #, python-format
 msgid "New Email users get the Role \"%(role)s\""
-msgstr "Neue Email-Benutzer erhalten die Rolle \"%(role)s\""
+msgstr "Neue E-Mail-Benutzer erhalten die Rolle \"%(role)s\""
 
-#: ../roundup/admin.py:1238
+#: ../roundup/admin.py:1319
 #, python-format
 msgid "Role \"%(name)s\":"
 msgstr "Rolle \"%(name)s\":"
 
-#: ../roundup/admin.py:1280
+#: ../roundup/admin.py:1324
 #, python-format
 msgid " %(description)s (%(name)s for \"%(klass)s\": %(properties)s only)"
-msgstr " %(description)s (%(name)s für \"%(klass)s\": ausschließlich %(properties)s)"
+msgstr " %(description)s (%(name)s für \"%(klass)s\": ausschließlich %(properties)s)"
 
-#: ../roundup/admin.py:1241
+#: ../roundup/admin.py:1327
 #, python-format
 msgid " %(description)s (%(name)s for \"%(klass)s\" only)"
-msgstr "%(description)s (%(name)s einzig für \"%(klass)s\")"
+msgstr "%(description)s (%(name)s einzig für \"%(klass)s\")"
 
-#: ../roundup/admin.py:1244
+#: ../roundup/admin.py:1330
 #, python-format
 msgid " %(description)s (%(name)s)"
 msgstr " %(description)s (%(name)s)"
 
-#: ../roundup/admin.py:1273
+#: ../roundup/admin.py:1335
+msgid ""
+"Usage: migrate\n"
+"        Update a tracker's database to be compatible with the Roundup\n"
+"        codebase.\n"
+"\n"
+"        You should run the \"migrate\" command for your tracker once you've\n"
+"        installed the latest codebase. \n"
+"\n"
+"        Do this before you use the web, command-line or mail interface and\n"
+"        before any users access the tracker.\n"
+"\n"
+"        This command will respond with either \"Tracker updated\" (if you've\n"
+"        not previously run it on an RDBMS backend) or \"No migration action\n"
+"        required\" (if you have run it, or have used another interface to the\n"
+"        tracker, or possibly because you are using anydbm).\n"
+"\n"
+"        It's safe to run this even if it's not required, so just get into\n"
+"        the habit.\n"
+"        "
+msgstr ""
+"Verwendung: migrate\n"
+"        Aktualisiert die Datenbank eines Trackers, um sie mit dem aktuellen\n"
+"        Roundup-Code kompatibel zu machen\n"
+"\n"
+"        Sie sollten \"migrate\" einmalig ausführen, wenn Sie auf die neueste        Roundup-Version aktualisiert haben.\n"
+"\n"
+"        Tun Sie das, bevor Sie per Web (Internet-Browser), Kommandozeile\n"
+"        oder Mail auf den Tracker zugreifen.\n"
+"\n"
+"        Das Kommando wird anschließend \"Tracker aktualisiert\" antworten\n"
+"        (sofern Sie es nicht schon vorher ausgeführt haben) oder\n"
+"        \"Keine Migration notwendig\" (wenn Sie es schon ausgeführt hatten,\n"
+"        oder ein anderes Datenbank-Backend verwendet haben, oder\n"
+"        vielleicht weil Sie anydbm verwenden.\n"
+"\n"
+"        Es kann nicht schaden, dieses Kommando auszuführen, selbst wenn\n"
+"        es nicht nötig ist; also gewöhnen Sie es sich einfach an.\n"
+"        "
+
+#: ../roundup/admin.py:1354
+msgid "Tracker updated"
+msgstr "Tracker aktualisiert"
+
+#: ../roundup/admin.py:1357
+msgid "No migration action required"
+msgstr "Keine Migration notwendig"
+
+#: ../roundup/admin.py:1386
 #, python-format
 msgid "Unknown command \"%(command)s\" (\"help commands\" for a list)"
 msgstr "Der Befehl \"%(command)s\" existiert nicht (siehe \"help commands\")"
 
-#: ../roundup/admin.py:1279
+#: ../roundup/admin.py:1392
 #, python-format
 msgid "Multiple commands match \"%(command)s\": %(list)s"
-msgstr "Zur Abkürzung \"%(command)s\" passen mehrere Befehle: %(list)s"
+msgstr "Zur Abkürzung \"%(command)s\" passen mehrere Befehle: %(list)s"
 
-#: ../roundup/admin.py:1286
+#: ../roundup/admin.py:1399
 msgid "Enter tracker home: "
 msgstr "Tracker-Verzeichnis: "
 
 # ../roundup/admin.py:1263 :1269 :1289
-#: ../roundup/admin.py:1293 ../roundup/admin.py:1299 ../roundup/admin.py:1319
+#: ../roundup/admin.py:1406
+#: ../roundup/admin.py:1412
+#: ../roundup/admin.py:1432
+#: ../roundup/admin.py:1406:1412
+#: :1432
 #, python-format
 msgid "Error: %(message)s"
 msgstr "Fehler: %(message)s"
 
-#: ../roundup/admin.py:1307
+#: ../roundup/admin.py:1420
 #, python-format
 msgid "Error: Couldn't open tracker: %(message)s"
-msgstr "Fehler: Die Tracker-Instanz konnte nicht geöffnet werden: %(message)s"
+msgstr "Fehler: Die Tracker-Instanz konnte nicht geöffnet werden: %(message)s"
 
-#: ../roundup/admin.py:1332
+#: ../roundup/admin.py:1445
 #, python-format
 msgid ""
 "Roundup %s ready for input.\n"
@@ -1052,209 +1170,253 @@
 "Roundup %s ist bereit.\n"
 "Schreiben Sie \"help\", um zur Hilfe zu gelangen."
 
-#: ../roundup/admin.py:1337
+#: ../roundup/admin.py:1450
 msgid "Note: command history and editing not available"
-msgstr "Bemerkung: Befehlsverlauf/-bearbeitung nicht verfügbar"
+msgstr "Bemerkung: Befehlsverlauf/-bearbeitung möglicherweise nicht verfügbar"
 
-#: ../roundup/admin.py:1341
+#: ../roundup/admin.py:1454
 msgid "roundup> "
 msgstr "roundup> "
 
-#: ../roundup/admin.py:1343
+#: ../roundup/admin.py:1456
 msgid "exit..."
 msgstr "beenden..."
 
-#: ../roundup/admin.py:1353
+#: ../roundup/admin.py:1466
 msgid "There are unsaved changes. Commit them (y/N)? "
-msgstr "Es gibt noch ungespeicherte Änderungen. Änderungen speichern (y/N)?"
+msgstr "Es gibt noch ungespeicherte Änderungen. Änderungen speichern (y/N)?"
+
+#: ../roundup/backends/back_anydbm.py:218
+#: ../roundup/backends/sessions_dbm.py:50
+msgid "Couldn't identify database type"
+msgstr "Konnte den Datenbanktyp nicht ermitteln"
+
+#: ../roundup/backends/back_anydbm.py:244
+#, python-format
+msgid "Couldn't open database - the required module '%s' is not available"
+msgstr "Konnte die Datenbank nicht öffnen - das erforderliche Modul '%s' ist nicht verfügbar"
+
+#: ../roundup/backends/back_anydbm.py:799
+#: ../roundup/backends/back_anydbm.py:1074
+#: ../roundup/backends/back_anydbm.py:1271
+#: ../roundup/backends/back_anydbm.py:1289
+#: ../roundup/backends/back_anydbm.py:1335
+#: ../roundup/backends/back_anydbm.py:1905
+#: ../roundup/backends/back_anydbm.py:799:1074
+#: ../roundup/backends/rdbms_common.py:1396
+#: ../roundup/backends/rdbms_common.py:1625
+#: ../roundup/backends/rdbms_common.py:1831
+#: ../roundup/backends/rdbms_common.py:1851
+#: ../roundup/backends/rdbms_common.py:1904
+#: ../roundup/backends/rdbms_common.py:2512
+#: ../roundup/backends/rdbms_common.py:1396:1625
+#: :1271:1289
+#: :1335:1905
+#: :1831:1851
+#: :1904:2512
+msgid "Database open read-only"
+msgstr "Datenbank nur zum Lesen geöffnet"
 
-#: ../roundup/backends/back_anydbm.py:2054
+#: ../roundup/backends/back_anydbm.py:2007
 #, python-format
 msgid "WARNING: invalid date tuple %r"
-msgstr "WARNUNG: ungültiges Datums-Tupel %r"
+msgstr "WARNUNG: ungültiges Datums-Tupel %r"
 
-#: ../roundup/backends/rdbms_common.py:1425
+#: ../roundup/backends/rdbms_common.py:1525
 msgid "create"
 msgstr "erstellt"
 
-#: ../roundup/backends/rdbms_common.py:1588
+#: ../roundup/backends/rdbms_common.py:1691
 msgid "unlink"
-msgstr "Link gelöscht"
+msgstr "Link gelöscht"
 
-#: ../roundup/backends/rdbms_common.py:1592
+#: ../roundup/backends/rdbms_common.py:1695
 msgid "link"
 msgstr "verlinkt"
 
-#: ../roundup/backends/rdbms_common.py:1702
+#: ../roundup/backends/rdbms_common.py:1817
 msgid "set"
-msgstr "geändert"
+msgstr "geändert"
 
-#: ../roundup/backends/rdbms_common.py:1726
+#: ../roundup/backends/rdbms_common.py:1841
 msgid "retired"
 msgstr "verborgen"
 
-#: ../roundup/backends/rdbms_common.py:1756
+#: ../roundup/backends/rdbms_common.py:1871
 msgid "restored"
 msgstr "wiederhergestellt"
 
-#: ../roundup/cgi/actions.py:58
-#, python-format
-msgid "You do not have permission to %(action)s the %(classname)s class."
-msgstr ""
-"Sie sind nicht berechtigt, die Aktion(en) %(action)s auf die Klasse"
-" %(classname)s anzuwenden."
-
-#: ../roundup/cgi/actions.py:89
+#: ../roundup/cgi/actions.py:88
 msgid "No type specified"
 msgstr "Typ nicht spezifiziert"
 
-#: ../roundup/cgi/actions.py:91
+#: ../roundup/cgi/actions.py:90
 msgid "No ID entered"
 msgstr "keine ID spezifiziert"
 
-#: ../roundup/cgi/actions.py:97
+#: ../roundup/cgi/actions.py:96
 #, python-format
 msgid "\"%(input)s\" is not an ID (%(classname)s ID required)"
 msgstr "\"%(input)s\" ist keine ID (%(classname)s ID wird erwartet)"
 
-#: ../roundup/cgi/actions.py:117
-msgid "You may not retire the admin or anonymous user"
-msgstr "Sie können den Administrator oder den Gast-Benutzer nicht löschen"
+#: ../roundup/cgi/actions.py:108
+#: ../roundup/cgi/actions.py:287
+#: ../roundup/cgi/actions.py:590
+#: ../roundup/cgi/actions.py:636
+#: ../roundup/cgi/actions.py:822
+#: ../roundup/cgi/actions.py:940
+#: ../roundup/cgi/actions.py:108:287
+#: :590:636
+#: :822:940
+msgid "Invalid request"
+msgstr "Ungültige Anforderung"
+
+#: ../roundup/cgi/actions.py:126
+#: ../roundup/cgi/actions.py:382
+#: ../roundup/cgi/actions.py:126:382
+#, python-format
+msgid "You do not have permission to retire %(class)s"
+msgstr "Sie sind nicht berechtigt, Einträge der Klasse \"%(class)s\" zu löschen"
 
-#: ../roundup/cgi/actions.py:124
+#: ../roundup/cgi/actions.py:134
 #, python-format
 msgid "%(classname)s %(itemid)s has been retired"
-msgstr "%(classname)s %(itemid)s wurde gelöscht"
+msgstr "%(classname)s %(itemid)s wurde gelöscht"
 
 # ../roundup/cgi/actions.py:174 :202
-#: ../roundup/cgi/actions.py:174 ../roundup/cgi/actions.py:202
+#: ../roundup/cgi/actions.py:175
+#: ../roundup/cgi/actions.py:203
+#: ../roundup/cgi/actions.py:175:203
 msgid "You do not have permission to edit queries"
 msgstr "Sie haben keine Berechtigung, Abfragen zu bearbeiten."
 
 # ../roundup/cgi/actions.py:180 :209
-#: ../roundup/cgi/actions.py:180 ../roundup/cgi/actions.py:209
+#: ../roundup/cgi/actions.py:181
+#: ../roundup/cgi/actions.py:210
+#: ../roundup/cgi/actions.py:181:210
 msgid "You do not have permission to store queries"
 msgstr "Sie haben keine Berechtigung, Abfragen zu speichern."
 
-#: ../roundup/cgi/actions.py:279
+#: ../roundup/cgi/actions.py:321
+#: ../roundup/cgi/actions.py:507
+#: ../roundup/cgi/actions.py:321:507
+#, python-format
+msgid "You do not have permission to create %(class)s"
+msgstr "Sie sind nicht berechtigt, Einträge der Klasse \"%(class)s\" zu erstellen"
+
+#: ../roundup/cgi/actions.py:329
 #, python-format
 msgid "Not enough values on line %(line)s"
-msgstr "Nicht genügend Werte in Zeile %(line)s"
+msgstr "Nicht genügend Werte in Zeile %(line)s"
 
-#: ../roundup/cgi/actions.py:326
+#: ../roundup/cgi/actions.py:339
+#: ../roundup/cgi/actions.py:495
+#: ../roundup/cgi/actions.py:339:495
+#, python-format
+msgid "You do not have permission to edit %(class)s"
+msgstr "Sie sind nicht berechtigt, Einträge der Klasse \"%(class)s\" zu bearbeiten"
+
+#: ../roundup/cgi/actions.py:389
 msgid "Items edited OK"
-msgstr "Die Einträge wurden aktualisiert"
+msgstr "Die Einträge wurden aktualisiert"
 
-#: ../roundup/cgi/actions.py:386
+#: ../roundup/cgi/actions.py:448
 #, python-format
 msgid "%(class)s %(id)s %(properties)s edited ok"
 msgstr "Eigenschaft \"%(properties)s\" bei \"%(class)s %(id)s\" bearbeitet"
 
-#: ../roundup/cgi/actions.py:389
+#: ../roundup/cgi/actions.py:451
 #, python-format
 msgid "%(class)s %(id)s - nothing changed"
-msgstr "%(class)s %(id)s - keine Änderungen"
+msgstr "%(class)s %(id)s - keine Änderungen"
 
-#: ../roundup/cgi/actions.py:401
+#: ../roundup/cgi/actions.py:463
 #, python-format
 msgid "%(class)s %(id)s created"
 msgstr "Der Eintrag \"%(class)s%(id)s\" wurde erstellt"
 
-#: ../roundup/cgi/actions.py:433
-#, python-format
-msgid "You do not have permission to edit %(class)s"
-msgstr ""
-"Sie sind nicht berechtigt, Einträge der Klasse \"%(class)s\" zu "
-"bearbeiten"
-
-#: ../roundup/cgi/actions.py:445
+#: ../roundup/cgi/actions.py:575
 #, python-format
-msgid "You do not have permission to create %(class)s"
-msgstr ""
-"Sie sind nicht berechtigt, Einträge der Klasse \"%(class)s\" zu "
-"erstellen"
-
-#: ../roundup/cgi/actions.py:468
-msgid "You do not have permission to edit user roles"
-msgstr "Sie sind nicht berechtigt, Benutzer-Rollen zu ändern"
-
-#: ../roundup/cgi/actions.py:537
-#, 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 ""
-"Fehler: Jemand anders hat dieses %s bearbeitet (%s). Sehen Sie "
-"<a target=\"_new\" href=\"%s%s\">dessen Änderungen</a> in einem neuen Fenster."
+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 "Fehler: Jemand anders hat dieses %s bearbeitet (%s). Sehen Sie <a target=\"_new\" href=\"%s%s\">dessen Änderungen</a> in einem neuen Fenster."
 
-#: ../roundup/cgi/actions.py:530
+#: ../roundup/cgi/actions.py:607
 #, python-format
 msgid "Edit Error: %s"
 msgstr "Fehler bei der Bearbeitung: %s"
 
 # ../roundup/cgi/actions.py:546 :556
-#: ../roundup/cgi/actions.py:561 ../roundup/cgi/actions.py:572
-#: ../roundup/cgi/actions.py:743 ../roundup/cgi/actions.py:762
+#: ../roundup/cgi/actions.py:642
+#: ../roundup/cgi/actions.py:658
+#: ../roundup/cgi/actions.py:828
+#: ../roundup/cgi/actions.py:847
+#: ../roundup/cgi/actions.py:642:658
+#: :828:847
 #, python-format
 msgid "Error: %s"
 msgstr "Fehler: %s"
 
-#: ../roundup/cgi/actions.py:598
+#: ../roundup/cgi/actions.py:684
 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 ""
-"Ungültiger Authentifizierungscode!\n"
-"(Ein Fehler in Mozilla kann diese Meldung hervorrufen, bitte prüfen Sie Ihr "
-"Email-Konto)"
+"Ungültiger Authentifizierungscode!\n"
+"(Ein Fehler in Mozilla kann diese Meldung hervorrufen, bitte prüfen Sie Ihr E-Mail-Konto)"
 
-#: ../roundup/cgi/actions.py:640
+#: ../roundup/cgi/actions.py:726
 #, python-format
 msgid "Password reset and email sent to %s"
-msgstr "Ihr Paßwort wurde zurückgesetzt und per Email an %s versandt"
+msgstr "Ihr Passwort wurde zurückgesetzt und per E-Mail an %s versandt"
 
-#: ../roundup/cgi/actions.py:649
+#: ../roundup/cgi/actions.py:735
 msgid "Unknown username"
 msgstr "Benutzername unbekannt"
 
-#: ../roundup/cgi/actions.py:657
+#: ../roundup/cgi/actions.py:743
 msgid "Unknown email address"
-msgstr "Email-Adresse unbekannt"
+msgstr "E-Mail-Adresse unbekannt"
 
-#: ../roundup/cgi/actions.py:662
+#: ../roundup/cgi/actions.py:748
 msgid "You need to specify a username or address"
-msgstr "Sie müssen einen Benutzernamen oder eine Email-Adresse angeben"
+msgstr "Sie müssen einen Benutzernamen oder eine E-Mail-Adresse angeben"
 
-#: ../roundup/cgi/actions.py:687
+#: ../roundup/cgi/actions.py:773
 #, python-format
 msgid "Email sent to %s"
-msgstr "Eine Email wurde an %s versandt"
+msgstr "Eine E-Mail wurde an %s versandt"
 
-#: ../roundup/cgi/actions.py:706
+#: ../roundup/cgi/actions.py:787
 msgid "You are now registered, welcome!"
 msgstr "Sie sind nun registriert. Willkommen!"
 
-#: ../roundup/cgi/actions.py:751
+#: ../roundup/cgi/actions.py:836
 msgid "It is not permitted to supply roles at registration."
-msgstr "Bei der Registrierung dürfen keine Rollen angegeben werden"
+msgstr "Bei der Registrierung dürfen keine Rollen angegeben werden"
 
-#: ../roundup/cgi/actions.py:834
+#: ../roundup/cgi/actions.py:923
 msgid "You are logged out"
 msgstr "Sie wurden vom System abgemeldet"
 
-#: ../roundup/cgi/actions.py:845
+#: ../roundup/cgi/actions.py:944
 msgid "Username required"
 msgstr "Benutzername notwendig"
 
-#: ../roundup/cgi/actions.py:873 ../roundup/cgi/actions.py:877
+#: ../roundup/cgi/actions.py:978
+#: ../roundup/cgi/actions.py:982
+#: ../roundup/cgi/actions.py:978:982
 msgid "Invalid login"
-msgstr "Ungültiger Benutzername"
+msgstr "Ungültiger Benutzername"
 
-#: ../roundup/cgi/actions.py:883
+#: ../roundup/cgi/actions.py:988
 msgid "You do not have permission to login"
 msgstr "Sie sind nicht berechtigt, sich anzumelden"
 
+#: ../roundup/cgi/actions.py:1047
+#, python-format
+msgid "You do not have permission to view %(class)s"
+msgstr "Sie sind nicht berechtigt, Einträge der Klasse \"%(class)s\" zu lesen"
+
 #: ../roundup/cgi/cgitb.py:49
 #, python-format
 msgid ""
@@ -1298,7 +1460,7 @@
 "</table></li>\n"
 msgstr ""
 "\n"
-"<li>Beim Ausführen von %(info)r auf Zeile %(line)d\n"
+"<li>Beim Ausführen von %(info)r auf Zeile %(line)d\n"
 "<table class=\"otherinfo\" style=\"font-size: 90%%\">\n"
 " <tr><th colspan=\"2\" class=\"header\">Aktuelle Variablen:</th></tr>\n"
 " %(globals)s\n"
@@ -1307,7 +1469,7 @@
 
 #: ../roundup/cgi/cgitb.py:103
 msgid "Full traceback:"
-msgstr "Vollständiger Traceback:"
+msgstr "Vollständiger Traceback:"
 
 #: ../roundup/cgi/cgitb.py:116
 #, python-format
@@ -1315,19 +1477,12 @@
 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 ""
-"<p>Ein Problem trat auf, als ein Python-Script ausgeführt wurde. Hier sehen "
-"Sie die Aufrufe, welche zu dem Fehler führten. Der letzte (innerste) Aufruf erscheint "
-"dabei zuoberst. Der Fehler hat folgende Attribute: "
+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>Ein Problem trat auf, als ein Python-Script ausgeführt wurde. Hier sehen Sie die Aufrufe, welche zu dem Fehler führten. Der letzte (innerste) Aufruf erscheint dabei zuoberst. Der Fehler hat folgende Attribute: "
 
 #: ../roundup/cgi/cgitb.py:129
 msgid "&lt;file is None - probably inside <tt>eval</tt> or <tt>exec</tt>&gt;"
-msgstr "&lt;file ist None - Wahrscheinlich in einem <tt>eval</tt> oder einem "
-"<tt>exec</tt>&gt;"
+msgstr "&lt;file ist None - Wahrscheinlich in einem <tt>eval</tt> oder einem <tt>exec</tt>&gt;"
 
 #: ../roundup/cgi/cgitb.py:138
 #, python-format
@@ -1335,361 +1490,460 @@
 msgstr "in <strong>%s</strong>"
 
 # ../roundup/cgi/cgitb.py:145 :151
-#: ../roundup/cgi/cgitb.py:172 ../roundup/cgi/cgitb.py:178
+#: ../roundup/cgi/cgitb.py:172
+#: ../roundup/cgi/cgitb.py:178
+#: ../roundup/cgi/cgitb.py:172:178
 msgid "<em>undefined</em>"
 msgstr "<em>nicht definiert</em>"
 
-#: ../roundup/cgi/client.py:49
-msgid ""
-"<html><head><title>Ein Fehler ist aufgetreten</title></head>\n"
-"<body><h1>Ein Fehler ist aufgetreten</h1>\n"
-"<p>Bei der Bearbeitung Ihrer Daten ist ein Fehler aufgetreten. "
-"Die Admistratoren wurden benachrichtigt.</p>\n"
-"</body></html>"
-msgstr ""
-
-#: ../roundup/cgi/client.py:291
+#: ../roundup/cgi/client.py:517
 msgid "Form Error: "
 msgstr "Formular-Fehler: "
 
-#: ../roundup/cgi/client.py:344
+#: ../roundup/cgi/client.py:575
 #, python-format
 msgid "Unrecognized charset: %r"
 msgstr "Zeichensatz nicht erkannt: %r"
 
-#: ../roundup/cgi/client.py:446
+#: ../roundup/cgi/client.py:696
 msgid "Anonymous users are not allowed to use the web interface"
-msgstr ""
-"Gast-Benutzer sind nicht berechtigt, das Web-Interface zu benutzen."
+msgstr "Gast-Benutzer sind nicht berechtigt, das Web-Interface zu benutzen."
 
-#: ../roundup/cgi/client.py:597
+#: ../roundup/cgi/client.py:851
 msgid "You are not allowed to view this file."
 msgstr "Sie sind nicht berechtigt, diese Seite anzuzeigen."
 
-#: ../roundup/cgi/client.py:689
+#: ../roundup/cgi/client.py:968
 #, python-format
 msgid "%(starttag)sTime elapsed: %(seconds)fs%(endtag)s\n"
-msgstr "%(starttag)sBenötigte Zeit: %(seconds)fs%(endtag)s\n"
+msgstr "%(starttag)sBenötigte Zeit: %(seconds)fs%(endtag)s\n"
 
-#: ../roundup/cgi/client.py:693
+#: ../roundup/cgi/client.py:972
 #, 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 benutzt: %(cache_hits)d, verfehlt: %(cache_misses)d. " 
-"Einträge laden: %(get_items)fs; filtern: %(filtering)fs.%(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 benutzt: %(cache_hits)d, verfehlt: %(cache_misses)d. Einträge laden: %(get_items)fs; filtern: %(filtering)fs.%(endtag)s\n"
 
 #: ../roundup/cgi/form_parser.py:283
 #, python-format
-msgid "link \"%(key)s\" value \"%(value)s\" not a designator"
-msgstr ""
-"Der Wert \"%(value)s\" ist kein gültiger Bezeichner für die Verknüpfung \"%"
-"(key)s\""
+msgid "link \"%(key)s\" value \"%(entry)s\" not a designator"
+msgstr "Der Wert \"%(entry)s\" ist kein gültiger Bezeichner für die Verknüpfung \"%(key)s\""
 
-#: ../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 "%(class)s %(property)s ist weder ein Link noch ein Mehrfachlink"
 
-#: ../roundup/cgi/form_parser.py:312
+#: ../roundup/cgi/form_parser.py:313
 #, python-format
-msgid ""
-"You have submitted a %(action)s action for the property \"%(property)s\" "
-"which doesn't exist"
-msgstr "Die Aktion %(action)s gilt nicht für die Eigenschaft \"%(property)s\" "
+msgid "The form action claims to require property \"%(property)s\" which doesn't exist"
+msgstr "Die Aktion erfordert die fehlende Angabe \"%(property)s\""
+
+#: ../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"
+msgstr "Die Aktion %(action)s gilt nicht für die Eigenschaft \"%(property)s\" "
 
 # ../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 "Sie haben mehr als einen Wert für die Eigenschaft \"%s\" übermittelt"
+msgstr "Sie haben mehr als einen Wert für die Eigenschaft \"%s\" übermittelt"
 
 # ../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 "Die beiden Paßwort-Felder stimmen nicht überein"
+msgstr "Die beiden Passwortfelder stimmen nicht überein"
 
-#: ../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 "Der Wert \"%(value)s\" ist nicht in der Liste für \"%(propname)s\""
+msgstr "Der Wert \"%(value)s\" ist nicht in der Liste für \"%(propname)s\""
 
-#: ../roundup/cgi/form_parser.py:509
+#: ../roundup/cgi/form_parser.py:557
 #, python-format
 msgid "Required %(class)s property %(property)s not supplied"
 msgid_plural "Required %(class)s properties %(property)s not supplied"
-msgstr[0] ""
-"Die Eigenschaft \"%(property)s\" muß für die Klasse \"%(class)s\" angegeben "
-"werden"
-msgstr[1] ""
-"Die Eigenschaften \"%(property)\" müssen für die Klasse \"%(class)s\" "
-"angegeben werden"
+msgstr[0] "Die Eigenschaft \"%(property)s\" muss für die Klasse \"%(class)s\" angegeben werden"
+msgstr[1] "Die Eigenschaften \"%(property)s\" müssen für die Klasse \"%(class)s\" angegeben werden"
 
-#: ../roundup/cgi/form_parser.py:529
+#: ../roundup/cgi/form_parser.py:580
 msgid "File is empty"
-msgstr "Die ausgewählte Datei ist leer"
+msgstr "Die ausgewählte Datei ist leer"
 
-#: ../roundup/cgi/templating.py:68
+#: ../roundup/cgi/templating.py:77
 #, python-format
 msgid "You are not allowed to %(action)s items of class %(class)s"
-msgstr ""
-"Sie sind nicht berechtigt, die Aktion  \"%(action)s\" auf Einträge der "
-"Klasse \"%(class)s\" anzuwenden"
+msgstr "Sie sind nicht berechtigt, die Aktion  \"%(action)s\" auf Einträge der Klasse \"%(class)s\" anzuwenden"
 
-#: ../roundup/cgi/templating.py:612
+#: ../roundup/cgi/templating.py:664
 msgid "(list)"
 msgstr "(Liste)"
 
-#: ../roundup/cgi/templating.py:646
+#: ../roundup/cgi/templating.py:733
 msgid "Submit New Entry"
 msgstr "Eintrag speichern"
 
-#: ../roundup/cgi/templating.py:656
+#: ../roundup/cgi/templating.py:747
+#: ../roundup/cgi/templating.py:886
+#: ../roundup/cgi/templating.py:1358
+#: ../roundup/cgi/templating.py:1387
+#: ../roundup/cgi/templating.py:1407
+#: ../roundup/cgi/templating.py:1420
+#: ../roundup/cgi/templating.py:1471
+#: ../roundup/cgi/templating.py:1494
+#: ../roundup/cgi/templating.py:1530
+#: ../roundup/cgi/templating.py:1567
+#: ../roundup/cgi/templating.py:1620
+#: ../roundup/cgi/templating.py:1637
+#: ../roundup/cgi/templating.py:1721
+#: ../roundup/cgi/templating.py:1741
+#: ../roundup/cgi/templating.py:1759
+#: ../roundup/cgi/templating.py:1791
+#: ../roundup/cgi/templating.py:1801
+#: ../roundup/cgi/templating.py:1853
+#: ../roundup/cgi/templating.py:2069
+#: ../roundup/cgi/templating.py:747:886
+#: :1358:1387
+#: :1407:1420
+#: :1471:1494
+#: :1530:1567
+#: :1620:1637
+#: :1721:1741
+#: :1759:1791
+#: :1801:1853
+#: :2069
+msgid "[hidden]"
+msgstr "[verborgen]"
+
+#: ../roundup/cgi/templating.py:748
 msgid "New node - no history"
 msgstr "Neuer Eintrag - Noch kein Verlauf"
 
-#: ../roundup/cgi/templating.py:756
+#: ../roundup/cgi/templating.py:868
 msgid "Submit Changes"
 msgstr "Speichern"
 
-#: ../roundup/cgi/templating.py:837
+#: ../roundup/cgi/templating.py:950
 msgid "<em>The indicated property no longer exists</em>"
-msgstr "<em>Die gewählte Eigenschaft existiert nicht mehr</em>"
+msgstr "<em>Die gewählte Eigenschaft existiert nicht mehr</em>"
 
-#: ../roundup/cgi/templating.py:838
+#: ../roundup/cgi/templating.py:951
 #, python-format
 msgid "<em>%s: %s</em>\n"
 msgstr "<em>%s: %s</em>\n"
 
-#: ../roundup/cgi/templating.py:851
+#: ../roundup/cgi/templating.py:964
 #, python-format
 msgid "The linked class %(classname)s no longer exists"
 msgstr "Die verlinkte Klasse \"%(classname)s\" existiert nicht mehr"
 
 # ../roundup/cgi/templating.py:905 :926
-#: ../roundup/cgi/templating.py:884 ../roundup/cgi/templating.py:905
+#: ../roundup/cgi/templating.py:998
+#: ../roundup/cgi/templating.py:1023
+#: ../roundup/cgi/templating.py:998:1023
 msgid "<strike>The linked node no longer exists</strike>"
-msgstr "<strike>Der verknüpfte Eintrag existiert nicht mehr</strike>"
+msgstr "<strike>Der verknüpfte Eintrag existiert nicht mehr</strike>"
 
-#: ../roundup/cgi/templating.py:944
-msgid "No"
-msgstr "Nein"
-
-#: ../roundup/cgi/templating.py:944
-msgid "Yes"
-msgstr "Ja"
-
-#: ../roundup/cgi/templating.py:955
+#: ../roundup/cgi/templating.py:1077
 #, python-format
 msgid "%s: (no value)"
 msgstr "%s: (kein Wert)"
 
-#: ../roundup/cgi/templating.py:967
-msgid ""
-"<strong><em>This event is not handled by the history display!</em></strong>"
-msgstr ""
-"<strong><em>Dieses Ereignis kann nicht im Verlauf angezeigt werden!</em></strong>"
+#: ../roundup/cgi/templating.py:1089
+msgid "<strong><em>This event is not handled by the history display!</em></strong>"
+msgstr "<strong><em>Dieses Ereignis kann nicht im Verlauf angezeigt werden!</em></strong>"
 
-#: ../roundup/cgi/templating.py:979
+#: ../roundup/cgi/templating.py:1101
 msgid "<tr><td colspan=4><strong>Note:</strong></td></tr>"
-msgstr "<tr><td colspan=4><strong>Notiz:</strong></td></tr>"
+msgstr "<tr><td colspan=\"4\"><strong>Bitte beachten:</strong></td></tr>"
 
-#: ../roundup/cgi/templating.py:988
+#: ../roundup/cgi/templating.py:1110
 msgid "History"
 msgstr "Verlauf"
 
-#: ../roundup/cgi/templating.py:990
+#: ../roundup/cgi/templating.py:1112
 msgid "<th>Date</th>"
 msgstr "<th>Datum</th>"
 
-#: ../roundup/cgi/templating.py:991
+#: ../roundup/cgi/templating.py:1113
 msgid "<th>User</th>"
 msgstr "<th>Benutzer</th>"
 
-#: ../roundup/cgi/templating.py:992
+#: ../roundup/cgi/templating.py:1114
 msgid "<th>Action</th>"
 msgstr "<th>Aktion</th>"
 
-#: ../roundup/cgi/templating.py:993
+#: ../roundup/cgi/templating.py:1115
 msgid "<th>Args</th>"
 msgstr "<th>Argumente</th>"
 
-#: ../roundup/cgi/templating.py:1097
+#: ../roundup/cgi/templating.py:1160
 #, python-format
 msgid "Copy of %(class)s %(id)s"
-msgstr ""
-"Kopie von %(class)s %(id)s"
+msgstr "Kopie von %(class)s %(id)s"
 
-#: ../roundup/cgi/templating.py:1234
+#: ../roundup/cgi/templating.py:1498
 msgid "*encrypted*"
-msgstr "*verschlüsselt*"
+msgstr "*verschlüsselt*"
 
-#: ../roundup/cgi/templating.py:1412
-msgid ""
-"default value for DateHTMLProperty must be either DateHTMLProperty or string "
-"date representation."
+#: ../roundup/cgi/templating.py:1571
+#: ../roundup/cgi/templating.py:1592
+#: ../roundup/cgi/templating.py:1598
+#: ../roundup/cgi/templating.py:1066:1571
+#: :1592:1598
+msgid "No"
+msgstr "Nein"
+
+#: ../roundup/cgi/templating.py:1571
+#: ../roundup/cgi/templating.py:1590
+#: ../roundup/cgi/templating.py:1595
+#: ../roundup/cgi/templating.py:1066:1571
+#: :1590:1595
+msgid "Yes"
+msgstr "Ja"
+
+#: ../roundup/cgi/templating.py:1684
+msgid "default value for DateHTMLProperty must be either DateHTMLProperty or string date representation."
 msgstr ""
-"Der voreingestellte Wert einer DateHTML-Eigenschaft muß entweder ein\n"
-"DateHTML-Objekt sein oder ein Datum repräsentieren."
+"Der voreingestellte Wert einer DateHTML-Eigenschaft muss entweder ein\n"
+"DateHTML-Objekt sein oder ein Datum repräsentieren."
 
-#: ../roundup/cgi/templating.py:1674
+#: ../roundup/cgi/templating.py:1844
 #, python-format
 msgid "Attempt to look up %(attr)s on a missing value"
-msgstr ""
-"Versuch, das Attribut %(attr)s eines nicht vorhandenen Werts abzufragen"
+msgstr "Versuch, das Attribut %(attr)s eines nicht vorhandenen Werts abzufragen"
 
-#: ../roundup/cgi/templating.py:1750
-#: ../roundup/cgi/templating.py:1600
+#: ../roundup/cgi/templating.py:1929
 #, python-format
 msgid "<option %svalue=\"-1\">- no selection -</option>"
-msgstr "<option %svalue=\"-1\">- nichts ausgewählt -</option>"
+msgstr "<option %svalue=\"-1\">- nichts ausgewählt -</option>"
 
-#: ../roundup/date.py:180
-#, python-format
-msgid "Not a date spec: %s"
-msgstr "Kein gültiges Datum: %s"
+#: ../roundup/date.py:292
+msgid "Not a date spec: \"yyyy-mm-dd\", \"mm-dd\", \"HH:MM\", \"HH:MM:SS\" or \"yyyy-mm-dd.HH:MM:SS.SSS\""
+msgstr "Kein gültiges Datum: \"yyyy-mm-dd\", \"mm-dd\", \"HH:MM\", \"HH:MM:SS\" oder \"yyyy-mm-dd.HH:MM:SS.SSS\""
 
-#: ../roundup/date.py:186
-msgid ""
-"Kein gültiges Datum: \"yyyy-mm-dd\", \"mm-dd\", \"HH:MM\", \"HH:MM:SS\" oder \"yyyy-"
-"mm-dd.HH:MM:SS.SSS\""
-msgstr ""
+#: ../roundup/date.py:315
+msgid "Could not determine granularity"
+msgstr "Konnte Körnigkeit nicht ermitteln"
 
-#: ../roundup/date.py:231
+#: ../roundup/date.py:365
 #, python-format
-msgid "%r not a date spec (%s)"
-msgstr "%r ist kein gültiges Datum (%s)"
+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 ist keine Datums- oder Zeitangabe (\"yyyy-mm-dd\", \"mm-dd\", \"HH:MM\", \"HH:MM:SS\" oder \"yyyy-mm-dd.HH:MM:SS.SSS\")"
 
-#: ../roundup/date.py:522
-msgid ""
-"Not an interval spec:"
-" [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [date spec]"
-msgstr ""
-"Keine gültige Intervall-Angabe:"
-" [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [Datum]"
+#: ../roundup/date.py:677
+msgid "Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [date spec]"
+msgstr "Keine gültige Intervall-Angabe: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [Datum]"
 
-#: ../roundup/date.py:541
-msgid "Not an interval spec:"
-" [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]"
-msgstr ""
-"Keine gültige Intervall-Angabe:"
-" [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]"
+#: ../roundup/date.py:699
+msgid "Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]"
+msgstr "Keine gültige Intervall-Angabe: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]"
 
-#: ../roundup/date.py:678
+#: ../roundup/date.py:836
 #, python-format
 msgid "%(number)s year"
 msgid_plural "%(number)s years"
 msgstr[0] "%(number)s Jahr"
 msgstr[1] "%(number)s Jahren"
 
-#: ../roundup/date.py:682
+#: ../roundup/date.py:840
 #, python-format
 msgid "%(number)s month"
 msgid_plural "%(number)s months"
 msgstr[0] "%(number)s Monat"
 msgstr[1] "%(number)s Monaten"
 
-#: ../roundup/date.py:686
+#: ../roundup/date.py:844
 #, python-format
 msgid "%(number)s week"
 msgid_plural "%(number)s weeks"
 msgstr[0] "%(number)s Woche"
 msgstr[1] "%(number)s Wochen"
 
-#: ../roundup/date.py:690
+#: ../roundup/date.py:848
 #, python-format
 msgid "%(number)s day"
 msgid_plural "%(number)s days"
 msgstr[0] "%(number)s Tag"
 msgstr[1] "%(number)s Tagen"
 
-#: ../roundup/date.py:694
+#: ../roundup/date.py:852
 msgid "tomorrow"
 msgstr "morgen"
 
-#: ../roundup/date.py:696
+#: ../roundup/date.py:854
 msgid "yesterday"
 msgstr "gestern"
 
-#: ../roundup/date.py:699
+#: ../roundup/date.py:857
 #, python-format
 msgid "%(number)s hour"
 msgid_plural "%(number)s hours"
 msgstr[0] "%(number)s Stunde"
 msgstr[1] "%(number)s Stunden"
 
-#: ../roundup/date.py:703
+#: ../roundup/date.py:861
 msgid "an hour"
 msgstr "eine Stunde"
 
-#: ../roundup/date.py:705
+#: ../roundup/date.py:863
 msgid "1 1/2 hours"
 msgstr "1 1/2 Stunden"
 
-#: ../roundup/date.py:707
+#: ../roundup/date.py:865
 #, python-format
 msgid "1 %(number)s/4 hours"
 msgid_plural "1 %(number)s/4 hours"
-msgstr[0] "1 %(number)s/4 Stunden"
+msgstr[0] "1 %(number)s/4 Stunde"
 msgstr[1] "1 %(number)s/4 Stunden"
 
-#: ../roundup/date.py:711
+#: ../roundup/date.py:869
 msgid "in a moment"
-msgstr "in Kürze"
+msgstr "in Kürze"
 
-#: ../roundup/date.py:713
+#: ../roundup/date.py:871
 msgid "just now"
 msgstr "soeben"
 
-#: ../roundup/date.py:716
+#: ../roundup/date.py:874
 msgid "1 minute"
 msgstr "1 Minute"
 
-#: ../roundup/date.py:719
+#: ../roundup/date.py:877
 #, python-format
 msgid "%(number)s minute"
 msgid_plural "%(number)s minutes"
 msgstr[0] "%(number)s Minute"
 msgstr[1] "%(number)s Minuten"
 
-#: ../roundup/date.py:722
+#: ../roundup/date.py:880
 msgid "1/2 an hour"
 msgstr "1/2 Stunde"
 
-#: ../roundup/date.py:724
+#: ../roundup/date.py:882
 #, python-format
 msgid "%(number)s/4 hour"
 msgid_plural "%(number)s/4 hours"
-msgstr[0] "%(number)s/4 Stunden"
+msgstr[0] "%(number)s/4 Stunde"
 msgstr[1] "%(number)s/4 Stunden"
 
-#: ../roundup/date.py:728
+#: ../roundup/date.py:886
 #, python-format
 msgid "%s ago"
 msgstr "vor %s"
 
-#: ../roundup/date.py:730
+#: ../roundup/date.py:888
 #, python-format
 msgid "in %s"
 msgstr "in %s"
 
-#: ../roundup/init.py:132
+#: ../roundup/hyperdb.py:91
+#, python-format
+msgid "property %s: %s"
+msgstr "Eigenschaft %s: %s"
+
+#: ../roundup/hyperdb.py:111
+#, python-format
+msgid "property %s: %r is an invalid date (%s)"
+msgstr "Eigenschaft %s: %r ist kein gültiges Datum (%s)"
+
+#: ../roundup/hyperdb.py:128
+#, python-format
+msgid "property %s: %r is an invalid date interval (%s)"
+msgstr "Eigenschaft %s: %r ist kein gültiges Datumsintervall (%s)"
+
+#: ../roundup/hyperdb.py:223
+#, python-format
+msgid "property %s: %r is not currently an element"
+msgstr "Eigenschaft %s: %r ist derzeit kein Element"
+
+#: ../roundup/hyperdb.py:267
+#, python-format
+msgid "property %s: %r is not a number"
+msgstr "Eigenschaft %s: %r ist keine Zahl"
+
+#: ../roundup/hyperdb.py:280
+#, python-format
+msgid "\"%s\" not a node designator"
+msgstr "\"%s\" ist kein gültiger Bezeichner"
+
+#: ../roundup/hyperdb.py:953
+#: ../roundup/hyperdb.py:961
+#: ../roundup/hyperdb.py:953:961
+#, python-format
+msgid "Not a property name: %s"
+msgstr "Keine Eigenschaft: %s"
+
+#: ../roundup/hyperdb.py:1244
+#, python-format
+msgid "property %s: %r is not a %s."
+msgstr "Eigenschaft %s: %r ist kein %s."
+
+#: ../roundup/hyperdb.py:1247
+#, python-format
+msgid "you may only enter ID values for property %s"
+msgstr "Sie können für die Eigenschaft %s  nur IDs eingeben"
+
+#: ../roundup/hyperdb.py:1277
+#, python-format
+msgid "%r is not a property of %s"
+msgstr "%r ist keine Eigenschaft von %s"
+
+#: ../roundup/init.py:136
 #, python-format
 msgid ""
 "WARNING: directory '%s'\n"
 "\tcontains old-style template - ignored"
 msgstr ""
 "WARNUNG: Das Verzeichnis '%s'\n"
-"\tenthält Templates im alten Format, die ignoriert werden."
+"\tenthält Templates im alten Format, die ignoriert werden."
+
+#: ../roundup/mailgw.py:201
+#: ../roundup/mailgw.py:213
+#: ../roundup/mailgw.py:201:213
+#, python-format
+msgid "Message signed with unknown key: %s"
+msgstr "Nachricht signiert mit unbekanntem Schlüssel: %s"
+
+#: ../roundup/mailgw.py:204
+#, python-format
+msgid "Message signed with an expired key: %s"
+msgstr "Nachricht signiert mit abgelaufenem Schlüssel: %s"
+
+#: ../roundup/mailgw.py:207
+#, python-format
+msgid "Message signed with a revoked key: %s"
+msgstr "Nachricht signiert mit zurückgezogenem Schlüssel: %s"
 
-#: ../roundup/mailgw.py:586
+#: ../roundup/mailgw.py:210
+msgid "Invalid PGP signature detected."
+msgstr "Ungültige PGP-Signatur festgestellt."
+
+#: ../roundup/mailgw.py:464
+msgid "Unknown multipart/encrypted version."
+msgstr "Unbekannte Version von multipart/encrypted."
+
+#: ../roundup/mailgw.py:473
+msgid "Unable to decrypt your message."
+msgstr "Kann Ihre Nachricht nicht entschlüsseln"
+
+#: ../roundup/mailgw.py:502
+msgid "No PGP signature found in message."
+msgstr "Keine PGP-Signatur in Nachricht gefunden"
+
+#: ../roundup/mailgw.py:812
 msgid ""
 "\n"
 "Emails to Roundup trackers must include a Subject: line!\n"
 msgstr ""
+"\n"
+"Mails an Roundup müssen eine Subject-Zeile haben (Betreff)!\n"
 
-#: ../roundup/mailgw.py:674
+#: ../roundup/mailgw.py:936
 #, python-format
 msgid ""
 "\n"
@@ -1705,31 +1959,83 @@
 "\n"
 "Subject was: '%(subject)s'\n"
 msgstr ""
+"\n"
+"Ihre Nachricht an Roundup enthielt keine gültige Betreffzeile (subject).\n"
+"Der Betreff muß einen Klassennamen oder Bezeichner enthalten, um\n"
+"anzuzeigen, worum es geht. Zum Beispiel:\n"
+"  Betreff: [issue] Ein neues Thema\n"
+"   - dadurch wird ein neuer issue-Eintrag mit dem Titel\n"
+"     'Ein neues Thema' angelegt\n"
+"  Betreff: [issue1234] Vorhandenes Thema 1234\n"
+"   - dadurch wird dem vorhandenen 'issue' mit der Nummer 1234\n"
+"     eine neue Nachricht hinzugefügt\n"
+"\n"
+"Der Betreff war: '%(subject)s'\n"
 
-#: ../roundup/mailgw.py:705
+#: ../roundup/mailgw.py:974
 #, 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"
+"Der von Ihnen in der Betreffzeile angegebene Klassenname (\"%(classname)s\")existiert in der Datenbank nicht.\n"
+"Gültige Klassen sind: %(validname)s\n"
+"Die Betreffzeile war: \"%(subject)s\"\n"
+
+#: ../roundup/mailgw.py:982
+#, 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"
+"Sie haben in der Betreffzeile keinen Klassennamen angegeben, und es ist für\n"
+"diesen Tracker kein Standardwert gesetzt. Die Betreffzeile muß eine Klasse\n"
+"oder einen Bezeichner des Gegenstands Ihrer Nachricht enthalten;\n"
+"zum Beispiel:\n"
+"    Subject: [issue] Dies ist ein neues Issue\n"
+"      - dies erzeugt ein neues Issue im Tracker mit dem Titel 'Dies\n"
+"        ist ein neues Issue'.\n"
+"    Subject: [issue1234] Dies bezieht sich auf Issue 1234\n"
+"      - fügt den Inhalt der Nachricht dem existierenden Issue 1234 hinzu\n"
+"\n"
+"Die Betreffzeile (Subject) war:\n"
+"   '%(subject)s'\n"
 
-#: ../roundup/mailgw.py:733
+#: ../roundup/mailgw.py:1023
 #, 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"
+"Ich kann Ihre Nachricht keinem Eintrag in der Datenbank zuordnen - Sie müssen\n"
+"entweder einen vollen Bezeichner angeben (mit Nummer, z. B. \"[issue123]\")\n"
+"oder die Betreffzeile intakt lassen, so daß ich diese zuordnen kann.\n"
+"\n"
+"Die Betreffzeile (Subject) war:\n"
+"   '%(subject)s'\n"
 
-#: ../roundup/mailgw.py:766
+#: ../roundup/mailgw.py:1056
 #, python-format
 msgid ""
 "\n"
@@ -1738,8 +2044,14 @@
 "\n"
 "Subject was: \"%(subject)s\"\n"
 msgstr ""
+"\n"
+"Der in der Betreffzeile Ihre Nachricht bezeichnete Eintrag\n"
+"(\"%(nodeid)s\") existiert nicht.\n"
+"\n"
+"Die Betreffzeile (Subject) war:\n"
+"   '%(subject)s'\n"
 
-#: ../roundup/mailgw.py:794
+#: ../roundup/mailgw.py:1084
 #, python-format
 msgid ""
 "\n"
@@ -1747,8 +2059,12 @@
 "%(mailadmin)s and have them fix the incorrect class specified as:\n"
 "  %(current_class)s\n"
 msgstr ""
+"\n"
+"Das Mail-Gateway ist nicht korrekt eingerichtet. Bitte wenden\n"
+"Sie sich an %(mailadmin)s und bitten Sie um Korrektur der\n"
+"fehlerhaften Klasse:  %(current_class)s\n"
 
-#: ../roundup/mailgw.py:817
+#: ../roundup/mailgw.py:1107
 #, python-format
 msgid ""
 "\n"
@@ -1756,31 +2072,39 @@
 "%(mailadmin)s and have them fix the incorrect properties:\n"
 "  %(errors)s\n"
 msgstr ""
+"\n"
+"Das Mail-Gateway ist nicht korrekt eingerichtet. Bitte wenden\n"
+"Sie sich an %(mailadmin)s und bitten Sie um Korrektur der\n"
+"fehlerhaften Eigenschaften:  %(errors)s\n"
 
-#: ../roundup/mailgw.py:847
+#: ../roundup/mailgw.py:1147
 #, 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"
+"Sie sind kein registrierter Anwender.%(registration_info)s\n"
+"\n"
+"Unbekannte Adresse: %(from_address)s\n"
 
-#: ../roundup/mailgw.py:855
+#: ../roundup/mailgw.py:1155
 msgid "You are not permitted to access this tracker."
-msgstr ""
+msgstr "Sie haben keinen Zugriff auf diesen Tracker."
 
-#: ../roundup/mailgw.py:862
+#: ../roundup/mailgw.py:1162
 #, python-format
 msgid "You are not permitted to edit %(classname)s."
-msgstr ""
+msgstr "Sie sind nicht berechtigt, die Klasse \"%(classname)s\" zu bearbeiten"
 
-#: ../roundup/mailgw.py:866
+#: ../roundup/mailgw.py:1166
 #, python-format
 msgid "You are not permitted to create %(classname)s."
-msgstr ""
+msgstr "Sie sind nicht berechtigt, ein \"%(classname)s\" zu erzeugen"
 
-#: ../roundup/mailgw.py:913
+#: ../roundup/mailgw.py:1213
 #, python-format
 msgid ""
 "\n"
@@ -1789,135 +2113,183 @@
 "\n"
 "Subject was: \"%(subject)s\"\n"
 msgstr ""
+"\n"
+"Es sind Probleme aufgetreten bei der Verarbeitung Ihrer Betreffzeile:\n"
+"- %(errors)s\n"
+"\n"
+"Die Betreffzeile war: \"%(subject)s\"\n"
 
-#: ../roundup/mailgw.py:942
+#: ../roundup/mailgw.py:1266
+msgid ""
+"\n"
+"This tracker has been configured to require all email be PGP signed or\n"
+"encrypted."
+msgstr ""
+"\n"
+"Dieser Tracker wurde konfiguriert, Email-Nachrichten nur PGP-signiert oder\n"
+"verschlüsselt entgegenzunehmen."
+
+#: ../roundup/mailgw.py:1273
 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"
+"Dieser Tracker akzeptiert nur einfache Textnachrichten. Der Mail-Parser konnte\n"
+"keinen entsprechenden Teil (\"text/plain\") finden.\n"
 
-#: ../roundup/mailgw.py:964
+#: ../roundup/mailgw.py:1290
 msgid "You are not permitted to create files."
-msgstr ""
+msgstr "Sie sind nicht berechtigt, Dateien zu erzeugen."
 
-#: ../roundup/mailgw.py:978
+#: ../roundup/mailgw.py:1304
 #, python-format
 msgid "You are not permitted to add files to %(classname)s."
-msgstr ""
+msgstr "Sie sind nicht berechtigt, Dateien zu %(classname)s hinzuzufügen."
 
-#: ../roundup/mailgw.py:996
+#: ../roundup/mailgw.py:1322
 msgid "You are not permitted to create messages."
-msgstr ""
+msgstr "Sie sind nicht berechtigt, Nachrichten zu erzeugen"
 
-#: ../roundup/mailgw.py:1004
+#: ../roundup/mailgw.py:1330
 #, python-format
 msgid ""
 "\n"
 "Mail message was rejected by a detector.\n"
 "%(error)s\n"
 msgstr ""
+"\n"
+"Die Mail-Nachricht wurde von einem Detektor zurückgewiesen.\n"
+"%(error)s\n"
 
-#: ../roundup/mailgw.py:1012
+#: ../roundup/mailgw.py:1338
 #, python-format
 msgid "You are not permitted to add messages to %(classname)s."
-msgstr ""
+msgstr "Sie sind nicht berechtigt, Kommentare zu %(classname)s hinzuzufügen."
 
-#: ../roundup/mailgw.py:1039
+#: ../roundup/mailgw.py:1365
 #, python-format
 msgid "You are not permitted to edit property %(prop)s of class %(classname)s."
 msgstr ""
+"Sie sind nicht berechtigt, die Eigenschaft %(prop)s der Klasse %(classname)s\n"
+"zu bearbeiten."
 
-#: ../roundup/mailgw.py:1047
+#: ../roundup/mailgw.py:1374
 #, python-format
 msgid ""
 "\n"
 "There was a problem with the message you sent:\n"
 "   %(message)s\n"
 msgstr ""
+"\n"
+"Es gab ein Problem mit Ihrer Nachricht:\n"
+"   %(message)s\n"
 
-#: ../roundup/mailgw.py:1069
+#: ../roundup/mailgw.py:1396
 msgid "not of form [arg=value,value,...;arg=value,value,...]"
-msgstr ""
+msgstr "entspricht nicht der Form [arg=wert,wert,...;arg=wert,wert,...]"
 
-#: ../roundup/roundupdb.py:142
+#: ../roundup/roundupdb.py:174
 msgid "files"
 msgstr "Dateien"
 
-#: ../roundup/roundupdb.py:141
+#: ../roundup/roundupdb.py:174
 msgid "messages"
 msgstr "Kommentare"
 
-#: ../roundup/roundupdb.py:141
+#: ../roundup/roundupdb.py:174
 msgid "nosy"
 msgstr "Interessenten"
 
-#: ../roundup/roundupdb.py:141
+#: ../roundup/roundupdb.py:174
 msgid "superseder"
 msgstr "ersetzt durch"
 
-#: ../roundup/roundupdb.py:141
+#: ../roundup/roundupdb.py:174
 msgid "title"
 msgstr "Titel"
 
-#: ../roundup/roundupdb.py:142
+#: ../roundup/roundupdb.py:175
 msgid "assignedto"
 msgstr "zugewiesen"
 
-#: ../roundup/roundupdb.py:142
+#: ../roundup/roundupdb.py:175
+msgid "keyword"
+msgstr "Schlagwort"
+
+#: ../roundup/roundupdb.py:175
 msgid "priority"
-msgstr "Prioriät"
+msgstr "Prioriät"
 
-#: ../roundup/roundupdb.py:142
+#: ../roundup/roundupdb.py:175
 msgid "status"
 msgstr "Status"
 
-#: ../roundup/roundupdb.py:142
-msgid "topic"
-msgstr "Schlagwort"
-# siehe die Anmerkung zu "Schlagwort"
-
-#: ../roundup/roundupdb.py:145
+#: ../roundup/roundupdb.py:178
 msgid "activity"
-msgstr "Aktivität"
+msgstr "Aktivität"
 
 #. following properties are common for all hyperdb classes
 #. they are listed here to keep things in one place
-#: ../roundup/roundupdb.py:145
+#: ../roundup/roundupdb.py:178
 msgid "actor"
 msgstr "Akteur"
 
-#: ../roundup/roundupdb.py:145
+#: ../roundup/roundupdb.py:178
 msgid "creation"
 msgstr "Erstellungsdatum"
 
-#: ../roundup/roundupdb.py:145
+#: ../roundup/roundupdb.py:178
 msgid "creator"
 msgstr "Ersteller"
 
-#: ../roundup/roundupdb.py:304
+#: ../roundup/roundupdb.py:335
 #, python-format
 msgid "New submission from %(authname)s%(authaddr)s:"
-msgstr ""
+msgstr "Neue Nachricht von %(authname)s%(authaddr)s:"
 
-#: ../roundup/roundupdb.py:307
+#: ../roundup/roundupdb.py:338
 #, python-format
 msgid "%(authname)s%(authaddr)s added the comment:"
 msgstr "%(authname)s%(authaddr)s merkte an:"
 
-#: ../roundup/roundupdb.py:310
-msgid "System message:"
-msgstr "System-Nachricht:"
+#: ../roundup/roundupdb.py:341
+#, python-format
+msgid "Change by %(authname)s%(authaddr)s:"
+msgstr "Änderung von %(authname)s%(authaddr)s:"
+
+#: ../roundup/roundupdb.py:361
+#, python-format
+msgid "File '%(filename)s' not attached - you can download it from %(link)s."
+msgstr ""
+"Die Datei '%(filename)s' ist nicht beigefügt - Sie können Sie unter\n"
+"%(link)s herunterladen."
+
+#: ../roundup/roundupdb.py:661
+#, python-format
+msgid ""
+"\n"
+"Now:\n"
+"%(new)s\n"
+"Was:\n"
+"%(old)s"
+msgstr ""
+"\n"
+"Jetzt:\n"
+"%(new)s\n"
+"Vorher:\n"
+"%(old)s"
 
 #: ../roundup/scripts/roundup_demo.py:32
 #, python-format
 msgid "Enter directory path to create demo tracker [%s]: "
-msgstr "Verzeichnis für Tracker-Demo eingeben [%s]: "
+msgstr "Verzeichnis für Tracker-Demo eingeben [%s]: "
 
 #: ../roundup/scripts/roundup_gettext.py:22
 #, python-format
 msgid "Usage: %(program)s <tracker home>"
-msgstr "Verwendung: %(program)s <Tracker Verzeichnis>"
+msgstr "Verwendung: %(program)s <Tracker-Verzeichnis>"
 
 #: ../roundup/scripts/roundup_gettext.py:37
 #, python-format
@@ -1927,8 +2299,7 @@
 #: ../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"
@@ -1962,16 +2333,26 @@
 " specified as:\n"
 "   mailbox /path/to/mailbox\n"
 "\n"
+"In all of the following the username and password can be stored in a\n"
+"~/.netrc file. In this case only the server name need be specified on\n"
+"the command-line.\n"
+"\n"
+"The username and/or password will be prompted for if not supplied on\n"
+"the command-line or in ~/.netrc.\n"
+"\n"
 "POP:\n"
 " In the third case, the gateway reads all messages from the POP server\n"
 " specified and submits each in turn to the roundup.mailgw module. The\n"
 " server is specified as:\n"
 "    pop username:password at server\n"
-" The username and password may be omitted:\n"
+" Alternatively, one can omit one or both of username and password:\n"
 "    pop username at server\n"
 "    pop server\n"
-" are both valid. The username and/or password will be prompted for if\n"
-" not supplied on the command-line.\n"
+" are both valid.\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"
@@ -1991,8 +2372,7 @@
 "    imaps username:password at server [mailbox]\n"
 "\n"
 msgstr ""
-"Verwendung: %(program)s [-v] [[-C Klasse] -S Eigenschaft=Wert]* <Tracker-"
-"Verzeichnis> [Methode]\n"
+"Verwendung: %(program)s [-v] [[-C Klasse] -S Eigenschaft=Wert]* <Tracker-Verzeichnis> [Methode]\n"
 "\n"
 "Optionen:\n"
 " -v: Versionsnummer ausgeben und beenden\n"
@@ -2006,74 +2386,78 @@
 " . mit einem Tracker-Verzeichnis und einem IMAP/IMAPS-Konto.\n"
 "\n"
 "Optional kann mit -C die Klasse des zu erstellenden Eintrags spezifiziert \n"
-"werden. Zudem können Sie mit -S oder --set Eigenschaften der Einträge\n"
+"werden. Zudem können Sie mit -S oder --set Eigenschaften der Einträge\n"
 "als Eigenschaft=Wert[;Eigenschaft=Wert]* setzen, analog zum Roundup-\n"
-"Kommandozeilen Programm, resp. zur Syntax in der Betreffszeile einer Email.\n"
+"Kommandozeilen Programm, resp. zur Syntax in der Betreffszeile einer E-Mail.\n"
 "Voreingestellt ist die Klasse \"msg\", aber auch Klassen wie \"issue\",\n"
-"\"user\" oder \"file\" können verwendet werden.\n"
+"\"user\" oder \"file\" können verwendet werden.\n"
 "\n"
-"Sie können dadurch mehrere Email-Konten für einen Tracker verwenden und\n"
+"Sie können dadurch mehrere E-Mail-Konten für einen Tracker verwenden und\n"
 "unterschiedliche Eintragstypen aus den Nachrichten erstellen.\n"
 "\n"
 "PIPE:\n"
 " Das Mail-Gateway liest eine Nachricht von der Standardeingabe und\n"
-" übergibt sie an das Modul roundup.mailgw.\n"
+" übergibt sie an das Modul roundup.mailgw.\n"
 "\n"
-"UNIX Mailbox:\n"
+"UNIX-Mailbox:\n"
 " Die angegebene Mailbox-Datei wird ausgelesen, und alle Nachrichten werden\n"
-" an das Modul roundup.mailgw übergeben. Nach erfolgreicher Verarbeitung \n"
+" an das Modul roundup.mailgw übergeben. Nach erfolgreicher Verarbeitung \n"
 " wird die Mail-Spooldatei geleert.\n"
-" Die Mailbox-Datei wird folgendermaßen angegeben:  mailbox /pfad/zur/"
-"mailbox\n"
+" Die Mailbox-Datei wird folgendermaßen angegeben:  mailbox /pfad/zur/mailbox\n"
 "\n"
 "POP:\n"
 " Das Gateway liest alle Nachrichten vom POP3-Konto und leitet sie weiter an \n"
 " das Modul roundup.mailgw. \n"
-" Das Konto wird folgendermaßen angegeben:\n"
-"    pop benutzername:paßwort at server\n"
-" Benutzername und Paßwort können weggelassen werden:\n"
+" Das Konto wird folgendermaßen angegeben:\n"
+"    pop benutzername:passwort at server\n"
+" Benutzername und Passwort können weggelassen werden:\n"
 "    pop benutzername at server\n"
 "    pop server\n"
 " In diesem Fall werden die Anmeldungs-Daten zur Laufzeit erfragt.\n"
 "\n"
+"POPS:\n"
+" Mit einem POP3-Server über SSL verbinden; dies erfordert Python 2.4 oder\n"
+" neuer. Argumente wie bei POP.\n"
+"\n"
 "APOP:\n"
 " Wie POP, aber unter Verwendung von authentifiziertem POP:\n"
-"    apop benutzername:paßwort at server\n"
+"    apop benutzername:passwort at server\n"
 "\n"
 "IMAP:\n"
 " Verbindung mit einem IMAP-Server. Die Syntax entspricht der POP-\n"
 " Spezifikation:\n"
-"    imap benutzername:paßwort at server\n"
+"    imap benutzername:passwort at server\n"
 " Um eine andere Mailbox anstelle von \"INBOX\" zu verwenden, benutzen Sie:\n"
-"    imap benutzername:paßwort at server mailbox\n"
+"    imap benutzername:passwort at server mailbox\n"
 "\n"
 "IMAPS:\n"
-" Verbindung zu einem IMAP-Server über eine sichere SSL-Verbindung.\n"
+" Verbindung zu einem IMAP-Server über eine sichere SSL-Verbindung.\n"
 " Die Syntax entspricht der IMAP-Spezifikation:\n"
-"    imaps benutzername:paßwort at server [mailbox]\n"
+"    imaps benutzername:passwort at server [mailbox]\n"
 "\n"
 
-#: ../roundup/scripts/roundup_mailgw.py:147
+#: ../roundup/scripts/roundup_mailgw.py:157
 msgid "Error: not enough source specification information"
-msgstr "Sie haben nicht genügend Angaben zur Mail-Quelle gemacht"
+msgstr "Sie haben nicht genügend Angaben zur E-Mail-Quelle gemacht"
 
-#: ../roundup/scripts/roundup_mailgw.py:157
-msgid "Error: pop specification not valid"
-msgstr "Fehler: pop Optionen ungültig"
+#: ../roundup/scripts/roundup_mailgw.py:186
+#, python-format
+msgid "Error: %s specification not valid"
+msgstr "Fehler: %s-Optionen ungültig"
 
-#: ../roundup/scripts/roundup_mailgw.py:164
-msgid "Error: apop specification not valid"
-msgstr "Fehler: apop Optionen ungültig"
+#: ../roundup/scripts/roundup_mailgw.py:192
+msgid "Error: a later version of python is required"
+msgstr "Fehler: eine neuere Python-Version wird benötigt"
 
-#: ../roundup/scripts/roundup_mailgw.py:178
-msgid ""
-"Error: The source must be either \"mailbox\", \"pop\", \"apop\", \"imap\" or "
-"\"imaps\""
-msgstr ""
-"Fehler: Als Mail-Quelle muß \"mailbox\", \"pop\", \"apop\", \"imap\" oder "
-"\"imaps\" gewählt werden"
+#: ../roundup/scripts/roundup_mailgw.py:203
+msgid "Error: The source must be either \"mailbox\", \"pop\", \"pops\", \"apop\", \"imap\" or \"imaps\""
+msgstr "Fehler: Als E-Mail-Quelle muss \"mailbox\", \"pop\", \"pops\", \"apop\", \"imap\" oder \"imaps\" gewählt werden"
 
-#: ../roundup/scripts/roundup_server.py:140
+#: ../roundup/scripts/roundup_server.py:76
+msgid "WARNING: generating temporary SSL certificate"
+msgstr "WARNUNG: erzeuge temporäres SSL-Zertifikat"
+
+#: ../roundup/scripts/roundup_server.py:253
 msgid ""
 "<html><head><title>Roundup trackers index</title></head>\n"
 "<body><h1>Roundup trackers index</h1><ol>\n"
@@ -2081,55 +2465,52 @@
 "<html><head><title>Roundup Tracker-Liste</title></head>\n"
 "<body><h1>Roundup Tracker-Liste</h1><ol>\n"
 
-#: ../roundup/scripts/roundup_server.py:242
+#: ../roundup/scripts/roundup_server.py:389
 #, python-format
 msgid "Error: %s: %s"
 msgstr "Fehler: %s: %s"
 
-#: ../roundup/scripts/roundup_server.py:252
+#: ../roundup/scripts/roundup_server.py:399
 msgid "WARNING: ignoring \"-g\" argument, not root"
-msgstr ""
-"WARNUNG: die Option \"-g\" wird ignoriert, da Sie nicht Administrator sind"
+msgstr "WARNUNG: die Option \"-g\" wird ignoriert, da Sie nicht Administrator sind"
 
-#: ../roundup/scripts/roundup_server.py:258
+#: ../roundup/scripts/roundup_server.py:405
 msgid "Can't change groups - no grp module"
 msgstr "Die Gruppe kann nicht gewechselt werden - das Modul grp fehlt"
 
-#: ../roundup/scripts/roundup_server.py:267
+#: ../roundup/scripts/roundup_server.py:414
 #, python-format
 msgid "Group %(group)s doesn't exist"
 msgstr "Die Gruppe %(group)s existiert nicht"
 
-#: ../roundup/scripts/roundup_server.py:278
+#: ../roundup/scripts/roundup_server.py:425
 msgid "Can't run as root!"
-msgstr "Dieser Prozeß kann nicht unter dem Administrator-Konto (\"root\") laufen!"
+msgstr "Dieser Prozess kann nicht unter dem Administrator-Konto (\"root\") laufen!"
 
-#: ../roundup/scripts/roundup_server.py:281
+#: ../roundup/scripts/roundup_server.py:428
 msgid "WARNING: ignoring \"-u\" argument, not root"
-msgstr ""
-"WARNUNG: die Option \"-u\" wird ignoriert, da Sie nicht Administrator sind"
+msgstr "WARNUNG: die Option \"-u\" wird ignoriert, da Sie nicht Administrator sind"
 
-#: ../roundup/scripts/roundup_server.py:286
+#: ../roundup/scripts/roundup_server.py:434
 msgid "Can't change users - no pwd module"
 msgstr "Der Benutzer kann nicht gewechselt werden - das Modul pwd fehlt"
 
-#: ../roundup/scripts/roundup_server.py:295
+#: ../roundup/scripts/roundup_server.py:443
 #, python-format
 msgid "User %(user)s doesn't exist"
 msgstr "Der Benutzer %(user)s existiert nicht"
 
-#: ../roundup/scripts/roundup_server.py:417
+#: ../roundup/scripts/roundup_server.py:592
 #, python-format
 msgid "Multiprocess mode \"%s\" is not available, switching to single-process"
-msgstr "Der Multiprozeß-Modus \"%s\" ist nicht verfügbar, Einprozeß-Modus"
-"aktiviert"
+msgstr "Der Multiprozessmodus \"%s\" ist nicht verfügbar, Einprozessmodus aktiviert"
 
-#: ../roundup/scripts/roundup_server.py:440
+#: ../roundup/scripts/roundup_server.py:620
 #, python-format
 msgid "Unable to bind to port %s, port already in use."
 msgstr "Start des Servers auf Port %s schlug fehl. Port bereits verwendet."
 
-#: ../roundup/scripts/roundup_server.py:507
+#: ../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"
@@ -2142,10 +2523,10 @@
 "               Um den Roundup-Server als Windows Service zu starten,\n"
 "               benutzen Sie eine Server-Konfiguration, in der die Tracker-\n"
 "               Instanzen angegeben werden.\n"
-"               Zudem müssen Sie die Logfile-Option aktivieren.\n"
+"               Zudem müssen Sie die Logfile-Option aktivieren.\n"
 "               \"roundup-server -c help\" zeigt eine weitere Hilfe zum Thema."
 
-#: ../roundup/scripts/roundup_server.py:514
+#: ../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"
@@ -2153,13 +2534,13 @@
 "               to the file indicated by PIDfile. The -l option *must* be\n"
 "               specified if -d is used."
 msgstr ""
-" -u <UID>      Startet den Roundup-Server mit dieser Benutzer-Nummer\n"
-" -g <GID>      Startet den Roundup-Server mit dieser Gruppen-Nummer\n"
-" -d <PIDDatei> Startet den Server als Hintergrundprozeß und schreibt\n"
-"               die Prozeß-ID in die Datei PIDDatei.\n"
-"               Die Option -l muß dann auch angegeben werden."
+" -u <UID>      Startet den Roundup-Server mit dieser Benutzernummer\n"
+" -g <GID>      Startet den Roundup-Server mit dieser Gruppennummer\n"
+" -d <PIDDatei> Startet den Server als Hintergrundprozess und schreibt\n"
+"               die Prozess-ID in die Datei PIDDatei.\n"
+"               Die Option -l muss dann auch angegeben werden."
 
-#: ../roundup/scripts/roundup_server.py:521
+#: ../roundup/scripts/roundup_server.py:702
 #, python-format
 msgid ""
 "%(message)sUsage: roundup-server [options] [name=tracker home]*\n"
@@ -2172,8 +2553,10 @@
 " -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"
@@ -2213,8 +2596,7 @@
 "   pairs on the command-line. Make sure the name part doesn't include\n"
 "   any url-unsafe characters like spaces, as these confuse IE.\n"
 msgstr ""
-"%(message)s"
-"Benutzung: roundup-server [Optionen] [Tracker-Name=Tracker-Verzeichnis]*\n"
+"%(message)sBenutzung: roundup-server [Optionen] [Tracker-Name=Tracker-Verzeichnis]*\n"
 "\n"
 "Optionen:\n"
 " -v            Versionsnummer ausgeben und beenden\n"
@@ -2224,9 +2606,9 @@
 " -n            Hostnamen des Serverprozesses bestimmen\n"
 " -p            Port bestimmen (Voreinstellung: %(port)s)\n"
 " -l            Logdatei bestimmen (anstelle \"stderr\" / \"stdout\")\n"
-" -N            Domainnamen in der Logdatei auflösen (viel langsamer)\n"
-" -t <Modus>    Multiprozeß-Modus (Voreinstellung: %(mp_def)s).\n"
-"               Verfügbare Modi: %(mp_types)s.\n"
+" -N            Domainnamen in der Logdatei auflösen (viel langsamer)\n"
+" -t <Modus>    Multiprozessmodus (Voreinstellung: %(mp_def)s).\n"
+"               Verfügbare Modi: %(mp_types)s.\n"
 "%(os_part)s\n"
 "\n"
 "Lange Optionen:\n"
@@ -2234,7 +2616,7 @@
 " --help             Diese Hilfe ausgeben und beenden\n"
 " --save-config      Konfiguration erstellen oder aktualiseren und beenden\n"
 " --config <fname>   Konfiguration <Datei> verwenden\n"
-" Die Einstellungen in der Sektion [main] der Konfigurationsdatei können Sie\n"
+" Die Einstellungen in der Sektion [main] der Konfigurationsdatei können Sie\n"
 " auch in der Form --<Name>=<Wert> angegeben.\n"
 "\n"
 "Beispiele:\n"
@@ -2253,51 +2635,50 @@
 "Konfigurations-Format:\n"
 "   Roundup Server benutzt das standardisierte .ini Format.\n"
 "   Konfigurationen, welche mit 'roundup-server -S' erstellt werden, \n"
-"   enthalten detaillierte Erklärungen zu jeder Option. Bitte konsultieren\n"
-"   Sie diese Datei für weitere Angaben.\n"
+"   enthalten detaillierte Erklärungen zu jeder Option. Bitte konsultieren\n"
+"   Sie diese Datei für weitere Angaben.\n"
 "\n"
 "Tracker-Name=Tracker-Verzeichnis:\n"
 "   Gibt an, welche Tracker-Instanz(en) verwendet werden. Der Tracker-Name\n"
 "   bestimmt den URL-Pfad im Web. Das Tracker-Verzeichnis gibt an, in \n"
 "   welchem Verzeichnis die Tracker-Konfiguration gespeichert wurde.\n"
-"   Sie können mehrere Tracker-Instanzen auf der Kommandozeile angeben oder\n"
+"   Sie können mehrere Tracker-Instanzen auf der Kommandozeile angeben oder\n"
 "   alternativ die Variable TRACKER_HOME in der roundup-server Datei \n"
 "   anpassen. \n"
 "   ACHTUNG: Der Tracker-Name darf keine Sonderzeichen enthalten, welche in \n"
-"   URLs Probleme bereiten könnten. Am besten verwenden Sie nur Buchstaben, \n"
+"   URLs Probleme bereiten könnten. Am besten verwenden Sie nur Buchstaben, \n"
 "   Zahlen und \"-_\".\n"
 
-#: ../roundup/scripts/roundup_server.py:669
+#: ../roundup/scripts/roundup_server.py:860
 msgid "Instances must be name=home"
-msgstr "Instanzen müssen als Tracker-Name=Tracker-Verzeichnis angegeben werden"
+msgstr "Instanzen müssen als Tracker-Name=Tracker-Verzeichnis angegeben werden"
 
-#: ../roundup/scripts/roundup_server.py:683
+#: ../roundup/scripts/roundup_server.py:874
 #, python-format
 msgid "Configuration saved to %s"
 msgstr "Konfiguration in der Datei %s gespeichert"
 
-#: ../roundup/scripts/roundup_server.py:694
+#: ../roundup/scripts/roundup_server.py:892
 msgid "Sorry, you can't run the server as a daemon on this Operating System"
-msgstr ""
-"Auf diesem Betriebssystem kann der Server nicht als Hintergrundprozeß laufen"
+msgstr "Auf diesem Betriebssystem kann der Server nicht als Hintergrundprozess laufen"
 
-#: ../roundup/scripts/roundup_server.py:706
+#: ../roundup/scripts/roundup_server.py:907
 #, python-format
 msgid "Roundup server started on %(HOST)s:%(PORT)s"
 msgstr "Der Roundup-Server wurde unter %(HOST)s:%(PORT)s gestartet"
 
-#: ../templates/classic/html/_generic.collision.html:4
-#: ../templates/minimal/html/_generic.collision.html:4
+#: ../share/roundup/templates/classic/html/_generic.collision.html:4
+#: ../share/roundup/templates/minimal/html/_generic.collision.html:4
 msgid "${class} Edit Collision - ${tracker}"
 msgstr "Kollision bei der Bearbeitung - ${tracker}"
 
-#: ../templates/classic/html/_generic.collision.html:7
-#: ../templates/minimal/html/_generic.collision.html:7
+#: ../share/roundup/templates/classic/html/_generic.collision.html:7
+#: ../share/roundup/templates/minimal/html/_generic.collision.html:7
 msgid "${class} Edit Collision"
 msgstr "Kollision bei der Bearbeitung"
 
-#: ../templates/classic/html/_generic.collision.html:14
-#: ../templates/minimal/html/_generic.collision.html:14
+#: ../share/roundup/templates/classic/html/_generic.collision.html:14
+#: ../share/roundup/templates/minimal/html/_generic.collision.html:14
 msgid ""
 "\n"
 "  There has been a collision. Another user updated this node\n"
@@ -2305,1107 +2686,1114 @@
 "  the node and review your edits.\n"
 msgstr ""
 "\n"
-"  Eine Kollision wurde festgestellt. Während Ihrer Bearbeitung\n"
-"  hat ein anderer Benutzer diesen Eintrag aktualisiert. Bitte   <a "
-"href='${context}'>laden Sie diese Seite neu</a> \n"
-"  und fügen Sie Ihre Änderungen erneut ein.\n"
-
-#: ../templates/classic/html/_generic.help.html:9
-#: ../templates/minimal/html/_generic.help.html:9
-msgid "${property} help - ${tracker}"
-msgstr "Hilfe zu \"${property}\" - ${tracker}"
+"  Eine Kollision wurde festgestellt. Während Ihrer Bearbeitung\n"
+"  hat ein anderer Benutzer diesen Eintrag aktualisiert. Bitte   <a href='${context}'>laden Sie diese Seite neu</a> \n"
+"  und fügen Sie Ihre Änderungen erneut ein.\n"
+
+#: ../share/roundup/templates/classic/html/_generic.help-empty.html:6
+msgid "Please specify your search parameters!"
+msgstr "Bitte geben Sie Ihre Suchparameter an!"
+
+#: ../share/roundup/templates/classic/html/_generic.help-list.html:20
+#: ../share/roundup/templates/classic/html/_generic.index.html:14
+#: ../share/roundup/templates/classic/html/_generic.item.html:12
+#: ../share/roundup/templates/classic/html/file.item.html:9
+#: ../share/roundup/templates/classic/html/issue.index.html:16
+#: ../share/roundup/templates/classic/html/issue.item.html:28
+#: ../share/roundup/templates/classic/html/msg.item.html:26
+#: ../share/roundup/templates/classic/html/user.index.html:9
+#: ../share/roundup/templates/classic/html/user.item.html:35
+#: ../share/roundup/templates/minimal/html/_generic.index.html:14
+#: ../share/roundup/templates/minimal/html/_generic.item.html:12
+#: ../share/roundup/templates/minimal/html/user.index.html:9
+#: ../share/roundup/templates/minimal/html/user.item.html:35
+#: ../share/roundup/templates/minimal/html/user.register.html:14
+msgid "You are not allowed to view this page."
+msgstr "Sie sind nicht berechtigt, diese Seite anzuzeigen."
 
-#: ../templates/classic/html/_generic.help.html:30
-#: ../templates/minimal/html/_generic.help.html:30
+#: ../share/roundup/templates/classic/html/_generic.help-list.html:34
+msgid "1..25 out of 50"
+msgstr "1..25 von 50"
+
+#: ../share/roundup/templates/classic/html/_generic.help-search.html:9
+msgid "Generic template ${template} or version for class ${classname} is not yet implemented"
+msgstr "Die generische Vorlage ${template} bzw. die Version für die Klasse ${classname} ist noch nicht implementiert"
+
+#: ../share/roundup/templates/classic/html/_generic.help-submit.html:57
+#: ../share/roundup/templates/classic/html/_generic.help.html:31
+#: ../share/roundup/templates/minimal/html/_generic.help.html:31
 msgid " Cancel "
 msgstr " Abbrechen "
 
-#: ../templates/classic/html/_generic.help.html:33
-#: ../templates/minimal/html/_generic.help.html:33
+#: ../share/roundup/templates/classic/html/_generic.help-submit.html:63
+#: ../share/roundup/templates/classic/html/_generic.help.html:34
+#: ../share/roundup/templates/minimal/html/_generic.help.html:34
 msgid " Apply "
 msgstr " Anwenden "
 
-#: ../templates/classic/html/_generic.help.html:40
-#: ../templates/classic/html/issue.index.html:67
-#: ../templates/minimal/html/_generic.help.html:40
+#: ../share/roundup/templates/classic/html/_generic.help.html:9
+#: ../share/roundup/templates/classic/html/user.help.html:13
+#: ../share/roundup/templates/minimal/html/_generic.help.html:9
+msgid "${property} help - ${tracker}"
+msgstr "Hilfe zu \"${property}\" - ${tracker}"
+
+#: ../share/roundup/templates/classic/html/_generic.help.html:41
+#: ../share/roundup/templates/classic/html/help.html:21
+#: ../share/roundup/templates/classic/html/issue.index.html:81
+#: ../share/roundup/templates/minimal/html/_generic.help.html:41
 msgid "&lt;&lt; previous"
-msgstr "&lt;&lt; zurück"
+msgstr "&lt;&lt; zurück"
 
-#: ../templates/classic/html/_generic.help.html:50
-#: ../templates/classic/html/issue.index.html:75
-#: ../templates/minimal/html/_generic.help.html:50
+#: ../share/roundup/templates/classic/html/_generic.help.html:53
+#: ../share/roundup/templates/classic/html/help.html:28
+#: ../share/roundup/templates/classic/html/issue.index.html:89
+#: ../share/roundup/templates/minimal/html/_generic.help.html:53
 msgid "${start}..${end} out of ${total}"
 msgstr "${start}..${end} von ${total}"
 
-#: ../templates/classic/html/_generic.help.html:54
-#: ../templates/classic/html/issue.index.html:78
-#: ../templates/minimal/html/_generic.help.html:54
+#: ../share/roundup/templates/classic/html/_generic.help.html:57
+#: ../share/roundup/templates/classic/html/help.html:32
+#: ../share/roundup/templates/classic/html/issue.index.html:92
+#: ../share/roundup/templates/minimal/html/_generic.help.html:57
 msgid "next &gt;&gt;"
 msgstr "weiter &gt;&gt;"
 
-#: ../templates/classic/html/_generic.index.html:6
-#: ../templates/classic/html/_generic.item.html:4
-#: ../templates/minimal/html/_generic.index.html:6
-#: ../templates/minimal/html/_generic.item.html:4
+#: ../share/roundup/templates/classic/html/_generic.index.html:6
+#: ../share/roundup/templates/classic/html/_generic.item.html:4
+#: ../share/roundup/templates/minimal/html/_generic.index.html:6
+#: ../share/roundup/templates/minimal/html/_generic.item.html:4
 msgid "${class} editing - ${tracker}"
 msgstr "Klasse bearbeiten - ${tracker}"
 
-#: ../templates/classic/html/_generic.index.html:9
-#: ../templates/classic/html/_generic.item.html:7
-#: ../templates/minimal/html/_generic.index.html:9
-#: ../templates/minimal/html/_generic.item.html:7
+#: ../share/roundup/templates/classic/html/_generic.index.html:9
+#: ../share/roundup/templates/classic/html/_generic.item.html:7
+#: ../share/roundup/templates/minimal/html/_generic.index.html:9
+#: ../share/roundup/templates/minimal/html/_generic.item.html:7
 msgid "${class} editing"
 msgstr "\"${class}\" bearbeiten"
 
-#: ../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:18
-#: ../templates/minimal/html/user.register.html:14
-msgid "You are not allowed to view this page."
-msgstr "Sie sind nicht berechtigt, diese Seite anzuzeigen."
-
-#: ../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>"
-msgstr ""
-"<p class=\"form-help\">Sie können hier die Einträge der Klasse \"${classname}\" "
-"bearbeiten. <strong>Hinweise:</strong></p>"
-"<ul>"
-"<li> Kommata, Zeilenumbrüche und "
-"Anführungszeichen (\") mit Vorsicht verwenden:"
-"<ul><li> Kommata und Zeilenumbrüche "
-"dürfen nur in Anführungszeichen (\") verwendet werden."
-"<li> Um Anführungszeichen in "
-"Werten zu verwenden, müssen sie verdoppelt werden (<q><tt>\"\"</tt></q>)</ul>"
-"<li>Mehrfachlinks werden durch Doppelpunkt (<q><tt>:</tt></q>) getrennt "
-"<tt>(... ,\"eins:zwei:drei\", ...)</tt>"
-"<li> Einträge können "
-"gelöscht werden, indem Sie Zeilen entfernen."
-"<li>Für neue Einträge fügen Sie Zeilen ein; "
-"geben Sie bei der ID-Spalte ein großes <tt>X</tt> an. </ul><p>"
+#: ../share/roundup/templates/classic/html/_generic.index.html:19
+#: ../share/roundup/templates/classic/html/_generic.item.html:16
+#: ../share/roundup/templates/classic/html/file.item.html:13
+#: ../share/roundup/templates/classic/html/issue.index.html:20
+#: ../share/roundup/templates/classic/html/issue.item.html:32
+#: ../share/roundup/templates/classic/html/msg.item.html:30
+#: ../share/roundup/templates/classic/html/user.index.html:13
+#: ../share/roundup/templates/classic/html/user.item.html:39
+#: ../share/roundup/templates/minimal/html/_generic.index.html:19
+#: ../share/roundup/templates/minimal/html/_generic.item.html:17
+#: ../share/roundup/templates/minimal/html/user.index.html:13
+#: ../share/roundup/templates/minimal/html/user.item.html:39
+#: ../share/roundup/templates/minimal/html/user.register.html:17
+msgid "Please login with your username and password."
+msgstr "Bitte melden Sie sich an mit Ihrem Benutzernamen und Paßwort"
+
+#: ../share/roundup/templates/classic/html/_generic.index.html:28
+#: ../share/roundup/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 "<p class=\"form-help\">Sie können hier die Einträge der Klasse \"${classname}\" bearbeiten. <strong>Hinweise:</strong></p><ul><li> Kommata, Zeilenumbrüche und Anführungszeichen (\") mit Vorsicht verwenden:<ul><li> Kommata und Zeilenumbrüche dürfen nur in Anführungszeichen (\") verwendet werden.<li> Um Anführungszeichen in Werten zu verwenden, müssen sie verdoppelt werden (<q><tt>\"\"</tt></q>)</ul><li>Mehrfachlinks werden durch Doppelpunkt (<q><tt>:</tt></q>) getrennt <tt>(... ,\"eins:zwei:drei\", ...)</tt><li> Einträge können gelöscht werden, indem Sie Zeilen entfernen.<li>Für neue Einträge fügen Sie Zeilen ein; geben Sie bei der ID-Spalte ein großes <tt>X</tt> an. </ul><p>"
 
-#: ../templates/classic/html/_generic.index.html:44
-#: ../templates/minimal/html/_generic.index.html:44
+#: ../share/roundup/templates/classic/html/_generic.index.html:50
+#: ../share/roundup/templates/minimal/html/_generic.index.html:50
 msgid "Edit Items"
-msgstr "Einträge bearbeiten"
+msgstr "Einträge bearbeiten"
 
-#: ../templates/classic/html/file.index.html:4
+#: ../share/roundup/templates/classic/html/file.index.html:4
 msgid "List of files - ${tracker}"
 msgstr "Dateiliste - ${tracker}"
 
-#: ../templates/classic/html/file.index.html:5
+#: ../share/roundup/templates/classic/html/file.index.html:5
 msgid "List of files"
 msgstr "Dateiliste"
 
-#: ../templates/classic/html/file.index.html:10
+#: ../share/roundup/templates/classic/html/file.index.html:10
 msgid "Download"
 msgstr "Herunterladen"
 
-#: ../templates/classic/html/file.index.html:11
-#: ../templates/classic/html/file.item.html:23
-#: ../templates/classic/html/file.item.html:51
+#: ../share/roundup/templates/classic/html/file.index.html:11
+#: ../share/roundup/templates/classic/html/file.item.html:27
 msgid "Content Type"
 msgstr "Inhaltstyp"
 
-#: ../templates/classic/html/file.index.html:12
+#: ../share/roundup/templates/classic/html/file.index.html:12
 msgid "Uploaded By"
 msgstr "Hochgeladen von"
 
-#: ../templates/classic/html/file.index.html:13
-#: ../templates/classic/html/msg.item.html:38
+#: ../share/roundup/templates/classic/html/file.index.html:13
+#: ../share/roundup/templates/classic/html/msg.item.html:48
 msgid "Date"
 msgstr "Datum"
 
-#: ../templates/classic/html/file.item.html:2
+#: ../share/roundup/templates/classic/html/file.item.html:2
 msgid "File display - ${tracker}"
 msgstr "Datei anzeigen - ${tracker}"
 
-#: ../templates/classic/html/file.item.html:4
+#: ../share/roundup/templates/classic/html/file.item.html:4
 msgid "File display"
 msgstr "Datei anzeigen"
 
-#: ../templates/classic/html/file.item.html:18
-#: ../templates/classic/html/user.item.html:39
-#: ../templates/classic/html/user.register.html:17
+#: ../share/roundup/templates/classic/html/file.item.html:23
+#: ../share/roundup/templates/classic/html/user.register.html:17
 msgid "Name"
 msgstr "Name"
 
-#: ../templates/classic/html/file.item.html:41
+#: ../share/roundup/templates/classic/html/file.item.html:45
 msgid "download"
 msgstr "herunterladen"
 
-#: ../templates/classic/html/home.classlist.html:2
-#: ../templates/minimal/html/home.classlist.html:2
+#: ../share/roundup/templates/classic/html/home.classlist.html:2
+#: ../share/roundup/templates/minimal/html/home.classlist.html:2
 msgid "List of classes - ${tracker}"
 msgstr "Klassenliste - ${tracker}"
 
-#: ../templates/classic/html/home.classlist.html:4
-#: ../templates/minimal/html/home.classlist.html:4
+#: ../share/roundup/templates/classic/html/home.classlist.html:4
+#: ../share/roundup/templates/minimal/html/home.classlist.html:4
 msgid "List of classes"
 msgstr "Klassenliste"
 
-#: ../templates/classic/html/issue.index.html:4
-msgid "List of issues - ${tracker}"
-msgstr "Aufgabenliste - ${tracker}"
-
-#: ../templates/classic/html/issue.index.html:6
+#: ../share/roundup/templates/classic/html/issue.index.html:4
+#: ../share/roundup/templates/classic/html/issue.index.html:10
 msgid "List of issues"
 msgstr "Aufgabenliste"
 
-#: ../templates/classic/html/issue.index.html:6
-msgid "List of issues - ${query} - ${tracker}"
-msgstr "Aufgabenliste - ${query} - ${tracker}"
-
-#: ../templates/classic/html/issue.index.html:6
-msgid "List of issues - ${query}"
-msgstr "Aufgabenliste - ${query}"
-
-#: ../templates/classic/html/issue.index.html:17
-#: ../templates/classic/html/issue.item.html:38
+#: ../share/roundup/templates/classic/html/issue.index.html:27
+#: ../share/roundup/templates/classic/html/issue.item.html:49
 msgid "Priority"
-msgstr "Priorität"
+msgstr "Priorität"
 
-#: ../templates/classic/html/issue.index.html:18
+#: ../share/roundup/templates/classic/html/issue.index.html:28
 msgid "ID"
 msgstr "ID"
 
-#: ../templates/classic/html/issue.index.html:19
+#: ../share/roundup/templates/classic/html/issue.index.html:29
 msgid "Creation"
 msgstr "Erstellungsdatum"
 
-#: ../templates/classic/html/issue.index.html:20
+#: ../share/roundup/templates/classic/html/issue.index.html:30
 msgid "Activity"
-msgstr "Aktivität"
+msgstr "Aktivität"
 
-#: ../templates/classic/html/issue.index.html:21
+#: ../share/roundup/templates/classic/html/issue.index.html:31
 msgid "Actor"
 msgstr "Akteur"
 
-#: ../templates/classic/html/issue.index.html:22
-msgid "Topic"
+#: ../share/roundup/templates/classic/html/issue.index.html:32
+#: ../share/roundup/templates/classic/html/keyword.item.html:37
+msgid "Keyword"
 msgstr "Schlagwort"
-# Schlagwort <-> verschlagworten. Fachbegriff; besser als Stichwort oder Thema!
 
-#: ../templates/classic/html/issue.index.html:23
-#: ../templates/classic/html/issue.item.html:33
+#: ../share/roundup/templates/classic/html/issue.index.html:33
+#: ../share/roundup/templates/classic/html/issue.item.html:44
 msgid "Title"
 msgstr "Titel"
 
-#: ../templates/classic/html/issue.index.html:24
-#: ../templates/classic/html/issue.item.html:40
+#: ../share/roundup/templates/classic/html/issue.index.html:34
+#: ../share/roundup/templates/classic/html/issue.item.html:51
 msgid "Status"
 msgstr "Status"
 
-#: ../templates/classic/html/issue.index.html:25
+#: ../share/roundup/templates/classic/html/issue.index.html:35
 msgid "Creator"
 msgstr "Ersteller"
 
-#: ../templates/classic/html/issue.index.html:26
+#: ../share/roundup/templates/classic/html/issue.index.html:36
 msgid "Assigned&nbsp;To"
 msgstr "Zugewiesen"
 
-#: ../templates/classic/html/issue.index.html:90
+#: ../share/roundup/templates/classic/html/issue.index.html:105
 msgid "Download as CSV"
 msgstr "Als CSV-Datei herunterladen"
 
-#: ../templates/classic/html/issue.index.html:98
+#: ../share/roundup/templates/classic/html/issue.index.html:115
 msgid "Sort on:"
 msgstr "Sortieren:"
 
-#: ../templates/classic/html/issue.index.html:101
-#: ../templates/classic/html/issue.index.html:118
+#: ../share/roundup/templates/classic/html/issue.index.html:119
+#: ../share/roundup/templates/classic/html/issue.index.html:140
 msgid "- nothing -"
 msgstr "- nichts -"
 
-#: ../templates/classic/html/issue.index.html:109
-#: ../templates/classic/html/issue.index.html:126
+#: ../share/roundup/templates/classic/html/issue.index.html:127
+#: ../share/roundup/templates/classic/html/issue.index.html:148
 msgid "Descending:"
 msgstr "Absteigend:"
 
-#: ../templates/classic/html/issue.index.html:115
+#: ../share/roundup/templates/classic/html/issue.index.html:136
 msgid "Group on:"
 msgstr "Gruppieren:"
 
-#: ../templates/classic/html/issue.index.html:132
+#: ../share/roundup/templates/classic/html/issue.index.html:155
 msgid "Redisplay"
 msgstr "Aktualisieren"
 
-#: ../templates/classic/html/issue.item.html:7
+#: ../share/roundup/templates/classic/html/issue.item.html:7
 msgid "Issue ${id}: ${title} - ${tracker}"
 msgstr "Aufgabe ${id}: ${title} - ${tracker}"
 
-#: ../templates/classic/html/issue.item.html:10
+#: ../share/roundup/templates/classic/html/issue.item.html:10
 msgid "New Issue - ${tracker}"
 msgstr "Neue Aufgabe - ${tracker}"
 
-#: ../templates/classic/html/issue.item.html:14
+#: ../share/roundup/templates/classic/html/issue.item.html:14
 msgid "New Issue"
 msgstr "Neue Aufgabe"
 
-#: ../templates/classic/html/issue.item.html:16
+#: ../share/roundup/templates/classic/html/issue.item.html:16
 msgid "New Issue Editing"
 msgstr "Neue Aufgabe bearbeiten"
 
-#: ../templates/classic/html/issue.item.html:19
+#: ../share/roundup/templates/classic/html/issue.item.html:19
 msgid "Issue${id}"
 msgstr "Aufgabe${id}"
 
-#: ../templates/classic/html/issue.item.html:22
+#: ../share/roundup/templates/classic/html/issue.item.html:22
 msgid "Issue${id} Editing"
 msgstr "Aufgabe ${id} bearbeiten"
 
-#: ../templates/classic/html/issue.item.html:45
+#: ../share/roundup/templates/classic/html/issue.item.html:56
 msgid "Superseder"
 msgstr "Ersetzt durch"
 
-#: ../templates/classic/html/issue.item.html:50
-msgid "View: ${link}"
-msgstr "Anzeigen: ${link}"
+#: ../share/roundup/templates/classic/html/issue.item.html:61
+msgid "View:"
+msgstr "Anzeigen:"
 
-#: ../templates/classic/html/issue.item.html:54
+#: ../share/roundup/templates/classic/html/issue.item.html:67
 msgid "Nosy List"
 msgstr "Interessenten"
 
-#: ../templates/classic/html/issue.item.html:63
+#: ../share/roundup/templates/classic/html/issue.item.html:76
 msgid "Assigned To"
 msgstr "Zugewiesen"
 
-#: ../templates/classic/html/issue.item.html:65
-msgid "Topics"
-msgstr "Schlagwörter"
-# siehe die Anmerkung zu "Schlagwort"
+#: ../share/roundup/templates/classic/html/issue.item.html:78
+#: ../share/roundup/templates/classic/html/page.html:103
+#: ../share/roundup/templates/minimal/html/page.html:102
+msgid "Keywords"
+msgstr "Schlagwörter"
 
-#: ../templates/classic/html/issue.item.html:73
+#: ../share/roundup/templates/classic/html/issue.item.html:86
 msgid "Change Note"
-msgstr "Änderungsnotiz"
+msgstr "Änderungsnotiz"
 
-#: ../templates/classic/html/issue.item.html:81
+#: ../share/roundup/templates/classic/html/issue.item.html:94
 msgid "File"
 msgstr "Datei"
 
-#: ../templates/classic/html/issue.item.html:99
+#: ../share/roundup/templates/classic/html/issue.item.html:106
 msgid "Make a copy"
 msgstr "kopieren"
 
-#: ../templates/classic/html/issue.item.html:100
-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>Achtung:&nbsp;</td> <th class=\"required"
-"\">Fett markierte</th> <td>&nbsp;Felder sind immer auszufüllen. </td> </tr> "
-"</table>"
-
-#: ../templates/classic/html/issue.item.html:114
-msgid ""
-"Created on <b>${creation}</b> by <b>${creator}</b>, last changed <b>"
-"${activity}</b> by <b>${actor}</b>."
-msgstr ""
-"Erstellt am <b>${creation}</b> durch <b>${creator}</b>, geändert am <b>"
-"${activity}</b> durch <b>${actor}</b>."
+#: ../share/roundup/templates/classic/html/issue.item.html:114
+#: ../share/roundup/templates/classic/html/user.item.html:153
+#: ../share/roundup/templates/classic/html/user.register.html:69
+#: ../share/roundup/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>Achtung:&nbsp;</td> <th class=\"required\">Fett markierte</th> <td>&nbsp;Felder sind immer auszufüllen. </td> </tr> </table>"
+
+#: ../share/roundup/templates/classic/html/issue.item.html:128
+#, fuzzy
+msgid "Created on ${creation} by ${creator}, last changed ${activity} by ${actor}."
+msgstr "Erstellt am <b>${creation}</b> durch <b>${creator}</b>, geändert am <b>${activity}</b> durch <b>${actor}</b>."
 
-#: ../templates/classic/html/issue.item.html:118
-#: ../templates/classic/html/msg.item.html:51
+#: ../share/roundup/templates/classic/html/issue.item.html:132
+#: ../share/roundup/templates/classic/html/msg.item.html:61
 msgid "Files"
 msgstr "Dateien"
 
-#: ../templates/classic/html/issue.item.html:120
-#: ../templates/classic/html/msg.item.html:53
+#: ../share/roundup/templates/classic/html/issue.item.html:134
+#: ../share/roundup/templates/classic/html/msg.item.html:63
 msgid "File name"
 msgstr "Dateiname"
 
-#: ../templates/classic/html/issue.item.html:121
-#: ../templates/classic/html/msg.item.html:54
+#: ../share/roundup/templates/classic/html/issue.item.html:135
+#: ../share/roundup/templates/classic/html/msg.item.html:64
 msgid "Uploaded"
 msgstr "Hochgeladen"
 
-#: ../templates/classic/html/issue.item.html:122
+#: ../share/roundup/templates/classic/html/issue.item.html:136
 msgid "Type"
 msgstr "Typ"
 
-#: ../templates/classic/html/issue.item.html:123
-#: ../templates/classic/html/query.edit.html:30
+#: ../share/roundup/templates/classic/html/issue.item.html:137
+#: ../share/roundup/templates/classic/html/query.edit.html:30
 msgid "Edit"
 msgstr "bearbeiten"
 
-#: ../templates/classic/html/issue.item.html:124
+#: ../share/roundup/templates/classic/html/issue.item.html:138
 msgid "Remove"
 msgstr "verbergen"
 
-#: ../templates/classic/html/issue.item.html:144
-#: ../templates/classic/html/issue.item.html:165
-#: ../templates/classic/html/query.edit.html:50
+#: ../share/roundup/templates/classic/html/issue.item.html:158
+#: ../share/roundup/templates/classic/html/issue.item.html:179
+#: ../share/roundup/templates/classic/html/query.edit.html:50
 msgid "remove"
 msgstr "verbergen"
 
-#: ../templates/classic/html/issue.item.html:151
-#: ../templates/classic/html/msg.index.html:9
+#: ../share/roundup/templates/classic/html/issue.item.html:165
+#: ../share/roundup/templates/classic/html/msg.index.html:9
 msgid "Messages"
 msgstr "Kommentare"
 
-#: ../templates/classic/html/issue.item.html:155
+#: ../share/roundup/templates/classic/html/issue.item.html:169
 msgid "msg${id} (view)"
 msgstr "Kommentar msg${id} (betrachten)"
 
-#: ../templates/classic/html/issue.item.html:156
+#: ../share/roundup/templates/classic/html/issue.item.html:170
 msgid "Author: ${author}"
 msgstr "Autor: ${author}"
 
-#: ../templates/classic/html/issue.item.html:158
+#: ../share/roundup/templates/classic/html/issue.item.html:172
 msgid "Date: ${date}"
 msgstr "Datum: ${date}"
 
-#: ../templates/classic/html/issue.search.html:2
+#: ../share/roundup/templates/classic/html/issue.search.html:2
 msgid "Issue searching - ${tracker}"
 msgstr "Aufgaben suchen - ${tracker}"
 
-#: ../templates/classic/html/issue.search.html:4
+#: ../share/roundup/templates/classic/html/issue.search.html:4
 msgid "Issue searching"
 msgstr "Aufgaben suchen"
 
-#: ../templates/classic/html/issue.search.html:25
+#: ../share/roundup/templates/classic/html/issue.search.html:31
 msgid "Filter on"
 msgstr "Filtern"
 
-#: ../templates/classic/html/issue.search.html:26
+#: ../share/roundup/templates/classic/html/issue.search.html:32
 msgid "Display"
 msgstr "anzeigen"
 
-#: ../templates/classic/html/issue.search.html:27
+#: ../share/roundup/templates/classic/html/issue.search.html:33
 msgid "Sort on"
 msgstr "sortieren"
 
-#: ../templates/classic/html/issue.search.html:28
+#: ../share/roundup/templates/classic/html/issue.search.html:34
 msgid "Group on"
 msgstr "gruppieren"
 
-#: ../templates/classic/html/issue.search.html:32
+#: ../share/roundup/templates/classic/html/issue.search.html:38
 msgid "All text*:"
 msgstr "Volltext*:"
 
-#: ../templates/classic/html/issue.search.html:40
+#: ../share/roundup/templates/classic/html/issue.search.html:46
 msgid "Title:"
 msgstr "Titel:"
 
-#: ../templates/classic/html/issue.search.html:50
-msgid "Topic:"
+#: ../share/roundup/templates/classic/html/issue.search.html:56
+msgid "Keyword:"
 msgstr "Schlagwort:"
-# siehe die Anmerkung zu "Schlagwort"
 
-#: ../templates/classic/html/issue.search.html:58
+#: ../share/roundup/templates/classic/html/issue.search.html:58
+#: ../share/roundup/templates/classic/html/issue.search.html:123
+#: ../share/roundup/templates/classic/html/issue.search.html:139
+msgid "not selected"
+msgstr "nicht ausgewählt"
+
+#: ../share/roundup/templates/classic/html/issue.search.html:67
 msgid "ID:"
 msgstr "ID:"
 
-#: ../templates/classic/html/issue.search.html:66
+#: ../share/roundup/templates/classic/html/issue.search.html:75
 msgid "Creation Date:"
 msgstr "Erstellungsdatum:"
 
-#: ../templates/classic/html/issue.search.html:77
+#: ../share/roundup/templates/classic/html/issue.search.html:86
 msgid "Creator:"
 msgstr "Ersteller:"
 
-#: ../templates/classic/html/issue.search.html:79
+#: ../share/roundup/templates/classic/html/issue.search.html:88
 msgid "created by me"
 msgstr "von mir erstellt"
 
-#: ../templates/classic/html/issue.search.html:88
+#: ../share/roundup/templates/classic/html/issue.search.html:97
 msgid "Activity:"
-msgstr "Aktivität:"
+msgstr "Aktivität:"
 
-#: ../templates/classic/html/issue.search.html:99
+#: ../share/roundup/templates/classic/html/issue.search.html:108
 msgid "Actor:"
 msgstr "Akteur:"
 
-#: ../templates/classic/html/issue.search.html:101
+#: ../share/roundup/templates/classic/html/issue.search.html:110
 msgid "done by me"
-msgstr "von mir zuletzt geändert"
+msgstr "von mir zuletzt geändert"
 
-#: ../templates/classic/html/issue.search.html:112
+#: ../share/roundup/templates/classic/html/issue.search.html:121
 msgid "Priority:"
-msgstr "Priorität:"
+msgstr "Priorität:"
 
-#: ../templates/classic/html/issue.search.html:114
-#: ../templates/classic/html/issue.search.html:130
-msgid "not selected"
-msgstr "nicht gesetzt"
-
-#: ../templates/classic/html/issue.search.html:125
+#: ../share/roundup/templates/classic/html/issue.search.html:134
 msgid "Status:"
 msgstr "Status:"
 
-#: ../templates/classic/html/issue.search.html:128
+#: ../share/roundup/templates/classic/html/issue.search.html:137
 msgid "not resolved"
-msgstr "ungelöst"
+msgstr "ungelöst"
 
-#: ../templates/classic/html/issue.search.html:143
+#: ../share/roundup/templates/classic/html/issue.search.html:152
 msgid "Assigned to:"
 msgstr "Zugewiesen:"
 
-#: ../templates/classic/html/issue.search.html:146
+#: ../share/roundup/templates/classic/html/issue.search.html:155
 msgid "assigned to me"
 msgstr "mir zugewiesen"
 
-#: ../templates/classic/html/issue.search.html:148
+#: ../share/roundup/templates/classic/html/issue.search.html:157
 msgid "unassigned"
 msgstr "nicht zugewiesen"
 
-#: ../templates/classic/html/issue.search.html:158
+#: ../share/roundup/templates/classic/html/issue.search.html:167
 msgid "No Sort or group:"
-msgstr ""
-"Nicht sortieren/gruppieren:"
+msgstr "Nicht sortieren/gruppieren:"
 
-#: ../templates/classic/html/issue.search.html:166
+#: ../share/roundup/templates/classic/html/issue.search.html:175
 msgid "Pagesize:"
-msgstr "Einträge/Seite:"
+msgstr "Einträge/Seite:"
 
-#: ../templates/classic/html/issue.search.html:164
+#: ../share/roundup/templates/classic/html/issue.search.html:181
 msgid "Start With:"
 msgstr "Starten bei:"
 
-#: ../templates/classic/html/issue.search.html:170
+#: ../share/roundup/templates/classic/html/issue.search.html:187
 msgid "Sort Descending:"
 msgstr "Absteigend sortieren:"
 
-#: ../templates/classic/html/issue.search.html:177
+#: ../share/roundup/templates/classic/html/issue.search.html:194
 msgid "Group Descending:"
 msgstr "Absteigend gruppieren:"
 
-#: ../templates/classic/html/issue.search.html:184
+#: ../share/roundup/templates/classic/html/issue.search.html:201
 msgid "Query name**:"
 msgstr "Speichern unter**:"
 
-#: ../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
+#: ../share/roundup/templates/classic/html/issue.search.html:213
+#: ../share/roundup/templates/classic/html/page.html:43
+#: ../share/roundup/templates/classic/html/page.html:92
+#: ../share/roundup/templates/classic/html/user.help-search.html:69
+#: ../share/roundup/templates/minimal/html/page.html:43
+#: ../share/roundup/templates/minimal/html/page.html:91
 msgid "Search"
 msgstr "Suchen"
 
-#: ../templates/classic/html/issue.search.html:209
+#: ../share/roundup/templates/classic/html/issue.search.html:218
 msgid "*: The \"all text\" field will look in message bodies and issue titles"
-msgstr ""
-"*: Das Feld \"Volltext\" durchsucht Titel von Aufgaben und Kommentartexte"
+msgstr "*: Das Feld \"Volltext\" durchsucht Titel von Aufgaben und Kommentartexte"
 
-#: ../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 ""
-"**: Geben Sie einen Namen für diese Abfrage ein, um sie in der Seitenleiste "
-"zu speichern. "
+#: ../share/roundup/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 "**: Geben Sie einen Namen für diese Abfrage ein, um sie in der Seitenleiste zu speichern. "
 
-#: ../templates/classic/html/keyword.item.html:3
+#: ../share/roundup/templates/classic/html/keyword.item.html:3
 msgid "Keyword editing - ${tracker}"
 msgstr "Schlagwort bearbeiten - ${tracker}"
 
-#: ../templates/classic/html/keyword.item.html:5
+#: ../share/roundup/templates/classic/html/keyword.item.html:5
 msgid "Keyword editing"
-msgstr "Schlagwörter bearbeiten"
-# siehe die Anmerkung zu "Schlagwort"
+msgstr "Schlagwörter bearbeiten"
 
-#: ../templates/classic/html/keyword.item.html:11
+#: ../share/roundup/templates/classic/html/keyword.item.html:11
 msgid "Existing Keywords"
-msgstr "Vorhandene Schlagwörter"
-# siehe die Anmerkung zu "Schlagwort"
+msgstr "Vorhandene Schlagwörter"
 
-#: ../templates/classic/html/keyword.item.html:20
-msgid ""
-"To edit an existing keyword (for spelling or typing errors), click on its "
-"entry above."
+#: ../share/roundup/templates/classic/html/keyword.item.html:20
+msgid "To edit an existing keyword (for spelling or typing errors), click on its entry above."
 msgstr "Um ein bestehendes Schlagwort zu bearbeiten, klicken Sie darauf."
-# siehe die Anmerkung zu "Schlagwort"
 
-#: ../templates/classic/html/keyword.item.html:27
+#: ../share/roundup/templates/classic/html/keyword.item.html:27
 msgid "To create a new keyword, enter it below and click \"Submit New Entry\"."
-msgstr ""
-"Um ein neues Schlagwort hinzufügen, tragen Sie es hier ein und klicken Sie "
-"auf \"Eintrag speichern\"."
-# siehe die Anmerkung zu "Schlagwort"
+msgstr "Um ein neues Schlagwort hinzufügen, tragen Sie es hier ein und klicken Sie auf \"Eintrag speichern\"."
 
-#: ../templates/classic/html/keyword.item.html:37
-msgid "Keyword"
-msgstr "Schlagwort"
-# siehe die Anmerkung zu "Schlagwort" ("Topic")
-
-#: ../templates/classic/html/msg.index.html:3
+#: ../share/roundup/templates/classic/html/msg.index.html:3
 msgid "List of messages - ${tracker}"
 msgstr "Kommentare - ${tracker}"
 
-#: ../templates/classic/html/msg.index.html:5
+#: ../share/roundup/templates/classic/html/msg.index.html:5
 msgid "Message listing"
 msgstr "Kommentare"
 
-#: ../templates/classic/html/msg.item.html:6
+#: ../share/roundup/templates/classic/html/msg.item.html:6
 msgid "Message ${id} - ${tracker}"
 msgstr "Kommentar ${id} - ${tracker}"
 
-#: ../templates/classic/html/msg.item.html:9
+#: ../share/roundup/templates/classic/html/msg.item.html:9
 msgid "New Message - ${tracker}"
 msgstr "Neuer Kommentar - ${tracker}"
 
-#: ../templates/classic/html/msg.item.html:13
+#: ../share/roundup/templates/classic/html/msg.item.html:13
 msgid "New Message"
 msgstr "Neuer Kommentar"
 
-#: ../templates/classic/html/msg.item.html:15
+#: ../share/roundup/templates/classic/html/msg.item.html:15
 msgid "New Message Editing"
 msgstr "Neuen Kommentar bearbeiten"
 
-#: ../templates/classic/html/msg.item.html:18
+#: ../share/roundup/templates/classic/html/msg.item.html:18
 msgid "Message${id}"
 msgstr "Kommentar ${id}"
 
-#: ../templates/classic/html/msg.item.html:21
+#: ../share/roundup/templates/classic/html/msg.item.html:21
 msgid "Message${id} Editing"
 msgstr "Kommentar ${id} bearbeiten"
 
-#: ../templates/classic/html/msg.item.html:28
+#: ../share/roundup/templates/classic/html/msg.item.html:38
 msgid "Author"
 msgstr "Autor"
 
-#: ../templates/classic/html/msg.item.html:33
+#: ../share/roundup/templates/classic/html/msg.item.html:43
 msgid "Recipients"
-msgstr "Empfänger"
+msgstr "Empfänger"
 
-#: ../templates/classic/html/msg.item.html:44
+#: ../share/roundup/templates/classic/html/msg.item.html:54
 msgid "Content"
 msgstr "Inhalt"
 
-#: ../templates/classic/html/page.html:28
+#: ../share/roundup/templates/classic/html/page.html:54
+#: ../share/roundup/templates/minimal/html/page.html:53
 msgid "<b>Your Queries</b> (<a href=\"query?@template=edit\">edit</a>)"
 msgstr "<b>Abfragen</b> (<a href=\"query?@template=edit\">bearbeiten</a>)"
 
-#: ../templates/classic/html/page.html:39
+#: ../share/roundup/templates/classic/html/page.html:65
+#: ../share/roundup/templates/minimal/html/page.html:64
 msgid "Issues"
 msgstr "Aufgaben"
 
-#: ../templates/classic/html/page.html:41
-#: ../templates/classic/html/page.html:60
+#: ../share/roundup/templates/classic/html/page.html:67
+#: ../share/roundup/templates/classic/html/page.html:105
+#: ../share/roundup/templates/minimal/html/page.html:66
+#: ../share/roundup/templates/minimal/html/page.html:104
 msgid "Create New"
 msgstr "neuer Eintrag"
 
-#: ../templates/classic/html/page.html:43
+#: ../share/roundup/templates/classic/html/page.html:69
+#: ../share/roundup/templates/minimal/html/page.html:68
 msgid "Show Unassigned"
 msgstr "nicht zugewiesen"
 
-#: ../templates/classic/html/page.html:45
+#: ../share/roundup/templates/classic/html/page.html:81
+#: ../share/roundup/templates/minimal/html/page.html:80
 msgid "Show All"
 msgstr "alle anzeigen"
 
-#: ../templates/classic/html/page.html:48
+#: ../share/roundup/templates/classic/html/page.html:93
+#: ../share/roundup/templates/minimal/html/page.html:92
 msgid "Show issue:"
 msgstr "Aufgabe anzeigen:"
 
-#: ../templates/classic/html/page.html:58
-msgid "Keywords"
-msgstr "Schlagwörter"
-# siehe die Anmerkung zu "Schlagwort"
-
-#: ../templates/classic/html/page.html:64
+#: ../share/roundup/templates/classic/html/page.html:108
+#: ../share/roundup/templates/minimal/html/page.html:107
 msgid "Edit Existing"
 msgstr "bearbeiten"
 
-#: ../templates/classic/html/page.html:70
-#: ../templates/minimal/html/page.html:48
+#: ../share/roundup/templates/classic/html/page.html:114
+#: ../share/roundup/templates/minimal/html/page.html:113
 msgid "Administration"
 msgstr "Administration"
 
-#: ../templates/classic/html/page.html:72
-#: ../templates/minimal/html/page.html:49
+#: ../share/roundup/templates/classic/html/page.html:116
+#: ../share/roundup/templates/minimal/html/page.html:115
 msgid "Class List"
 msgstr "Klassenliste"
 
-#: ../templates/classic/html/page.html:76
-#: ../templates/minimal/html/page.html:51
+#: ../share/roundup/templates/classic/html/page.html:120
+#: ../share/roundup/templates/minimal/html/page.html:119
 msgid "User List"
 msgstr "Benutzerliste"
 
-#: ../templates/classic/html/page.html:78
-#: ../templates/minimal/html/page.html:54
+#: ../share/roundup/templates/classic/html/page.html:122
+#: ../share/roundup/templates/minimal/html/page.html:121
 msgid "Add User"
-msgstr "Benutzer hinzufügen"
+msgstr "Benutzer hinzufügen"
 
-#: ../templates/classic/html/page.html:85
-#: ../templates/classic/html/page.html:89
-#: ../templates/minimal/html/page.html:30
+#: ../share/roundup/templates/classic/html/page.html:129
+#: ../share/roundup/templates/classic/html/page.html:135
+#: ../share/roundup/templates/minimal/html/page.html:128
+#: ../share/roundup/templates/minimal/html/page.html:134
 msgid "Login"
 msgstr "anmelden"
 
-#: ../templates/classic/html/page.html:104
-#: ../templates/minimal/html/page.html:45
+#: ../share/roundup/templates/classic/html/page.html:134
+#: ../share/roundup/templates/minimal/html/page.html:133
 msgid "Remember me?"
 msgstr "dauerhaft anmelden?"
 
-#: ../templates/classic/html/page.html:108
-#: ../templates/classic/html/user.register.html:63
-#: ../templates/minimal/html/page.html:50
-#: ../templates/minimal/html/user.register.html:58
+#: ../share/roundup/templates/classic/html/page.html:138
+#: ../share/roundup/templates/classic/html/user.register.html:63
+#: ../share/roundup/templates/minimal/html/page.html:137
+#: ../share/roundup/templates/minimal/html/user.register.html:61
 msgid "Register"
 msgstr "registrieren"
 
-#: ../templates/classic/html/page.html:94
+#: ../share/roundup/templates/classic/html/page.html:141
+#: ../share/roundup/templates/minimal/html/page.html:140
 msgid "Lost&nbsp;your&nbsp;login?"
-msgstr "Paßwort&nbsp;vergessen?"
+msgstr "Passwort&nbsp;vergessen?"
 
-#: ../templates/classic/html/page.html:99
+#: ../share/roundup/templates/classic/html/page.html:146
+#: ../share/roundup/templates/minimal/html/page.html:145
 msgid "Hello, ${user}"
 msgstr "Hallo, ${user}"
 
-#: ../templates/classic/html/page.html:101
+#: ../share/roundup/templates/classic/html/page.html:148
 msgid "Your Issues"
 msgstr "Ihre Aufgaben"
 
-#: ../templates/classic/html/page.html:102
-#: ../templates/minimal/html/page.html:40
+#: ../share/roundup/templates/classic/html/page.html:160
+#: ../share/roundup/templates/minimal/html/page.html:147
 msgid "Your Details"
 msgstr "Ihr Konto"
 
-#: ../templates/classic/html/page.html:104
-#: ../templates/minimal/html/page.html:42
+#: ../share/roundup/templates/classic/html/page.html:162
+#: ../share/roundup/templates/minimal/html/page.html:149
 msgid "Logout"
 msgstr "abmelden"
 
-#: ../templates/classic/html/page.html:108
+#: ../share/roundup/templates/classic/html/page.html:166
+#: ../share/roundup/templates/minimal/html/page.html:153
 msgid "Help"
 msgstr "Hilfe"
 
-#: ../templates/classic/html/page.html:109
+#: ../share/roundup/templates/classic/html/page.html:167
+#: ../share/roundup/templates/minimal/html/page.html:154
 msgid "Roundup docs"
 msgstr "Roundup-Handbuch"
 
-#: ../templates/classic/html/page.html:160
-#: ../templates/classic/html/page.html:136
-#: ../templates/minimal/html/page.html:81
-msgid "diese Nachricht löschen"
-msgstr ""
-
+#: ../share/roundup/templates/classic/html/page.html:177
+#: ../share/roundup/templates/minimal/html/page.html:164
+msgid "clear this message"
+msgstr "diese Nachricht löschen"
+
+#: ../share/roundup/templates/classic/html/page.html:241
+#: ../share/roundup/templates/classic/html/page.html:256
+#: ../share/roundup/templates/classic/html/page.html:270
+#: ../share/roundup/templates/minimal/html/page.html:228
+#: ../share/roundup/templates/minimal/html/page.html:243
+#: ../share/roundup/templates/minimal/html/page.html:257
 msgid "don't care"
 msgstr "egal"
 
-#: ../templates/classic/html/page.html:162
+#: ../share/roundup/templates/classic/html/page.html:243
+#: ../share/roundup/templates/classic/html/page.html:258
+#: ../share/roundup/templates/classic/html/page.html:271
+#: ../share/roundup/templates/minimal/html/page.html:230
+#: ../share/roundup/templates/minimal/html/page.html:245
+#: ../share/roundup/templates/minimal/html/page.html:258
 msgid "------------"
 msgstr "------------"
 
-#: ../templates/classic/html/page.html:188
+#: ../share/roundup/templates/classic/html/page.html:299
+#: ../share/roundup/templates/minimal/html/page.html:286
 msgid "no value"
 msgstr "kein Wert"
 
-#: ../templates/classic/html/query.edit.html:4
+#: ../share/roundup/templates/classic/html/query.edit.html:4
 msgid "\"Your Queries\" Editing - ${tracker}"
 msgstr "Abfragen bearbeiten - ${tracker}"
 
-#: ../templates/classic/html/query.edit.html:6
+#: ../share/roundup/templates/classic/html/query.edit.html:6
 msgid "\"Your Queries\" Editing"
 msgstr "Abfragen bearbeiten"
 
-#: ../templates/classic/html/query.edit.html:11
+#: ../share/roundup/templates/classic/html/query.edit.html:11
 msgid "You are not allowed to edit queries."
 msgstr "Sie sind nicht berechtigt, Abfragen zu bearbeiten."
 
-#: ../templates/classic/html/query.edit.html:28
+#: ../share/roundup/templates/classic/html/query.edit.html:28
 msgid "Query"
 msgstr "Abfrage"
 
-#: ../templates/classic/html/query.edit.html:29
+#: ../share/roundup/templates/classic/html/query.edit.html:29
 msgid "Include in \"Your Queries\""
-msgstr "Unter \"Abfragen\" aufführen"
+msgstr "Unter \"Abfragen\" aufführen"
 
-#: ../templates/classic/html/query.edit.html:31
+#: ../share/roundup/templates/classic/html/query.edit.html:31
 msgid "Private to you?"
-msgstr "Nur für Sie?"
+msgstr "Nur für Sie?"
 
-#: ../templates/classic/html/query.edit.html:44
+#: ../share/roundup/templates/classic/html/query.edit.html:44
 msgid "leave out"
 msgstr "weglassen"
 
-#: ../templates/classic/html/query.edit.html:45
+#: ../share/roundup/templates/classic/html/query.edit.html:45
 msgid "include"
-msgstr "anfügen"
+msgstr "anfügen"
 
-#: ../templates/classic/html/query.edit.html:49
+#: ../share/roundup/templates/classic/html/query.edit.html:49
 msgid "leave in"
 msgstr "belassen"
 
-#: ../templates/classic/html/query.edit.html:54
+#: ../share/roundup/templates/classic/html/query.edit.html:54
 msgid "[query is retired]"
 msgstr "[Abfrage ist verborgen]"
 
-#: ../templates/classic/html/query.edit.html:67
-#: ../templates/classic/html/query.edit.html:92
+#: ../share/roundup/templates/classic/html/query.edit.html:67
+#: ../share/roundup/templates/classic/html/query.edit.html:94
 msgid "edit"
 msgstr "bearbeiten"
 
-#: ../templates/classic/html/query.edit.html:71
+#: ../share/roundup/templates/classic/html/query.edit.html:71
 msgid "yes"
 msgstr "ja"
 
-#: ../templates/classic/html/query.edit.html:73
+#: ../share/roundup/templates/classic/html/query.edit.html:73
 msgid "no"
 msgstr "nein"
 
-#: ../templates/classic/html/query.edit.html:79
+#: ../share/roundup/templates/classic/html/query.edit.html:79
 msgid "Delete"
-msgstr "Löschen"
+msgstr "Löschen"
 
-#: ../templates/classic/html/query.edit.html:90
+#: ../share/roundup/templates/classic/html/query.edit.html:96
 msgid "[not yours to edit]"
 msgstr "[nicht Ihr Eintrag]"
 
-#: ../templates/classic/html/query.edit.html:96
+#: ../share/roundup/templates/classic/html/query.edit.html:104
 msgid "Save Selection"
 msgstr "Auswahl speichern"
 
-#: ../templates/classic/html/user.forgotten.html:3
+#: ../share/roundup/templates/classic/html/user.forgotten.html:3
 msgid "Password reset request - ${tracker}"
-msgstr "Paßwort zurücksetzen - ${tracker}"
+msgstr "Passwort zurücksetzen - ${tracker}"
 
-#: ../templates/classic/html/user.forgotten.html:5
+#: ../share/roundup/templates/classic/html/user.forgotten.html:5
 msgid "Password reset request"
-msgstr "Paßwort zurücksetzen"
+msgstr "Passwort zurücksetzen"
 
-#: ../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 ""
-"Um Ihr Paßwort zurückzusetzen, geben Sie entweder die Email-Adresse an, mit "
-"der Sie sich registriert haben..."
+#: ../share/roundup/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 "Um Ihr Passwort zurückzusetzen, geben Sie entweder die E-Mail-Adresse an, mit der Sie sich registriert haben..."
 
-#: ../templates/classic/html/user.forgotten.html:16
+#: ../share/roundup/templates/classic/html/user.forgotten.html:16
 msgid "Email Address:"
-msgstr "Email-Adresse"
+msgstr "E-Mail-Adresse"
 
-#: ../templates/classic/html/user.forgotten.html:24
-#: ../templates/classic/html/user.forgotten.html:34
+#: ../share/roundup/templates/classic/html/user.forgotten.html:24
+#: ../share/roundup/templates/classic/html/user.forgotten.html:34
 msgid "Request password reset"
-msgstr "Paßwort zurücksetzen"
+msgstr "Passwort zurücksetzen"
 
-#: ../templates/classic/html/user.forgotten.html:30
+#: ../share/roundup/templates/classic/html/user.forgotten.html:30
 msgid "Or, if you know your username, then enter it below."
 msgstr "... oder Ihren Benutzernamen."
 
-#: ../templates/classic/html/user.forgotten.html:33
+#: ../share/roundup/templates/classic/html/user.forgotten.html:33
 msgid "Username:"
 msgstr "Benutzername:"
 
-#: ../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 ""
-"Danach wird eine Bestätigungs-Mail verschickt. Bitte folgen Sie den "
-"Anweisungen darin, um ihr Paßwort zurückzusetzen."
+#: ../share/roundup/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 "Danach wird eine Bestätigungs-E-Mail verschickt. Bitte folgen Sie den Anweisungen darin, um ihr Passwort zurückzusetzen."
 
-#: ../templates/classic/html/user.index.html:3
-#: ../templates/minimal/html/user.index.html:3
+#: ../share/roundup/templates/classic/html/user.help-search.html:73
+msgid "Pagesize"
+msgstr "Einträge/Seite"
+
+#: ../share/roundup/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 "Ihr Browser unterstützt keine Frames; Sie sollten gleich weitergeleitetwerden, oder besuchen Sie ${link}."
+
+#: ../share/roundup/templates/classic/html/user.index.html:3
+#: ../share/roundup/templates/minimal/html/user.index.html:3
 msgid "User listing - ${tracker}"
 msgstr "Benutzerliste - ${tracker}"
 
-#: ../templates/classic/html/user.index.html:5
-#: ../templates/minimal/html/user.index.html:5
+#: ../share/roundup/templates/classic/html/user.index.html:5
+#: ../share/roundup/templates/minimal/html/user.index.html:5
 msgid "User listing"
 msgstr "Benutzerliste"
 
-#: ../templates/classic/html/user.index.html:14
-#: ../templates/minimal/html/user.index.html:14
+#: ../share/roundup/templates/classic/html/user.index.html:19
+#: ../share/roundup/templates/minimal/html/user.index.html:19
 msgid "Username"
 msgstr "Benutzername"
 
-#: ../templates/classic/html/user.index.html:15
+#: ../share/roundup/templates/classic/html/user.index.html:20
 msgid "Real name"
 msgstr "Name"
 
-#: ../templates/classic/html/user.index.html:16
-#: ../templates/classic/html/user.item.html:65
-#: ../templates/classic/html/user.register.html:45
+#: ../share/roundup/templates/classic/html/user.index.html:21
+#: ../share/roundup/templates/classic/html/user.register.html:45
 msgid "Organisation"
 msgstr "Organisation"
 
-#: ../templates/classic/html/user.index.html:17
-#: ../templates/minimal/html/user.index.html:15
+#: ../share/roundup/templates/classic/html/user.index.html:22
+#: ../share/roundup/templates/minimal/html/user.index.html:20
 msgid "Email address"
-msgstr "Email-Adresse"
+msgstr "E-Mail-Adresse"
 
-#: ../templates/classic/html/user.index.html:18
+#: ../share/roundup/templates/classic/html/user.index.html:23
 msgid "Phone number"
 msgstr "Telefonnummer"
 
-#: ../templates/classic/html/user.index.html:19
+#: ../share/roundup/templates/classic/html/user.index.html:24
 msgid "Retire"
-msgstr "Verbergen"
+msgstr "Entfernen"
 
-#: ../templates/classic/html/user.index.html:32
+#: ../share/roundup/templates/classic/html/user.index.html:41
 msgid "retire"
-msgstr "verbergen"
+msgstr "entfernen"
 
-#: ../templates/classic/html/user.item.html:7
-#: ../templates/minimal/html/user.item.html:7
+#: ../share/roundup/templates/classic/html/user.item.html:9
+#: ../share/roundup/templates/minimal/html/user.item.html:9
 msgid "User ${id}: ${title} - ${tracker}"
 msgstr "Benutzer ${id}: ${title} - ${tracker}"
 
-#: ../templates/classic/html/user.item.html:10
-#: ../templates/minimal/html/user.item.html:10
+#: ../share/roundup/templates/classic/html/user.item.html:12
+#: ../share/roundup/templates/minimal/html/user.item.html:12
 msgid "New User - ${tracker}"
 msgstr "Neuer Benutzer - ${tracker}"
 
-#: ../templates/classic/html/user.item.html:14
-#: ../templates/minimal/html/user.item.html:6
+#: ../share/roundup/templates/classic/html/user.item.html:21
+#: ../share/roundup/templates/minimal/html/user.item.html:21
 msgid "New User"
 msgstr "Neuer Benutzer"
 
-#: ../templates/classic/html/user.item.html:16
-#: ../templates/minimal/html/user.item.html:8
+#: ../share/roundup/templates/classic/html/user.item.html:23
+#: ../share/roundup/templates/minimal/html/user.item.html:23
 msgid "New User Editing"
 msgstr "Neuen Benutzer bearbeiten"
 
-#: ../templates/classic/html/user.item.html:19
-#: ../templates/minimal/html/user.item.html:11
+#: ../share/roundup/templates/classic/html/user.item.html:26
+#: ../share/roundup/templates/minimal/html/user.item.html:26
 msgid "User${id}"
 msgstr "Benutzer${id}"
 
-#: ../templates/classic/html/user.item.html:22
-#: ../templates/minimal/html/user.item.html:14
+#: ../share/roundup/templates/classic/html/user.item.html:29
+#: ../share/roundup/templates/minimal/html/user.item.html:29
 msgid "User${id} Editing"
 msgstr "Benutzer ${id} bearbeiten"
 
-#: ../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 "Benutzername"
-
-#: ../templates/classic/html/user.item.html:42
-#: ../templates/classic/html/user.register.html:25
-#: ../templates/minimal/html/user.item.html:31
-#: ../templates/minimal/html/user.register.html:30
-msgid "Login Password"
-msgstr "Paßwort"
-
-#: ../templates/classic/html/user.item.html:46
-#: ../templates/classic/html/user.register.html:29
-#: ../templates/minimal/html/user.item.html:35
-#: ../templates/minimal/html/user.register.html:34
-msgid "Confirm Password"
-msgstr "Paßwort bestätigen"
-
-#: ../templates/classic/html/user.item.html:50
-#: ../templates/classic/html/user.register.html:33
-#: ../templates/minimal/html/user.item.html:39
-#: ../templates/minimal/html/user.register.html:38
+#: ../share/roundup/templates/classic/html/user.item.html:80
+#: ../share/roundup/templates/classic/html/user.register.html:33
+#: ../share/roundup/templates/minimal/html/user.item.html:80
+#: ../share/roundup/templates/minimal/html/user.register.html:41
 msgid "Roles"
 msgstr "Rollen"
 
-#: ../templates/classic/html/user.item.html:61
-#: ../templates/minimal/html/user.item.html:58
+#: ../share/roundup/templates/classic/html/user.item.html:88
+#: ../share/roundup/templates/minimal/html/user.item.html:88
 msgid "(to give the user more than one role, enter a comma,separated,list)"
-msgstr ""
-"<tt>Verwenden,Sie,Kommata</tt>, um einem Benutzer mehrere Rollen zuzuteilen"
-
-#: ../templates/classic/html/user.item.html:61
-#: ../templates/classic/html/user.register.html:41
-msgid "Phone"
-msgstr "Telefon"
-
-#: ../templates/classic/html/user.item.html:69
-msgid "Timezone"
-msgstr "Zeitzone"
+msgstr "<tt>Verwenden,Sie,Kommata</tt>, um einem Benutzer mehrere Rollen zuzuteilen"
 
-#: ../templates/classic/html/user.item.html:73
+#: ../share/roundup/templates/classic/html/user.item.html:109
+#: ../share/roundup/templates/minimal/html/user.item.html:109
 msgid "(this is a numeric hour offset, the default is ${zone})"
 msgstr "(als Differenz zu GMT/UTC in Stunden - Voreinstellung: ${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 "Email-Adresse"
-
-#: ../templates/classic/html/user.item.html:82
-#: ../templates/classic/html/user.register.html:53
-#: ../templates/minimal/html/user.item.html:51
-#: ../templates/minimal/html/user.register.html:50
+#: ../share/roundup/templates/classic/html/user.item.html:130
+#: ../share/roundup/templates/classic/html/user.register.html:53
+#: ../share/roundup/templates/minimal/html/user.item.html:130
+#: ../share/roundup/templates/minimal/html/user.register.html:53
 msgid "Alternate E-mail addresses<br>One address per line"
-msgstr "<div title=\"alle, von denen Mails an den Bugtracker geschickt werden sollen\">Alternative Email-Adressen</div><i>(eine pro Zeile)</i>"
+msgstr "<div title=\"alle, von denen E-Mails an den Bugtracker geschickt werden sollen\">Alternative E-Mail-Adressen</div><i>(eine pro Zeile)</i>"
 
-#: ../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
+#: ../share/roundup/templates/classic/html/user.register.html:4
+#: ../share/roundup/templates/classic/html/user.register.html:7
+#: ../share/roundup/templates/minimal/html/user.register.html:4
+#: ../share/roundup/templates/minimal/html/user.register.html:7
 msgid "Registering with ${tracker}"
-msgstr "Registrieren für ${tracker}"
-
-#: ../templates/classic/html/user.rego_progress.html:4
-#: ../templates/minimal/html/user.rego_progress.html:4
-msgid "Registration in progress - ${tracker}"
-msgstr "Die Registrierung ist im Gange - ${tracker}"
-
-#: ../templates/classic/html/user.rego_progress.html:6
-#: ../templates/minimal/html/user.rego_progress.html:6
-msgid "Registration in progress..."
-msgstr "Die Registrierung ist im Gange..."
-
-#: ../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 ""
-"Sie werden in Kürze eine Bestätigungs-Mail erhalten. Um die Registrierung "
-"abzuschließen, klicken Sie auf den enthaltenen Link."
-
-#: ../templates/minimal/html/home.html:2
-msgid "Tracker home - ${tracker}"
-msgstr "Tracker-Start - ${tracker}"
-
-#: ../templates/minimal/html/home.html:4
-msgid "Tracker home"
-msgstr "Tracker-Start"
+msgstr "Registrieren für ${tracker}"
 
-#: ../templates/minimal/html/home.html:16
-msgid "Please select from one of the menu options on the left."
-msgstr "Bitte wählen Sie links eine Menu-Option."
-
-#: ../templates/minimal/html/home.html:19
-msgid "Please log in or register."
-msgstr "Bitte anmelden oder registrieren"
-
-#: ../templates/minimal/html/page.html:38
-msgid "Hello,<br>${user}"
-msgstr "Hallo,<br>${user}"
-
-#: ../templates/minimal/html/user.item.html:3
-msgid "User editing - ${tracker}"
-msgstr "Benutzer bearbeiten - ${tracker}"
-
-#: ../templates/classic/html/issue.item.html:120;
-msgid "Copy item"
-msgstr "kopieren"
-
-#: ../templating.py:1099;
-msgid "Copy of %(class)s %(id)s"
-msgstr "Kopie von %(class)s%(id)s"
-
-#: ../templates/classic/html/...:123
-msgid "No Sort or group"
-msgstr "nicht sortieren/gruppieren"
-
-#: ../templates/classic/html/issue.search.html:170
-msgid "sort descending"
-msgstr "absteigend sortieren"
-
-#: ../templates/classic/html/issue.search.html:177
-msgid "group descending"
-msgstr "absteigend gruppieren"
-
-#: ../templates/classic/html/issue.search.html:170
-msgid "don't sort"
-msgstr "nicht sortieren"
+#: ../share/roundup/templates/classic/html/user.register.html:21
+#: ../share/roundup/templates/minimal/html/user.register.html:29
+msgid "Login Name"
+msgstr "Benutzername"
 
-#: ../templates/classic/html/issue.search.html:177
-msgid "don't group"
-msgstr "nicht gruppieren"
+#: ../share/roundup/templates/classic/html/user.register.html:25
+#: ../share/roundup/templates/minimal/html/user.register.html:33
+msgid "Login Password"
+msgstr "Paßwort"
 
-#: ../templates/classic(th)/html/issue.search.html:XXX
-msgid "Sort/Group Descending:"
-msgstr "absteigend sortieren/gruppieren:"
+#: ../share/roundup/templates/classic/html/user.register.html:29
+#: ../share/roundup/templates/minimal/html/user.register.html:37
+msgid "Confirm Password"
+msgstr "Paßwort bestätigen"
 
-#: ../templates/classic(th)/html/issue.search.html:XXX
-msgid "Paged Output:"
-msgstr "seitenweise ausgeben:"
+#: ../share/roundup/templates/classic/html/user.register.html:41
+msgid "Phone"
+msgstr "Telefon"
 
-#: ../templates/classic(th)/html/issue.search.html:XXX
-msgid "Pagesize"
-msgstr "Einträge/Seite"
+#: ../share/roundup/templates/classic/html/user.register.html:49
+#: ../share/roundup/templates/minimal/html/user.register.html:49
+msgid "E-mail address"
+msgstr "Email-Adresse"
 
-#: ../templates/classic/html/user.item.html:XXX
-msgid "username"
-msgstr "Benutzername"
+#: ../share/roundup/templates/classic/html/user.rego_progress.html:4
+#: ../share/roundup/templates/minimal/html/user.rego_progress.html:4
+msgid "Registration in progress - ${tracker}"
+msgstr "Die Registrierung ist im Gange - ${tracker}"
 
-#: ../templates/classic/html/user.item.html:XXX
-msgid "realname"
-msgstr "Name"
+#: ../share/roundup/templates/classic/html/user.rego_progress.html:6
+#: ../share/roundup/templates/minimal/html/user.rego_progress.html:6
+msgid "Registration in progress..."
+msgstr "Die Registrierung ist im Gange..."
 
-#: ../templates/classic/html/user.item.html:XXX
-msgid "address"
-msgstr "Mail-Adresse"
+#: ../share/roundup/templates/classic/html/user.rego_progress.html:10
+#: ../share/roundup/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 "Sie werden in Kürze eine Bestätigungs-E-Mail erhalten. Um die Registrierung abzuschließen, klicken Sie auf den enthaltenen Link."
 
 # priority translations:
-#: ../templates/classic/initial_data.py:5
+#: ../share/roundup/templates/classic/initial_data.py:5
 msgid "critical"
 msgstr "Fehler (KRITISCH)"
 
-#: ../templates/classic/initial_data.py:6
+#: ../share/roundup/templates/classic/initial_data.py:6
 msgid "urgent"
 msgstr "Fehler (dringend)"
 
-#: ../templates/classic/initial_data.py:7
+#: ../share/roundup/templates/classic/initial_data.py:7
 msgid "bug"
 msgstr "Fehler"
 
-#: ../templates/classic/initial_data.py:8
+#: ../share/roundup/templates/classic/initial_data.py:8
 msgid "feature"
 msgstr "Anforderung"
 
-#: ../templates/classic/initial_data.py:9
+#: ../share/roundup/templates/classic/initial_data.py:9
 msgid "wish"
 msgstr "Wunsch"
 
-#: status translations:
-#: ../templates/classic/initial_data.py:12
+#: ../share/roundup/templates/classic/initial_data.py:12
 msgid "unread"
 msgstr "ungelesen"
 
-#: ../templates/classic/initial_data.py:13
+#: ../share/roundup/templates/classic/initial_data.py:13
 msgid "deferred"
-msgstr "zurückgestellt"
+msgstr "zurückgestellt"
 
-#: ../templates/classic/initial_data.py:14
+#: ../share/roundup/templates/classic/initial_data.py:14
 msgid "chatting"
 msgstr "in Diskussion"
 
-#: ../templates/classic/initial_data.py:15
-msgid "in-progress"
-msgstr "in Arbeit"
-
-#: ../templates/classic/initial_data.py:16
+#: ../share/roundup/templates/classic/initial_data.py:15
 msgid "need-eg"
 msgstr "Beispiel erbeten"
 
-#: ../templates/classic/initial_data.py:17
+#: ../share/roundup/templates/classic/initial_data.py:16
+msgid "in-progress"
+msgstr "in Arbeit"
+
+#: ../share/roundup/templates/classic/initial_data.py:17
 msgid "testing"
 msgstr "im Test"
 
-#: ../templates/classic/initial_data.py:18
+#: ../share/roundup/templates/classic/initial_data.py:18
 msgid "done-cbb"
 msgstr "erledigt (provisorisch)"
 
-#: ../templates/classic/initial_data.py:19
+#: ../share/roundup/templates/classic/initial_data.py:19
 msgid "resolved"
 msgstr "erledigt"
 
-#: ../scripts/roundup_mailgw.py:36
-msgid "try %(prog)s --help for usage info"
-msgstr "%(prog)s --help gibt eine Hilfe aus"
-
-#: ../templates/classic(th)/html/issue.item.html
-msgid "(cal)"
-msgstr "(Kal.)"
+#: ../share/roundup/templates/minimal/html/home.html:2
+msgid "Tracker home - ${tracker}"
+msgstr "Tracker-Startseite - ${tracker}"
+
+#: ../share/roundup/templates/minimal/html/home.html:4
+msgid "Tracker home"
+msgstr "Tracker-Startseite"
+
+#: ../share/roundup/templates/minimal/html/home.html:16
+msgid "Please select from one of the menu options on the left."
+msgstr "Bitte wählen Sie links eine Menu-Option."
+
+#: ../share/roundup/templates/minimal/html/home.html:19
+msgid "Please log in or register."
+msgstr "Bitte anmelden oder registrieren"
+
+#~ msgid "%(key)s: %(value)r"
+#~ msgstr "%(key)s: %(value)r"
+#~ msgid "You do not have permission to edit user roles"
+#~ msgstr "Sie sind nicht berechtigt, Benutzer-Rollen zu ändern"
+#~ msgid ""
+#~ "<html><head><title>An error has occurred</title></head>\n"
+#~ "<body><h1>An error has occurred</h1>\n"
+#~ "<p>A problem was encountered processing your request.\n"
+#~ "The tracker maintainers have been notified of the problem.</p>\n"
+#~ "</body></html>"
+#~ msgstr ""
+#~ "<html><head><title>Ein Fehler ist aufgetreten</title></head>\n"
+#~ "<body><h1>Ein Fehler ist aufgetreten</h1>\n"
+#~ "<p>Bei der Bearbeitung Ihrer Daten ist ein Fehler aufgetreten. Die "
+#~ "Admistratoren wurden benachrichtigt.</p>\n"
+#~ "</body></html>"
+#~ msgid "Not a date spec: %s"
+#~ msgstr "Kein gültiges Datum: %s"
+#~ 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"
+#~ "previous subject title intact so I can match that.\n"
+#~ "\n"
+#~ "Subject was: \"%(subject)s\"\n"
+#~ msgstr ""
+#~ "\n"
+#~ "Ich kann Ihre Nachricht keinem Eintrag in der Datenbank zuordnen - Sie "
+#~ "müssen\n"
+#~ "entweder einen vollen Bezeichner angeben (mit Nummer, z. B. \"[issue123]"
+#~ "\")\n"
+#~ "oder die Betreffzeile intakt lassen, so daß ich diese zuordnen kann.\n"
+#~ "\n"
+#~ "Die Betreffzeile (Subject) war:\n"
+#~ "   '%(subject)s'\n"
+#~ msgid ""
+#~ "\n"
+#~ "You are not a registered user.\n"
+#~ "\n"
+#~ "Unknown address: %(from_address)s\n"
+#~ msgstr ""
+#~ "\n"
+#~ "Sie sind kein registrierter Anwender.\n"
+#~ "\n"
+#~ "Unbekannte Adresse: %(from_address)s\n"
+#~ msgid "topic"
+#~ msgstr "Schlagwort"
+#~ msgid "Error: apop specification not valid"
+#~ msgstr "Fehler: apop Optionen ungültig"
+#~ msgid "List of issues - ${tracker}"
+#~ msgstr "Aufgabenliste - ${tracker}"
+#~ msgid "List of issues - ${query} - ${tracker}"
+#~ msgstr "Aufgabenliste - ${query} - ${tracker}"
+#~ msgid "List of issues - ${query}"
+#~ msgstr "Aufgabenliste - ${query}"
+#~ msgid "Topic"
+#~ msgstr "Schlagwort"
+#~ msgid "View: ${link}"
+#~ msgstr "Anzeigen: ${link}"
+#~ msgid "Topics"
+#~ msgstr "Schlagwörter"
+#~ msgid "Topic:"
+#~ msgstr "Schlagwort:"
+#~ msgid "Timezone"
+#~ msgstr "Zeitzone"
+#~ msgid "Hello,<br>${user}"
+#~ msgstr "Hallo,<br>${user}"
+#~ msgid "User editing - ${tracker}"
+#~ msgstr "Benutzer bearbeiten - ${tracker}"
+#~ msgid "Copy item"
+#~ msgstr "kopieren"
+#~ msgid "sort descending"
+#~ msgstr "absteigend sortieren"
+#~ msgid "group descending"
+#~ msgstr "absteigend gruppieren"
+#~ msgid "don't sort"
+#~ msgstr "nicht sortieren"
+#~ msgid "don't group"
+#~ msgstr "nicht gruppieren"
+#~ msgid "Sort/Group Descending:"
+#~ msgstr "absteigend sortieren/gruppieren:"
+#~ msgid "Paged Output:"
+#~ msgstr "seitenweise ausgeben:"
+#~ msgid "username"
+#~ msgstr "Benutzername"
+#~ msgid "realname"
+#~ msgstr "Name"
+#~ msgid "firstname"
+#~ msgstr "Vorname"
+#~ msgid "lastname"
+#~ msgstr "Nachname"
+#~ msgid "address"
+#~ msgstr "Mail-Adresse"
+#~ msgid "(cal)"
+#~ msgstr "(Kal.)"
+

Modified: tracker/roundup-src/locale/en.po
==============================================================================
--- tracker/roundup-src/locale/en.po	(original)
+++ tracker/roundup-src/locale/en.po	Sun Mar 15 22:43:30 2009
@@ -1,6 +1,6 @@
 # English message file for Roundup Issue Tracker
 #
-# $Id: en.po,v 1.2 2004/11/20 11:54:32 a1s Exp $
+# $Id: en.po,v 1.2 2004-11-20 11:54:32 a1s Exp $
 #
 # roundup.pot revision 1.9
 #

Modified: tracker/roundup-src/locale/fr.po
==============================================================================
--- tracker/roundup-src/locale/fr.po	(original)
+++ tracker/roundup-src/locale/fr.po	Sun Mar 15 22:43:30 2009
@@ -1,22 +1,24 @@
 # French message file for Roundup Issue Tracker
 # Georges Martin <georges.martin at pi.be>, 2004.
+# Patrick Decat <pdecat at gmail.com>, 2008.
+# Stéphane Raimbault <stephane.raimbault at gmail.com>, 2008.
 # This file is distributed under the same license as the Roundup package.
 # 
-# $Id: fr.po,v 1.5 2006/05/13 09:42:49 a1s Exp $
+# $Id: fr.po,v 1.6 2009-02-05 14:18:27 stefan Exp $
 # roundup.pot revision 1.18
 # 
 msgid ""
 msgstr ""
-"Project-Id-Version: Roundup 1.1.1\n"
+"Project-Id-Version: Roundup 1.4.6\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-05-09 12:08+0300\n"
-"Last-Translator: Patrick Decat <pdecat at gmail.com>\n"
-"Language-Team: French Translators <roundup-devel at lists.sourceforge.net>\n"
+"PO-Revision-Date: 2008-10-14 10:15+0200\n"
+"Last-Translator: Stéphane Raimbault <stephane.raimbault at gmail.com>\n"
+"Language-Team: GNOME French Team <gnomefr at traduc.org>\n"
 "MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=ISO-8859-1\n"
+"Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=n !=1;\n"
+"Plural-Forms: nplurals=2; plural=n>1;\n"
 
 # ../roundup/admin.py:85 :979 :1028 :1050
 # ../roundup/admin.py:1052 ../roundup/admin.py:85:981 :1030:1052
@@ -24,26 +26,24 @@
 #: ../roundup/admin.py:1052
 #, python-format
 msgid "no such class \"%(classname)s\""
-msgstr "la classe \"%(classname)s\" n'existe pas"
+msgstr "aucune classe nommée « %(classname)s »"
 
 # ../roundup/admin.py:95 :99
 # ../roundup/admin.py:95 ../roundup/admin.py:99 ../roundup/admin.py:95:99
 #: ../roundup/admin.py:95 ../roundup/admin.py:99
 #, python-format
 msgid "argument \"%(arg)s\" not propname=value"
-msgstr "l'argument \"%(arg)s\" n'est pas au format nom-de-propriété=valeur"
+msgstr "l'argument « %(arg)s » n'est pas au format nom-de-propriété=valeur"
 
 #: ../roundup/admin.py:112
 #, python-format
 msgid ""
 "Problem: %(message)s\n"
 "\n"
-msgstr ""
-"Problème: %(message)s\n"
-"\n"
+msgstr "Problème : %(message)s\n\n"
 
 #: ../roundup/admin.py:113
-#, fuzzy, python-format
+#, python-format
 msgid ""
 "%(message)sUsage: roundup-admin [options] [<command> <arguments>]\n"
 "\n"
@@ -68,10 +68,10 @@
 " roundup-admin help <command>             -- command-specific help\n"
 " roundup-admin help all                   -- all available help\n"
 msgstr ""
-"%(message)sUsage: roundup-admin [options] [<commande> <arguments>]\n"
+"%(message)sUtilisation : roundup-admin [options] [<commande> <arguments>]\n"
 "\n"
-"Options:\n"
-" -i base-du-pisteur -- spécifie le répertoire de base du pisteur à\n"
+"Options :\n"
+" -i racine pisteur  -- indique le répertoire racine du pisteur à\n"
 "                       administrer.\n"
 " -u                 -- le nom-d'utilisateur[:mot-de-passe] à utiliser\n"
 "                       pour les commandes.\n"
@@ -79,16 +79,18 @@
 "                       les numéros d'identification de classe.\n"
 " -c                 -- imprime les listes de données en les séparant par\n"
 "                       des virgules.\n"
-"                       Identique à '-S \",\"'.\n"
+"                       Identique à « -S \",\" ».\n"
 " -S <chaîne>        -- imprime les listes de données en les séparant par\n"
 "                       la chaîne spécifiée.\n"
 " -s                 -- imprime les listes de données en les séparant par\n"
 "                       des espaces.\n"
-"                       Identique à '-S \" \"'.\n"
+"                       Identique à « -S \" \" ».\n"
+" -V                 -- est verbeux à l'importation\n"
+" -v                 -- affiche les versions de Roundup et Python (et quitte).\n"
 "\n"
-" Une seule des options -s, -c ou -S peut être spécifiée.\n"
+" Seulement une des options parmi -s, -c ou -S peut être utilisée à la fois.\n"
 "\n"
-"Aide:\n"
+"Aide :\n"
 " roundup-admin -h\n"
 " roundup-admin help                       -- cette aide\n"
 " roundup-admin help <commande>            -- l'aide sur une commande\n"
@@ -96,16 +98,16 @@
 
 #: ../roundup/admin.py:140
 msgid "Commands:"
-msgstr "Commandes:"
+msgstr "Commandes :"
 
 #: ../roundup/admin.py:147
 msgid ""
 "Commands may be abbreviated as long as the abbreviation\n"
 "matches only one command, e.g. l == li == lis == list."
 msgstr ""
-"Les commandes peuvent être abrégées, pour autant\n"
-"que l'abréviation ne corresponde qu'à une seule commande,\n"
-"par ex.: l == li == lis == list."
+"Les commandes peuvent être abrégées, dans le cas\n"
+"où l'abréviation ne correspond qu'à une seule commande,\n"
+"par ex. : l == li == lis == list."
 
 #: ../roundup/admin.py:177
 msgid ""
@@ -174,68 +176,66 @@
 "Command help:\n"
 msgstr ""
 "\n"
-"Toutes les commandes (à l'exception de \"help\") nécessitent\n"
-"un spécificateur de pisteur. Il s'agit juste du chemin vers le pisteur\n"
-"roundup sur lequel vous désirez travailler. Un pisteur roundup est "
-"l'endroit\n"
-"où roundup garde la base de données et les fichiers de configuration qui\n"
-"définissent un pisteur de problèmes.Il peut être spécifié dans la variable "
-"d'environnement \"TRACKER_HOME\" ou\n"
-"dans la ligne de commande comme \"-i base-du-pisteur\".\n"
+"Toutes les commandes (à l'exception de « help ») nécessitent\n"
+"d'indiquer un pisteur. Il s'agit juste du chemin vers le pisteur\n"
+"Roundup sur lequel vous désirez travailler. Un pisteur Roundup est\n"
+"l'endroit où Roundup conserver la base de données et les fichiers de\n"
+"configuration qui définissent un pisteur de problèmes. Il peut être\n"
+"indiqué dans la variable d'environnement « TRACKER_HOME » ou en ligne\n"
+"de commande comme « -i racine-du-pisteur ».\n"
 "\n"
 "Un indicateur est la concaténation d'un nom de classe et d'un\n"
-"identificateur de noeud, par ex: bug1, user10,...\n"
+"identificateur de noeud, p. ex. : bug1, user10, ...\n"
 "\n"
-"Les valeurs de propriété sont représentés comme des chaînes de caractères\n"
-"dans les arguments de commande et dans les résultats imprimés:\n"
-" . les chaînes de caractères sont représentées telles quelles.\n"
-" . les dates sont imprimées dans le format de date complet avec le fuseau\n"
-"   horaire local et acceptées dans le format complet ou l'un des formats\n"
-"   partiels expliqués ci-dessous. . les valeurs de lien sont imprimées comme "
-"indicateurs de noeuds.\n"
+"Les valeurs de propriété sont représentées comme des chaînes de\n"
+"caractères dans les arguments de commande et dans les résultats\n"
+"imprimés :\n"
+" . Les chaînes de caractères sont représentées telles quelles.\n"
+" . Les dates sont imprimées dans le format de date complet avec le\n"
+"   fuseau horaire local et acceptées dans le format complet ou l'un\n"
+"   des formats partiels expliqués ci-dessous.\n"
+" . Les valeurs de liens sont imprimées comme indicateurs de noeuds.\n"
 "   Lorsqu'ils sont donnés comme arguments, les indicateurs de noeuds\n"
-"   et les chaînes de clés sont tout deux acceptés.\n"
-" . les valeurs des liens multiples sont imprimées comme listes de\n"
+"   et les chaînes de clés sont tous deux acceptés.\n"
+" . Les valeurs des liens multiples sont imprimées comme listes de\n"
 "   désignateurs de noeuds, séparés par des virgules. Lorsqu'ils sont\n"
-"   donnés comme arguments, des désignateurs de noeuds ou des clés\n"
-"   sous forme de chaîne de caractères sont acceptés; une chaîne de "
-"caractères\n"
-"   vide, un noeud seul ou une liste de noeuds séparés par des virgules \n"
-"   sont acceptés.\n"
-"\n"
-"Lorsque des valeurs de propriétés doivent contenir des espaces, entourez\n"
-"simplement la valeur avec des guillements simples ('') ou doubles (\"\").\n"
-"Un espace seul peut également être \"backslash-quoted\". Si une valeur doit\n"
-"contenir une caractère de \"quoting\", il doit être \"backslash-quoted\" ou "
-"être\n"
-"placé entre guillemets. Par ex.:\n"
-"           hello world        (2 éléments: hello, world)\n"
-"           \"hello world\"    (1 élément: hello world)\n"
-"           \"Roch'e\" Compaan (2 éléments: Roch'e Compaan)\n"
-"           Roch\\'e Compaan   (2 éléments: Roch'e Compaan)\n"
-"           address=\"1 2 3\"  (1 élément: address=1 2 3)\n"
-"           \\\\               (1 élément: \\)\n"
-"           \\n\\r\\t          (1 élément: un passage à la ligne, un\n"
-"                               retour-chariot et une tabulation)\n"
-"\n"
-"Lorsque plusieurs noeuds sont spécifiés aux commandes roundup \"get\"\n"
-"ou \"set\", les propriétés spécifiées sont extraites ou assignées à\n"
-"tout ces noeuds.\n"
-"\n"
-"Lorsque plusieurs résultats sont renvoyés par les commandes roundup \"get\"\n"
-"ou \"set\", ils sont, par défaut, imprimés un par ligne ou, avec \n"
-"l'option -c, séparés par des virgules.\n"
-"\n"
-"Lorsqu'une commande change des données, une authentification par nom et mot "
-"de\n"
-"passe est requise. L'authentification peut être donnée soit comme \"nom\",\n"
-"soit comme \"nom:mot-de-passe.\n"
+"   donnés comme arguments, des désignateurs de noeuds ou des clés sous\n"
+"   forme de chaîne de caractères sont acceptés ; une chaîne de\n"
+"   caractères vide, un noeud seul ou une liste de noeuds séparés par\n"
+"   des virgules sont acceptés.\n"
+"\n"
+"Lorsque des valeurs de propriétés doivent contenir des espaces,\n"
+"entourez simplement la valeur avec des guillements simples « ' » ou\n"
+"doubles « \" ».  Une espace seule peut également être protégée par un\n"
+"anti-slash. Si une valeur doit contenir un guillemet, il doit être\n"
+"protégé par un anti-slash ou être placé entre guillemets. Par\n"
+"exemple :\n"
+"           hello world       (2 éléments : hello, world)\n"
+"           \"hello world\"     (1 élément : hello world)\n"
+"           \"Roch'e\" Compaan  (2 éléments : Roch'e Compaan)\n"
+"           Roch\\'e Compaan   (2 éléments : Roch'e Compaan)\n"
+"           address=\"1 2 3\"   (1 élément : address=1 2 3)\n"
+"           \\\\                (1 élément : \\)\n"
+"           \\n\\r\\t            (1 élément : un passage à la ligne, un\n"
+"                              retour-chariot et une tabulation)\n"
+"\n"
+"Lorsque plusieurs noeuds sont indiqués aux commandes roundup « get »\n"
+"ou « set », les propriétés sont extraites ou assignées à tous ces\n"
+"noeuds.\n"
+"\n"
+"Lorsque plusieurs résultats sont renvoyés par les commandes roundup\n"
+"« get » ou « set », ils sont, par défaut, imprimés un par ligne ou,\n"
+"avec l'option -c, séparés par des virgules.\n"
+"\n"
+"Lorsqu'une commande modifie des données, une authentification par nom\n"
+"et mot de passe est requise. L'authentification peut être donnée soit\n"
+"comme « nom », soit comme « nom:mot-de-passe ».\n"
 " . comme variable d'environnement ROUNDUP_LOGIN\n"
 " . comme option -u dans la ligne de commande\n"
-"Si le nom ou le mot de passe ne sont pas fournis, ils sont demandés à la\n"
-"ligne de commande\n"
+"Si le nom ou le mot de passe ne sont pas fournis, ils sont demandés à\n"
+"la ligne de commande.\n"
 "\n"
-"Quelques exemples de dates:\n"
+"Quelques exemples de dates :\n"
 "  \"2000-04-17.03:45\" donne <Date 2000-04-17.08:45:00>\n"
 "  \"2000-04-17\" donne <Date 2000-04-17.00:00:00>\n"
 "  \"01-25\" donne <Date yyyy-01-25.00:00:00>\n"
@@ -245,12 +245,12 @@
 "  \"8:47:11\" donne <Date yyyy-mm-dd.13:47:11>\n"
 "  \".\" donne \"maintenant\"\n"
 "\n"
-"Aide sur les commandes:\n"
+"Aide sur les commandes :\n"
 
 #: ../roundup/admin.py:240
 #, python-format
 msgid "%s:"
-msgstr "%s:"
+msgstr "%s :"
 
 #: ../roundup/admin.py:245
 msgid ""
@@ -263,8 +263,8 @@
 "        all       -- all available help\n"
 "        "
 msgstr ""
-"Usage: help sujet\n"
-"        Donne de l'aide sur un sujet.\n"
+"Utilisation : help sujet\n"
+"        Affiche de l'aide sur un sujet.\n"
 "\n"
 "        commands   -- liste les commandes\n"
 "        <commande> -- aide spécifique à une commande\n"
@@ -275,19 +275,19 @@
 #: ../roundup/admin.py:268
 #, python-format
 msgid "Sorry, no help for \"%(topic)s\""
-msgstr "Désolé, aucune aide n'est disponible à propos de \"%(topic)s\""
+msgstr "Désolé, aucune aide n'est disponible au sujet de « %(topic)s »"
 
 # ../roundup/admin.py:338 :394
 # ../roundup/admin.py:340 ../roundup/admin.py:396 ../roundup/admin.py:340:396
 #: ../roundup/admin.py:340 ../roundup/admin.py:396
 msgid "Templates:"
-msgstr "Modèles:"
+msgstr "Modèles :"
 
 # ../roundup/admin.py:341 :405
 # ../roundup/admin.py:343 ../roundup/admin.py:407 ../roundup/admin.py:343:407
 #: ../roundup/admin.py:343 ../roundup/admin.py:407
 msgid "Back ends:"
-msgstr "Moteurs de stockage:"
+msgstr "Moteurs de stockage :"
 
 #: ../roundup/admin.py:346
 msgid ""
@@ -314,33 +314,32 @@
 "        See also initopts help.\n"
 "        "
 msgstr ""
-"Usage: install [template [backend [admin password [key=val[,key=val]]]]]\n"
+"Utilisation : install [template [backend [admin password [key=val[,key=val]]]]]\n"
 "        Installe un nouveau pisteur Roundup.\n"
 "\n"
-"        Cette commande demandera le répertoire de base du pisteur (s'il\n"
-"        n'est pas fourni par la variable d'environnement TRACKER_HOME\n"
-"        ou l'option -i).\n"
-"        Le modèle, le moteur de stockage et le mot de passe\n"
-"        d'administration peuvent être spécifiés dans cet ordre comme\n"
-"        arguments dans la ligne de commande.\n"
-"\n"
-"        Le dernier argument de la ligne de commande permet de préciser des\n"
-"        valeurs initiales pour les options de configuration. Par exemple,\n"
-"        \"web_http_auth=no,rdbms_user=dinsdale\" remplacera les valeurs par\n"
-"        défaut pour les options http_auth dans la section [web] and user\n"
-"        dans la in section [rdbms]. Soyez attentifs à ne pas mettre "
-"d'espace\n"
-"        dans cet argument! (Entourez-le de quotes si vous devez préciser "
-"des\n"
-"        valeurs contenant des espaces).\n"
-"\n"
-"        La commande \"initialise\" doît être appelée après cette commande,\n"
-"        pour initialiser la base de données du pisteur. Vous pouvez éditer\n"
-"        le contenu initial de la base de données avant d'exécuter cette\n"
-"        commande en modifiant la fonction init() du module dbinit.py du\n"
-"        pisteur.\n"
+"        Cette commande demandera le répertoire de base du pisteur\n"
+"        (s'il n'est pas fourni par la variable d'environnement\n"
+"        TRACKER_HOME ou l'option -i).  Le modèle, le moteur de\n"
+"        stockage et le mot de passe d'administration peuvent être\n"
+"        renseignés dans cet ordre comme arguments dans la ligne de\n"
+"        commande.\n"
+"\n"
+"        Le dernier argument de la ligne de commande permet de préciser\n"
+"        des valeurs initiales pour les options de configuration. Par\n"
+"        exemple, « web_http_auth=no,rdbms_user=dinsdale » remplacera\n"
+"        les valeurs par défaut pour les options http_auth dans la\n"
+"        section [web] and user dans la in section [rdbms]. Soyez\n"
+"        attentifs à ne pas mettre d'espace dans cet argument (protéger\n"
+"        les arguments avec des guillemets si vous devez préciser des\n"
+"        contenant des espaces).\n"
+"\n"
+"        La commande « initialise » doît être appelée après cette\n"
+"        commande, pour initialiser la base de données du pisteur. Vous\n"
+"        pouvez modifier le contenu initial de la base de données avant\n"
+"        d'exécuter cette commande en modifiant la fonction init() du\n"
+"        module dbinit.py du pisteur.\n"
 "\n"
-"        Voyez également l'aide sur \"initopts\".\n"
+"        Consultez également l'aide sur « initopts ».\n"
 "        "
 
 # ../roundup/admin.py:367 :464 :525 :604 :654 :712 :733 :761 :832 :899 :970
@@ -354,12 +353,12 @@
 #: ../roundup/admin.py:1042 ../roundup/admin.py:1069 ../roundup/admin.py:1136
 #: ../roundup/admin.py:1207
 msgid "Not enough arguments supplied"
-msgstr "Pas assez d'arguments fournis"
+msgstr "Pas suffisamment d'arguments fournis"
 
 #: ../roundup/admin.py:375
 #, python-format
 msgid "Instance home parent directory \"%(parent)s\" does not exist"
-msgstr "Le répertoire parent \"%(parent)s\" de l'instance de base n'existe pas"
+msgstr "Le répertoire parent « %(parent)s » de l'instance de base n'existe pas"
 
 #: ../roundup/admin.py:383
 #, python-format
@@ -368,22 +367,22 @@
 "If you re-install it, you will lose all the data!\n"
 "Erase it? Y/N: "
 msgstr ""
-"ATTENTION: Il semble qu'il y ait déjà un pisteur dans \"%(tracker_home)s\"!\n"
-"Si vous le réinstallez, vous perdrez toutes les données !\n"
-"Effacer le pisteur ? Y/N: "
+"ATTENTION : il semble qu'il y ait déjà un pisteur dans « %(tracker_home)s » !\n"
+"Si vous le réinstallez, vous perdrez toutes les données !\n"
+"Supprimer le pisteur (Y/N) ? "
 
 #: ../roundup/admin.py:398
 msgid "Select template [classic]: "
-msgstr "Sélectionnez un modèle [classic]: "
+msgstr "Sélection du modèle [classic] : "
 
 #: ../roundup/admin.py:409
 msgid "Select backend [anydbm]: "
-msgstr "Sélectionnez un moteur de stockage [anydbm]: "
+msgstr "Sélection du moteur de stockage [anydbm]: "
 
 #: ../roundup/admin.py:419
 #, python-format
 msgid "Error in configuration settings: \"%s\""
-msgstr "Erreur dans les paramètres de la configuration : \"%s\""
+msgstr "Erreur dans les paramètres de la configuration : « %s »"
 
 #: ../roundup/admin.py:428
 #, python-format
@@ -393,10 +392,14 @@
 " You should now edit the tracker configuration file:\n"
 "   %(config_file)s"
 msgstr ""
+"\n"
+"---------------------------------------------------------------------------\n"
+" Vous devez maintenant modifier le fichier de configuration du pisteur :\n"
+"    %(config_file)s"
 
 #: ../roundup/admin.py:438
 msgid " ... at a minimum, you must set following options:"
-msgstr ""
+msgstr " ou au minimum, vous devez définir les options suivantes :"
 
 #: ../roundup/admin.py:443
 #, python-format
@@ -414,6 +417,21 @@
 " the above steps.\n"
 "---------------------------------------------------------------------------\n"
 msgstr ""
+"\n"
+" Si vous souhaitez modifier le schéma de la base de données, vous\n"
+" devez aussi modifier la schéma du fichier :\n"
+"   %(database_config_file)s\n"
+"\n"
+" Vous pouvez aussi modifier le fichier d'initialisation de la base de\n"
+" données :\n"
+"   %(database_init_file)s\n"
+"\n"
+" Consultez la documentation sur la personnalisation pour plus\n"
+" d'informations.\n"
+"\n"
+" Vous DEVEZ exécuter la commande « roundup-admin initialise » une fois\n"
+" que vous avez réalisé les étapes précédentes.\n"
+"---------------------------------------------------------------------------\n"
 
 #: ../roundup/admin.py:461
 msgid ""
@@ -422,6 +440,10 @@
 "        in <filename>.\n"
 "        "
 msgstr ""
+"Utilisation : genconfig <nomfichier>\n"
+"              Génère un nouveau fichier de configuration du pisteur\n"
+"              (au format ini) avec des valeurs par défaut dans\n"
+"              <nomfichier>"
 
 #. password
 #: ../roundup/admin.py:471
@@ -434,26 +456,26 @@
 "        Execute the tracker's initialisation function dbinit.init()\n"
 "        "
 msgstr ""
-"Usage: initialise [adminpw]\n"
+"Utilisation : initialise [adminpw]\n"
 "        Initialise un nouveau pisteur Roundup.\n"
 "\n"
-"        Les détails sur l'administrateur sont réglés au cours de cette\n"
-"        étape.\n"
+"        Les détails au sujet de l'administrateur sont définis au cours\n"
+"        de cette étape.\n"
 "\n"
-"        Exécute la fonction d'initialisation dbinit.init() du pisteur\n"
+"        Exécute la fonction d'initialisation dbinit.init() du pisteur.\n"
 "        "
 
 #: ../roundup/admin.py:485
 msgid "Admin Password: "
-msgstr "Mot de passe d'administrateur: "
+msgstr "Mot de passe administrateur : "
 
 #: ../roundup/admin.py:486
 msgid "       Confirm: "
-msgstr "       Confirmez: "
+msgstr "       Confirmez : "
 
 #: ../roundup/admin.py:490
 msgid "Instance home does not exist"
-msgstr "Le répertoire de base de l'instance n'existe pas"
+msgstr "Le répertoire racine de l'instance n'existe pas"
 
 #: ../roundup/admin.py:494
 msgid "Instance has not been installed"
@@ -465,9 +487,9 @@
 "If you re-initialise it, you will lose all the data!\n"
 "Erase it? Y/N: "
 msgstr ""
-"ATTENTION: La base de données est déjà initialisée !\n"
-"Si vous la réinitialisez, vous perdrez toutes les données !\n"
-"Effacer la base de données ? Y/N: "
+"ATTENTION : la base de données est déjà initialisée !\n"
+"Si vous la réinitialisez, vous perdrez toutes les données !\n"
+"Supprimez la base de données (Y/N) ? "
 
 #: ../roundup/admin.py:520
 msgid ""
@@ -478,10 +500,10 @@
 "        by the designators.\n"
 "        "
 msgstr ""
-"Usage: get property indicateur[,indicateur]*\n"
-"        Donne la propriété spécifiée d'un ou plusieurs indicateurs.\n"
+"Utilisation : get property indicateur[,indicateur]*\n"
+"        Retourne la propriété demandée d'un ou plusieurs indicateurs.\n"
 "\n"
-"        Donne la valeur de la propriété des noeuds spécifiés par\n"
+"        Retourne la valeur de la propriété des noeuds spécifiés par\n"
 "        les indicateurs.\n"
 "        "
 
@@ -490,9 +512,7 @@
 #: ../roundup/admin.py:560 ../roundup/admin.py:575
 #, python-format
 msgid "property %s is not of type Multilink or Link so -d flag does not apply."
-msgstr ""
-"la propriété %s n'est pas de type Multilien ou Lien et donc l'option -d "
-"nes'applique pas."
+msgstr "la propriété %s n'est pas de type Multilien ou Lien et donc l'option -d ne s'applique pas."
 
 # ../roundup/admin.py:581 :981 :1030 :1052
 # ../roundup/admin.py:1054 ../roundup/admin.py:583:983 :1032:1054
@@ -500,13 +520,12 @@
 #: ../roundup/admin.py:1054
 #, python-format
 msgid "no such %(classname)s node \"%(nodeid)s\""
-msgstr "le noeud \"%(nodeid)s\" de classe \"%(classname)s\" n'existe pas"
+msgstr "le noeud « %(nodeid)s » de classe « %(classname)s » n'existe pas"
 
 #: ../roundup/admin.py:585
 #, python-format
 msgid "no such %(classname)s property \"%(propname)s\""
-msgstr ""
-"la propriété \"%(propname)s\" n'existe pas pour la classe \"%(classname)s\""
+msgstr "la propriété « %(propname)s » n'existe pas pour la classe « %(classname)s »"
 
 #: ../roundup/admin.py:594
 msgid ""
@@ -523,20 +542,19 @@
 "        ids for the multilink as comma-separated numbers (ie \"1,2,3\").\n"
 "        "
 msgstr ""
-"Usage: set éléments propriété=valeur propriété=valeur ...\n"
+"Utilisation : set éléments propriété=valeur propriété=valeur ...\n"
 "        Assigne les propriétés données à un ou plusieurs éléments.\n"
 "\n"
-"        Les éléments sont spécifiés par une classe ou par une liste\n"
-"        d'indicateurs séparés par des virgules (par ex.:\n"
-"        \"indicateur[,indicateur,...]\").\n"
-"\n"
-"        Cette commande assigne les valeurs données aux propriétés de tout\n"
-"        les indicateurs spécifiés. Si la valeur est absente (par ex.:\n"
-"        \"propriété=\") alors la propriété est #effacée. Si la propriété\n"
-"        est un lien multiple, les identificateurs attachés à ce lien sont\n"
-"        spécifiés comme des nombres séparés par des virgules (par ex.: \n"
-"        \"1,2,3\").\n"
-"        "
+"        Les éléments sont indiqués par une classe ou par une liste\n"
+"        d'indicateurs séparés par des virgules (par\n"
+"        ex. « indicateur[,indicateur,...] »).\n"
+"\n"
+"        Cette commande assigne les valeurs données aux propriétés de\n"
+"        tous les indicateurs indiqués. Si la valeur est absente (par\n"
+"        ex. « propriété= ») alors la propriété est effacée. Si la\n"
+"        propriété est un lien multiple, les identificateurs attachés à\n"
+"        ce lien sont indiqués comme des nombres séparés par des\n"
+"        virgules (par ex. « 1,2,3 »)."
 
 #: ../roundup/admin.py:648
 msgid ""
@@ -548,11 +566,11 @@
 "        value.\n"
 "        "
 msgstr ""
-"Usage: find nom-de-classe propriété=valeur ...\n"
-"        Recherche les noeuds de la classe spécifiée, ayant une propriété de\n"
+"Utilisation : find nom-de-classe propriété=valeur ...\n"
+"        Recherche les noeuds de la classe indiquée, ayant une propriété de\n"
 "        lien donnée.\n"
 "\n"
-"        Recherche les noeuds de la classe spécifiée, ayant une propriété de\n"
+"        Recherche les noeuds de la classe indiquée, ayant une propriété de\n"
 "        lien donnée. La valeur peut être soit l'identificateur de noeud du\n"
 "        noeud lié, ou sa valeur de clé.\n"
 "        "
@@ -563,7 +581,7 @@
 #: ../roundup/admin.py:920
 #, python-format
 msgid "%(classname)s has no property \"%(propname)s\""
-msgstr "%(classname)s n'a pas de propriété \"%(propname)s\""
+msgstr "%(classname)s n'a pas de propriété « %(propname)s »"
 
 #: ../roundup/admin.py:708
 msgid ""
@@ -573,21 +591,21 @@
 "        This lists the properties for a given class.\n"
 "        "
 msgstr ""
-"Usage: specification nom-de-classe\n"
-"        Donne les propriétés pour un nom de classe donné.\n"
+"Utilisation : specification nom-de-classe\n"
+"        Affiche les propriétés de la classe nommée.\n"
 "\n"
-"        Cette commande énumère les propriétés d'une classe donnée.\n"
+"        Cette commande énumère les propriétés de la classe nommée.\n"
 "        "
 
 #: ../roundup/admin.py:723
 #, python-format
 msgid "%(key)s: %(value)s (key property)"
-msgstr "%(key)s: %(value)s (propriété clé)"
+msgstr "%(key)s : %(value)s (propriété clé)"
 
 #: ../roundup/admin.py:725
 #, python-format
 msgid "%(key)s: %(value)s"
-msgstr "%(key)s: %(value)s"
+msgstr "%(key)s : %(value)s"
 
 #: ../roundup/admin.py:728
 msgid ""
@@ -598,11 +616,11 @@
 "        node.\n"
 "        "
 msgstr ""
-"Usage: display indicateur[,indicateur]*\n"
-"        Donne les valeurs des propriétés pour les noeuds spécifiés.\n"
+"Utilisation : display indicateur[,indicateur]*\n"
+"        Affiche les valeurs des propriétés des noeuds indiqués.\n"
 "\n"
 "        Cette commande énumère les propriétés et leurs valeurs du ou\n"
-"        des noeuds spécifiés.\n"
+"        des noeuds indiqués.\n"
 "        "
 
 #: ../roundup/admin.py:752
@@ -621,37 +639,38 @@
 "        command.\n"
 "        "
 msgstr ""
-"Usage: create nom-de-classe propriété=valeur ...\n"
+"Utilisation : create nom-de-classe propriété=valeur ...\n"
 "        Crée une nouvelle entrée d'une classe donnée.\n"
 "\n"
-"        Cette commande crée une nouvelle entrée d'une classe spécifiée en\n"
-"        utilisant les propriétés \"nom=valeur\" données en arguments dans\n"
-"        la ligne de commande, après la commande \"create\".\n"
+"        Cette commande crée une nouvelle entrée d'une classe indiquée\n"
+"        en utilisant les propriétés « nom=valeur » données en\n"
+"        arguments de la ligne de commande, après la commande\n"
+"        « create ».\n"
 "        "
 
 #: ../roundup/admin.py:782
 #, python-format
 msgid "%(propname)s (Password): "
-msgstr "%(propname)s (Mot de passe): "
+msgstr "%(propname)s (mot de passe) : "
 
 #: ../roundup/admin.py:784
 #, python-format
 msgid "   %(propname)s (Again): "
-msgstr "   %(propname)s (À nouveau): "
+msgstr "   %(propname)s (à nouveau) : "
 
 #: ../roundup/admin.py:786
 msgid "Sorry, try again..."
-msgstr "Désolé, essayez encore..."
+msgstr "Désolé, essayez à nouveau..."
 
 #: ../roundup/admin.py:790
 #, python-format
 msgid "%(propname)s (%(proptype)s): "
-msgstr "%(propname)s (%(proptype)s): "
+msgstr "%(propname)s (%(proptype)s) : "
 
 #: ../roundup/admin.py:808
 #, python-format
 msgid "you must provide the \"%(propname)s\" property."
-msgstr "Vous devez fournir la propriété \"%(propname)s\"."
+msgstr "vous devez renseigner la propriété « %(propname)s »."
 
 #: ../roundup/admin.py:819
 msgid ""
@@ -668,20 +687,19 @@
 "        for every class instance.\n"
 "        "
 msgstr ""
-"Usage: list nom-de-classe [propriété]\n"
-"        Donne toutes les instances d'une classe.\n"
+"Utilisation: list nom-de-classe [propriété]\n"
+"        Liste toutes les instances d'une classe.\n"
 "\n"
-"        Énumère toutes les instances d'une classe donnée. Si la propriété\n"
-"        n'est pas spécifiée, la propriété \"label\" est utilisée. Cette\n"
-"        propriété étiquette est déterminée selon l'ordre suivant: la clé,\n"
-"        les propriétés \"name\", \"title\" et la première propriété par\n"
-"        ordre alphabétique.\n"
+"        Énumère toutes les instances d'une classe donnée. Si la\n"
+"        propriété n'est pas indiquée, la propriété « label » est\n"
+"        utilisée. Cette propriété étiquette est déterminée selon\n"
+"        l'ordre suivant : la clé, les propriétés « name », « title »\n"
+"        et la première propriété par ordre alphabétique.\n"
 "\n"
 "        Avec les options -c, -S ou -s, affiche une liste des\n"
-"        identificateurs d'éléments si aucune propriété n'est spécifiée.\n"
-"        Si une propriété est spécifiée, affiche une liste de cette "
-"propriété\n"
-"        pour chaque instance de cette classe.\n"
+"        identificateurs d'éléments si aucune propriété n'est indiquée.\n"
+"        Si une propriété est indiquée, affiche une liste de cette\n"
+"        propriété pour chaque instance de cette classe.\n"
 "        "
 
 #: ../roundup/admin.py:832
@@ -691,7 +709,7 @@
 #: ../roundup/admin.py:868
 #, python-format
 msgid "%(nodeid)4s: %(value)s"
-msgstr "%(nodeid)4s: %(value)s"
+msgstr "%(nodeid)4s : %(value)s"
 
 #: ../roundup/admin.py:872
 msgid ""
@@ -724,15 +742,15 @@
 "        will result in a the 4 character wide \"Name\" column.\n"
 "        "
 msgstr ""
-"Usage: table nom-de-classe [propriété[,propriété]*]\n"
+"Utilisation : table nom-de-classe [propriété[,propriété]*]\n"
 "        Liste les instances d'une classe, sous forme de tableau.\n"
 "\n"
 "        Liste toutes les instances d'une classe. Si aucune propriété n'est\n"
-"        spécifiée, toutes les propriétés sont affichées. Par défaut,\n"
+"        indiquée, toutes les propriétés sont affichées. Par défaut,\n"
 "        les largeurs de colonnes sont de la largeur de la colonne la plus\n"
-"        large. La largeur peut être spécifiée explicitement en définissant\n"
-"        la propriété comme \"nom-de-propriété:largeur\".\n"
-"        Par ex.:\n"
+"        large. La largeur peut être indiquée explicitement en définissant\n"
+"        la propriété comme « nom-de-propriété:largeur ».\n"
+"        Par exemple :\n"
 "\n"
 "          roundup> table priority id,name:10\n"
 "          Id Name\n"
@@ -742,8 +760,8 @@
 "          4  feature\n"
 "\n"
 "        De même, pour fixer la largeur de la colonne sur la largeur de\n"
-"        l'étiquette, laissez le \":\" final sans donner de largeur pour \n"
-"        la propriété. Par ex.::\n"
+"        l'étiquette, laissez le « : » final sans donner de largeur pour \n"
+"        la propriété. Par exemple :\n"
 "\n"
 "          roundup> table priority id,name:\n"
 "          Id Name\n"
@@ -752,13 +770,13 @@
 "          3  usab\n"
 "          4  feat\n"
 "\n"
-"        donnera une colonne \"Name\" large de 4 caractères.\n"
+"        donnera une colonne « Name » large de 4 caractères.\n"
 "        "
 
 #: ../roundup/admin.py:916
 #, python-format
 msgid "\"%(spec)s\" not name:width"
-msgstr "\"%(spec)s\" ne correspond pas au format \"nom:largeur\""
+msgstr "« %(spec)s » ne correspond pas au format « nom:largeur »"
 
 #: ../roundup/admin.py:966
 msgid ""
@@ -769,8 +787,8 @@
 "designator.\n"
 "        "
 msgstr ""
-"Usage: history indicateur\n"
-"        Donne l'historique pour un indicateur.\n"
+"Utilisation : history indicateur\n"
+"        Affiche le journal des entrées d'un indicateur.\n"
 "\n"
 "        Liste les entrées de journal pour le noeud identifié par\n"
 "        l'indicateur.\n"
@@ -789,16 +807,16 @@
 "        they are successful.\n"
 "        "
 msgstr ""
-"Usage: commit\n"
+"Utilisation : commit\n"
 "        Valide les changements apportés à la base de données lors d'une\n"
 "        session interactive.\n"
 "\n"
-"        Les changements effectués lors d'une session interactive ne sont\n"
-"        pas automatiquement enregistrés dans la base de données - Ils\n"
-"        doivent être validés par cette commande.\n"
+"        Les changements effectués lors d'une session interactive ne\n"
+"        sont pas automatiquement enregistrés dans la base de données -\n"
+"        ils doivent être validés par cette commande.\n"
 "\n"
-"        Les commandes \"one-off\" données en ligne de commande sont\n"
-"        automatiquement validées si elles réussissent\n"
+"        Les commandes « one-off » en ligne de commande sont\n"
+"        automatiquement validées si elles réussissent.\n"
 "        "
 
 #: ../roundup/admin.py:1001
@@ -812,15 +830,16 @@
 "        immediately after would make no changes to the database.\n"
 "        "
 msgstr ""
-"Usage: rollback\n"
-"        Annule tout les changements en attente de validation pour la base\n"
+"Utlisation : rollback\n"
+"        Annule tous les changements en attente de validation pour la base\n"
 "        de données.\n"
 "\n"
-"        Les changements effectués lors d'une session interactive ne sont\n"
-"        pas automatiquement enregistrés dans la base de données - Ils\n"
-"        doivent être validés manuellement. Cette commande annule tout ces\n"
-"        changements, de telle manière qu'une validation effectuée\n"
-"        immédiatement n'apporterait aucun changement à la base de données.\n"
+"        Les changements effectués lors d'une session interactive ne\n"
+"        sont pas automatiquement enregistrés dans la base de données -\n"
+"        ils doivent être validés manuellement. Cette commande annule\n"
+"        tout ces changements, de telle manière qu'une validation\n"
+"        effectuée immédiatement n'apporterait aucun changement à la\n"
+"        base de données.\n"
 "        "
 
 #: ../roundup/admin.py:1013
@@ -832,12 +851,12 @@
 "        by the list or find commands, and its key value may be re-used.\n"
 "        "
 msgstr ""
-"Usage: retire indicateur[,indicateur]*\n"
-"        Abandonne le noeud spécifié par l'indicateur.\n"
+"Utilisation : retire indicateur[,indicateur]*\n"
+"        Retire le noeud indiqué par l'indicateur.\n"
 "\n"
 "        Cette action indique qu'un noeud particulier ne doit plus être\n"
-"        trouvé par les commandes \"list\" ou \"find\", et que sa valeur\n"
-"        de clé peut être ré-utilisée.\n"
+"        trouvé par les commandes « list » ou « find », et que sa\n"
+"        valeur de clé peut être ré-utilisée.\n"
 "        "
 
 #: ../roundup/admin.py:1036
@@ -848,11 +867,12 @@
 "        The given nodes will become available for users again.\n"
 "        "
 msgstr ""
-"Usage: restore indicateur[,indicateur]*\n"
-"        Restaure le ou les noeuds abandonnés, spécifiés par le ou les\n"
+"Utilisaiton : restore indicateur[,indicateur]*\n"
+"        Restaure le ou les noeuds retirés, indiqués par le ou les\n"
 "        indicateurs.\n"
 "\n"
-"        Les noeuds spécifiés seront à nouveau acessibles aux utilisateurs.\n"
+"        Les noeuds indiqués seront à nouveau acessibles aux\n"
+"        utilisateurs.\n"
 "        "
 
 #. grab the directory to export to
@@ -868,11 +888,11 @@
 "        destination directory.\n"
 "        "
 msgstr ""
-"Usage: export [classe[,classe]] répertoire-d'exportation\n"
+"Utilisation : export [classe[,classe]] répertoire-d'exportation\n"
 "        Exporte la base de données vers des fichiers dans un format\n"
 "        aux valeurs séparées par des double-points.\n"
 "\n"
-"        Limite éventuellement l'exportation aux classes spécifiées.\n"
+"        Limite éventuellement l'exportation aux classes indiquées.\n"
 "\n"
 "        Cette action exporte les données actuelles de la base de données,\n"
 "        vers des fichiers placés dans le répertoire désigné, et dans un \n"
@@ -901,7 +921,7 @@
 "        database (or, tediously, retire all the old data.)\n"
 "        "
 msgstr ""
-"Usage: import répertoire-d'importation\n"
+"Utilisation: import répertoire-d'importation\n"
 "        Importe une base de données à partir d'un répertoire contenant des\n"
 "        fichiers, d'un format aux valeurs séparées par des doubles points,\n"
 "        deux par classe à importer.\n"
@@ -910,18 +930,18 @@
 "\n"
 "        <classe>.csv\n"
 "          Celui-ci définit les mêmes propriétés que la classe (avec\n"
-"          une ligne \"header\" donnant ces noms de propriétés).\n"
+"          une ligne « header » donnant ces noms de propriétés).\n"
 "        <classe>-journals.csv\n"
 "          Celui-ci définit les journaux pour les éléments importés.\n"
 "\n"
 "        Les noeuds importés auront les mêmes identificateurs de noeuds\n"
-"        (\"nodeid\") que ceux définis dans le fichier d'importation,\n"
+"        (« nodeid ») que ceux définis dans le fichier d'importation,\n"
 "        remplaçant dès lors tout contenu existant.\n"
 "\n"
 "        Les nouveaux noeuds sont ajoutés à la base de données - si, en\n"
 "        fait, vous désirez créer une nouvelle base de données avec les\n"
 "        données importées, créez plutôt une nouvelle base de données (ou,\n"
-"        plus péniblement, \"abandonnez\" toutes les anciennes données).\n"
+"        plus péniblement, « abandonnez » toutes les anciennes données).\n"
 "        "
 
 #: ../roundup/admin.py:1189
@@ -945,30 +965,30 @@
 "\n"
 "        "
 msgstr ""
-"Usage: pack période | date\n"
+"Utilisation: pack période | date\n"
 "\n"
-"        Efface les entrées de journaux antérieures à une période\n"
-"        ou à une date donnée.\n"
+"        Efface les entrées de journaux antérieures à une période ou à\n"
+"        une date donnée.\n"
 "\n"
-"        Une période est spécifiée en utilisant les suffixes \"y\" (pour\n"
-"        \"year\" - année), \"m\" (pour \"month\" - mois), et \"d\" (pour\n"
-"        \"day\" - jour).\n"
+"        Une période est indiquée en utilisant les suffixes « y » (pour\n"
+"        « year » - année), « m » (pour « month » - mois), et « d »\n"
+"        (pour « day » - jour).\n"
 "\n"
-"        Le suffixe \"w\" (pour \"week\" - semaine) signifie 7 jours.\n"
+"        Le suffixe « w » (pour « week » - semaine) signifie 7 jours.\n"
 "\n"
-"              \"3y\" signifie 3 ans\n"
-"              \"2y 1m\" signifie 2 ans et un mois\n"
-"              \"1m 25d\" signifie un an et 25 jours\n"
-"              \"2w 3d\" signifie 2 semaines et 3 jours\n"
+"              « 3y » signifie 3 ans\n"
+"              « 2y 1m » signifie 2 ans et un mois\n"
+"              « 1m 25d » signifie un an et 25 jours\n"
+"              « 2w 3d » signifie 2 semaines et 3 jours\n"
 "\n"
-"        Le format de date est \"AAAA-MM-JJ\", par ex.:\n"
+"        Le format de date est « AAAA-MM-JJ », par exemple :\n"
 "              2001-01-01\n"
 "\n"
 "        "
 
 #: ../roundup/admin.py:1217
 msgid "Invalid format"
-msgstr "Format invalide"
+msgstr "Format non valide"
 
 #: ../roundup/admin.py:1227
 msgid ""
@@ -979,17 +999,17 @@
 "        This will typically happen automatically.\n"
 "        "
 msgstr ""
-"Usage: reindex [classname|designator]*\n"
+"Utilisation: reindex [classname|designator]*\n"
 "        Regénère les index de recherche d'un pisteur.\n"
 "\n"
 "        Cette commande regénèrera les index de recherche d'un pisteur.\n"
-"        Cette manoeuvre doit normalement s'effectuer automatiquement.\n"
+"        Cette opération est normalement effectuer automatiquement.\n"
 "        "
 
 #: ../roundup/admin.py:1241
 #, python-format
 msgid "no such item \"%(designator)s\""
-msgstr ""
+msgstr "pas d'élément « %(designator)s »"
 
 #: ../roundup/admin.py:1251
 msgid ""
@@ -997,49 +1017,49 @@
 "        Display the Permissions available to one or all Roles.\n"
 "        "
 msgstr ""
-"Usage: security [nom-de-rôle]\n"
+"Utilisation : security [nom-de-rôle]\n"
 "        Affiche les permissions disponible pour un ou plusieurs rôles.\n"
 "        "
 
 #: ../roundup/admin.py:1259
 #, python-format
 msgid "No such Role \"%(role)s\""
-msgstr "Ce rôle \"%(role)s\" n'existe pas"
+msgstr "Ce rôle « %(role)s » n'existe pas"
 
 #: ../roundup/admin.py:1265
 #, python-format
 msgid "New Web users get the Roles \"%(role)s\""
-msgstr "Les nouveaux utilisateurs Web ont les rôles \"%(role)s\""
+msgstr "Les nouveaux utilisateurs Web ont les rôles « %(role)s »"
 
 #: ../roundup/admin.py:1267
 #, python-format
 msgid "New Web users get the Role \"%(role)s\""
-msgstr "Les nouveaux utilisateurs Web ont le rôle \"%(role)s\""
+msgstr "Les nouveaux utilisateurs Web ont le rôle « %(role)s »"
 
 #: ../roundup/admin.py:1270
 #, python-format
 msgid "New Email users get the Roles \"%(role)s\""
-msgstr "Les nouveaux utilisateurs Email ont les rôles \"%(role)s\""
+msgstr "Les nouveaux utilisateurs Courriel ont les rôles « %(role)s »"
 
 #: ../roundup/admin.py:1272
 #, python-format
 msgid "New Email users get the Role \"%(role)s\""
-msgstr "Les nouveaux utilisateurs Email ont le rôle \"%(role)s\""
+msgstr "Les nouveaux utilisateurs Courriel ont le rôle « %(role)s »"
 
 #: ../roundup/admin.py:1275
 #, python-format
 msgid "Role \"%(name)s\":"
-msgstr "Rôle \"%(name)s\":"
+msgstr "Rôle « %(name)s » :"
 
 #: ../roundup/admin.py:1280
 #, python-format
 msgid " %(description)s (%(name)s for \"%(klass)s\": %(properties)s only)"
-msgstr ""
+msgstr " %(description)s (%(name)s pour « %(klass)s » : %(properties)s uniquement)"
 
 #: ../roundup/admin.py:1283
 #, python-format
 msgid " %(description)s (%(name)s for \"%(klass)s\" only)"
-msgstr " %(description)s (%(name)s pour \"%(klass)s\" uniquement)"
+msgstr " %(description)s (%(name)s pour « %(klass)s » uniquement)"
 
 #: ../roundup/admin.py:1286
 #, python-format
@@ -1049,28 +1069,28 @@
 #: ../roundup/admin.py:1315
 #, python-format
 msgid "Unknown command \"%(command)s\" (\"help commands\" for a list)"
-msgstr "Commande inconnue \"%(command)s\" (\"help commands\" pour la liste)"
+msgstr "Commande inconnue « %(command)s » (« help commands » pour la liste)"
 
 #: ../roundup/admin.py:1321
 #, python-format
 msgid "Multiple commands match \"%(command)s\": %(list)s"
-msgstr "Plusieurs commandes correspondent à \"%(command)s\": %(list)s"
+msgstr "Plusieurs commandes correspondent à « %(command)s » : %(list)s"
 
 #: ../roundup/admin.py:1328
 msgid "Enter tracker home: "
-msgstr "Entrez le répertoire de base du pisteur: "
+msgstr "Saisissez le répertoire racine du pisteur : "
 
 # ../roundup/admin.py:1332 :1338 :1358
 # ../roundup/admin.py:1335:1341:1361
 #: ../roundup/admin.py:1335 ../roundup/admin.py:1341 ../roundup/admin.py:1361
 #, python-format
 msgid "Error: %(message)s"
-msgstr "Erreur: %(message)s"
+msgstr "Erreur : %(message)s"
 
 #: ../roundup/admin.py:1349
 #, python-format
 msgid "Error: Couldn't open tracker: %(message)s"
-msgstr "Erreur: impossible d'ouvrir le pisteur: %(message)s"
+msgstr "Erreur : impossible d'ouvrir le pisteur, %(message)s"
 
 #: ../roundup/admin.py:1374
 #, python-format
@@ -1078,12 +1098,12 @@
 "Roundup %s ready for input.\n"
 "Type \"help\" for help."
 msgstr ""
-"Roundup %s prêt pour l'entrée.\n"
-"Entrez \"help\" pour l'aide."
+"Roundup %s est prêt pour la saisie.\n"
+"Saisissez « help » pour l'aide."
 
 #: ../roundup/admin.py:1379
 msgid "Note: command history and editing not available"
-msgstr "Note: l'historique et l'édition des commandes n'est pas disponible"
+msgstr "Note : l'historique et l'édition des commandes n'est pas disponible"
 
 #: ../roundup/admin.py:1383
 msgid "roundup> "
@@ -1095,12 +1115,12 @@
 
 #: ../roundup/admin.py:1395
 msgid "There are unsaved changes. Commit them (y/N)? "
-msgstr "Des changements n'ont pas été enregistrés. À valider ? (y/N)? "
+msgstr "Des changements n'ont pas été enregistrés, les valider (y/N) ?"
 
 #: ../roundup/backends/back_anydbm.py:2001
 #, python-format
 msgid "WARNING: invalid date tuple %r"
-msgstr ""
+msgstr "ATTENTION : tuple de date non valide %r"
 
 #: ../roundup/backends/rdbms_common.py:1434
 msgid "create"
@@ -1120,7 +1140,7 @@
 
 #: ../roundup/backends/rdbms_common.py:1748
 msgid "retired"
-msgstr "abandonné"
+msgstr "retiré"
 
 #: ../roundup/backends/rdbms_common.py:1778
 msgid "restored"
@@ -1138,14 +1158,12 @@
 
 #: ../roundup/cgi/actions.py:91
 msgid "No ID entered"
-msgstr "Aucun identifiant entré"
+msgstr "Aucun identifiant saisi"
 
 #: ../roundup/cgi/actions.py:97
 #, python-format
 msgid "\"%(input)s\" is not an ID (%(classname)s ID required)"
-msgstr ""
-"\"%(input)s\" n'est pas un identifiant (l'identifiant de %(classname)s est "
-"requis)"
+msgstr "« %(input)s » n'est pas un identifiant (l'identifiant de %(classname)s est requis)"
 
 #: ../roundup/cgi/actions.py:117
 msgid "You may not retire the admin or anonymous user"
@@ -1154,7 +1172,7 @@
 #: ../roundup/cgi/actions.py:124
 #, python-format
 msgid "%(classname)s %(itemid)s has been retired"
-msgstr "%(classname)s %(itemid)s a été abandonné"
+msgstr "%(classname)s %(itemid)s a été retiré"
 
 # ../roundup/cgi/actions.py:174 :202
 # ../roundup/cgi/actions.py:174:202
@@ -1166,21 +1184,21 @@
 # ../roundup/cgi/actions.py:180:209
 #: ../roundup/cgi/actions.py:180 ../roundup/cgi/actions.py:209
 msgid "You do not have permission to store queries"
-msgstr "Vous n'avez pas la permission de sauvegarder des requêtes"
+msgstr "Vous n'avez pas la permission d'enregistrer des requêtes"
 
 #: ../roundup/cgi/actions.py:297
 #, python-format
 msgid "Not enough values on line %(line)s"
-msgstr "Pas assez de valeurs sur la ligne %(line)s"
+msgstr "Pas suffisament de valeurs sur la ligne %(line)s"
 
 #: ../roundup/cgi/actions.py:344
 msgid "Items edited OK"
-msgstr "Les éléments ont bien été modifiés"
+msgstr "Les éléments ont été modifiés avec succès"
 
 #: ../roundup/cgi/actions.py:404
 #, python-format
 msgid "%(class)s %(id)s %(properties)s edited ok"
-msgstr "%(class)s %(id)s %(properties)s modifié(s)"
+msgstr "%(class)s %(id)s %(properties)s modifié(s) avec succès"
 
 #: ../roundup/cgi/actions.py:407
 #, python-format
@@ -1211,14 +1229,12 @@
 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 ""
-"Erreur de modification: quelqu'un d'autre a édité ce %s (%s). Voir <a target="
-"\"new\" href=\"%s%s\">ses modifications</a> dans une nouvelle fenêtre."
+msgstr "Erreur de modification : quelqu'un d'autre a modifié ce %s (%s). Consultez <a target=\"new\" href=\"%s%s\">ses modifications</a> dans une nouvelle fenêtre."
 
 #: ../roundup/cgi/actions.py:565
 #, python-format
 msgid "Edit Error: %s"
-msgstr "Erreur de modification: %s"
+msgstr "Erreur de modification : %s"
 
 # ../roundup/cgi/actions.py:596 :607 :778 :797
 # ../roundup/cgi/actions.py:596:607 :778:797
@@ -1226,7 +1242,7 @@
 #: ../roundup/cgi/actions.py:778 ../roundup/cgi/actions.py:797
 #, python-format
 msgid "Error: %s"
-msgstr "Erreur: %s"
+msgstr "Erreur : %s"
 
 #: ../roundup/cgi/actions.py:633
 msgid ""
@@ -1234,9 +1250,8 @@
 "(a Mozilla bug may cause this message to show up erroneously, please check "
 "your email)"
 msgstr ""
-"La clé à usage unique est invalide !\n"
-"(un bug dans Mozilla peut provoquer une apparition erronée de ce message, "
-"veuillez vérifier votre courriel)"
+"La clé à usage unique n'est pas valide.\n"
+"Un bug dans Mozilla peut provoquer une apparition erronée de ce message, vérifiez votre courriel."
 
 #: ../roundup/cgi/actions.py:675
 #, python-format
@@ -1249,11 +1264,11 @@
 
 #: ../roundup/cgi/actions.py:692
 msgid "Unknown email address"
-msgstr "Adresse courriel inconnue"
+msgstr "Adresse électronique inconnue"
 
 #: ../roundup/cgi/actions.py:697
 msgid "You need to specify a username or address"
-msgstr "Vous devez spécifier un nom d'utilisateur ou une adresse courriel"
+msgstr "Vous devez indiquer un nom d'utilisateur ou une adresse électronique"
 
 #: ../roundup/cgi/actions.py:722
 #, python-format
@@ -1262,11 +1277,11 @@
 
 #: ../roundup/cgi/actions.py:741
 msgid "You are now registered, welcome!"
-msgstr "Vous êtes désormais inscrit, bienvenue !"
+msgstr "Vous êtes désormais inscrit, bienvenue !"
 
 #: ../roundup/cgi/actions.py:786
 msgid "It is not permitted to supply roles at registration."
-msgstr ""
+msgstr "Impossible de renseigner les rôles à l'inscription."
 
 #: ../roundup/cgi/actions.py:878
 msgid "You are logged out"
@@ -1280,7 +1295,7 @@
 # ../roundup/cgi/actions.py:930:934
 #: ../roundup/cgi/actions.py:930 ../roundup/cgi/actions.py:934
 msgid "Invalid login"
-msgstr "Tentative de connexion invalide"
+msgstr "Tentative de connexion non valide"
 
 #: ../roundup/cgi/actions.py:940
 msgid "You do not have permission to login"
@@ -1294,18 +1309,18 @@
 "<p class=\"help\">Debugging information follows</p>"
 msgstr ""
 "<h1>Erreur de modèle</h1>\n"
-"<p><b>%(exc_type)s</b>: %(exc_value)s</p>\n"
+"<p><b>%(exc_type)s</b> : %(exc_value)s</p>\n"
 "<p class=\"help\">Les informations de déboguage suivent</p>"
 
 #: ../roundup/cgi/cgitb.py:64
 #, python-format
 msgid "<li>\"%(name)s\" (%(info)s)</li>"
-msgstr "<li>\"%(name)s\" (%(info)s)</li>"
+msgstr "<li>« %(name)s » (%(info)s)</li>"
 
 #: ../roundup/cgi/cgitb.py:67
 #, python-format
 msgid "<li>Looking for \"%(name)s\", current path:<ol>%(path)s</ol></li>"
-msgstr "<li>Recherche de \"%(name)s\", chemin actuel:<ol>%(path)s</ol></li>"
+msgstr "<li>Recherche de « %(name)s », chemin actuel :<ol>%(path)s</ol></li>"
 
 #: ../roundup/cgi/cgitb.py:71
 #, python-format
@@ -1315,7 +1330,7 @@
 #: ../roundup/cgi/cgitb.py:76
 #, python-format
 msgid "A problem occurred in your template \"%s\"."
-msgstr "Un problème est apparu dans votre modèle \"%s\"."
+msgstr "Un problème est apparu dans votre modèle « %s »."
 
 #: ../roundup/cgi/cgitb.py:84
 #, python-format
@@ -1331,35 +1346,30 @@
 "\n"
 "<li>Lors de l'évaluation de l'expression %(info)r à la ligne %(line)d\n"
 "<table class=\"otherinfo\" style=\"font-size: 90%%\">\n"
-" <tr><th colspan=\"2\" class=\"header\">Variables courantes:</th></tr>\n"
+" <tr><th colspan=\"2\" class=\"header\">Variables actuelles :</th></tr>\n"
 " %(globals)s\n"
 " %(locals)s\n"
 "</table></li>\n"
 
 #: ../roundup/cgi/cgitb.py:103
 msgid "Full traceback:"
-msgstr "Historique complet:"
+msgstr "Historique complet :"
 
 #: ../roundup/cgi/cgitb.py:116
 #, python-format
 msgid "<font size=+1><strong>%(exc_type)s</strong>: %(exc_value)s</font>"
-msgstr "<font size=+1><strong>%(exc_type)s</strong>: %(exc_value)s</font>"
+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 ""
-"<p>Un problème est apparu lors de l'exécution d'un script Python. Voici la "
-"suite d'appels de fonction menant à l'erreur, avec l'appel le plus récent "
-"(le plus imbriqué) d'abord. Les attributs de l'exception sont:"
+msgstr "<p>Un problème est apparu lors de l'exécution d'un script Python. Voici la suite d'appels de fonction menant à l'erreur, avec l'appel le plus récent (le plus imbriqué) d'abord. Les attributs de l'exception sont :"
 
 #: ../roundup/cgi/cgitb.py:129
 msgid "&lt;file is None - probably inside <tt>eval</tt> or <tt>exec</tt>&gt;"
-msgstr ""
-"&lt;\"file\" est à \"None\" - probablement dans un <tt>eval</tt> ou un "
-"<tt>exec</tt>&gt;"
+msgstr "&lt;« file » est à « None » - probablement dans un <tt>eval</tt> ou un <tt>exec</tt>&gt;"
 
 #: ../roundup/cgi/cgitb.py:138
 #, python-format
@@ -1370,7 +1380,7 @@
 # ../roundup/cgi/cgitb.py:172:178
 #: ../roundup/cgi/cgitb.py:172 ../roundup/cgi/cgitb.py:178
 msgid "<em>undefined</em>"
-msgstr "<em>non défini</em>"
+msgstr "<em>indéfini</em>"
 
 #: ../roundup/cgi/client.py:49
 msgid ""
@@ -1388,12 +1398,12 @@
 
 #: ../roundup/cgi/client.py:308
 msgid "Form Error: "
-msgstr "Erreur de formulaire: "
+msgstr "Erreur de formulaire : "
 
 #: ../roundup/cgi/client.py:363
 #, python-format
 msgid "Unrecognized charset: %r"
-msgstr "Table de caractères non reconnue: %r"
+msgstr "Jeu de caractères non reconnu : %r"
 
 #: ../roundup/cgi/client.py:491
 msgid "Anonymous users are not allowed to use the web interface"
@@ -1414,15 +1424,12 @@
 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)sAccès au cache: %(cache_hits)d, ratés %(cache_misses)d. "
-"Chargement d'éléments: %(get_items)f secondes. Filtrage: %(filtering)f "
-"secondes.%(endtag)s\n"
+msgstr "%(starttag)sAccès au cache : %(cache_hits)d, manqués %(cache_misses)d. Chargement d'éléments : %(get_items)f secondes. Filtrage : %(filtering)f secondes.%(endtag)s\n"
 
 #: ../roundup/cgi/form_parser.py:283
 #, python-format
 msgid "link \"%(key)s\" value \"%(value)s\" not a designator"
-msgstr "la valeur \"%(value)s\" du lien \"%(key)s\" n'est pas un indicateur"
+msgstr "la valeur « %(value)s » du lien « %(key)s » n'est pas un indicateur"
 
 #: ../roundup/cgi/form_parser.py:290
 #, python-format
@@ -1434,9 +1441,7 @@
 msgid ""
 "You have submitted a %(action)s action for the property \"%(property)s\" "
 "which doesn't exist"
-msgstr ""
-"Vous avez demandé une action \"%(action)s\" sur une propriété \"%(property)s"
-"\" qui n'existe pas"
+msgstr "Vous avez demandé une action « %(action)s » sur une propriété « %(property)s » qui n'existe pas"
 
 # ../roundup/cgi/form_parser.py:331 :357
 # ../roundup/cgi/form_parser.py:331:357
@@ -1454,9 +1459,7 @@
 #: ../roundup/cgi/form_parser.py:395
 #, python-format
 msgid "property \"%(propname)s\": \"%(value)s\" not currently in list"
-msgstr ""
-"propriété \"%(propname)s\": \"%(value)s\" n'est pas actuellement dans la "
-"liste"
+msgstr "propriété « %(propname)s » : « %(value)s » n'est pas actuellement dans la liste"
 
 #: ../roundup/cgi/form_parser.py:512
 #, python-format
@@ -1502,7 +1505,7 @@
 
 #: ../roundup/cgi/templating.py:711
 msgid "New node - no history"
-msgstr "Nouveau noeud - pas d'historique"
+msgstr "Nouveau n~ud - pas d'historique"
 
 #: ../roundup/cgi/templating.py:811
 msgid "Submit Changes"
@@ -1515,7 +1518,7 @@
 #: ../roundup/cgi/templating.py:894
 #, python-format
 msgid "<em>%s: %s</em>\n"
-msgstr "<em>%s: %s</em>\n"
+msgstr "<em>%s : %s</em>\n"
 
 #: ../roundup/cgi/templating.py:907
 #, python-format
@@ -1547,18 +1550,16 @@
 #: ../roundup/cgi/templating.py:1017
 #, python-format
 msgid "%s: (no value)"
-msgstr "%s: (pas de valeur)"
+msgstr "%s : (pas de valeur)"
 
 #: ../roundup/cgi/templating.py:1029
 msgid ""
 "<strong><em>This event is not handled by the history display!</em></strong>"
-msgstr ""
-"<strong><em>Cet évènement n'est pas géré par l'affichage de l'historique !</"
-"em></strong>"
+msgstr "<strong><em>Cet évènement n'est pas géré par l'affichage de l'historique.</em></strong>"
 
 #: ../roundup/cgi/templating.py:1041
 msgid "<tr><td colspan=4><strong>Note:</strong></td></tr>"
-msgstr "<tr><td colspan=4><strong>Note:</strong></td></tr>"
+msgstr "<tr><td colspan=4><strong>Note :</strong></td></tr>"
 
 #: ../roundup/cgi/templating.py:1050
 msgid "History"
@@ -1587,7 +1588,7 @@
 
 #: ../roundup/cgi/templating.py:1331
 msgid "*encrypted*"
-msgstr "*encrypté*"
+msgstr "*crypté*"
 
 #: ../roundup/cgi/templating.py:1514
 msgid ""
@@ -1611,31 +1612,23 @@
 msgid ""
 "Not a date spec: \"yyyy-mm-dd\", \"mm-dd\", \"HH:MM\", \"HH:MM:SS\" or "
 "\"yyyy-mm-dd.HH:MM:SS.SSS\""
-msgstr ""
-"Ceci n'est pas une représentation de date: \"aaaa-mm-jj\", \"mm-jj\", \"HH:MM"
-"\", \"HH:MM:SS\" or \"aaaa-mm-jj.HH:MM:SS.SSS\""
+msgstr "Ceci n'est pas une représentation de date : « aaaa-mm-jj », « mm-jj », « HH:MM », « HH:MM:SS » or « aaaa-mm-jj.HH:MM:SS.SSS »"
 
 #: ../roundup/date.py:240
 #, 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 n'est pas une représentation de date ou d'heure \"aaaa-mm-jj\", \"mm-jj"
-"\", \"HH:MM\", \"HH:MM:SS\" or \"aaaa-mm-jj.HH:MM:SS.SSS\""
+msgstr "%r n'est pas une représentation de date ou d'heure « aaaa-mm-jj », « mm-jj », « HH:MM », « HH:MM:SS » or « aaaa-mm-jj.HH:MM:SS.SSS »"
 
 #: ../roundup/date.py:538
 msgid ""
 "Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [date spec]"
-msgstr ""
-"Ceci n'est pas une représentation d'intervalle: [+-] [#a] [#m] [#s] [#j] "
-"[[[H]H:MM]:SS] [représentation de date]"
+msgstr "Ceci n'est pas une représentation d'intervalle : [+-] [#a] [#m] [#s] [#j] [[[H]H:MM]:SS] [représentation de date]"
 
 #: ../roundup/date.py:557
 msgid "Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]"
-msgstr ""
-"Ceci n'est pas une représentation d'intervalle: [+-] [#a] [#m] [#s] [#j] "
-"[[[H]H:MM]:SS]"
+msgstr "Ceci n'est pas une représentation d'intervalle : [+-] [#a] [#m] [#s] [#j] [[[H]H:MM]:SS]"
 
 #: ../roundup/date.py:694
 #, python-format
@@ -1693,7 +1686,7 @@
 msgid "1 %(number)s/4 hours"
 msgid_plural "1 %(number)s/4 hours"
 msgstr[0] "1 heure et quart"
-msgstr[1] "1 heure trois-quart"
+msgstr[1] "1 heure %(number)s/4"
 
 #: ../roundup/date.py:727
 msgid "in a moment"
@@ -1723,12 +1716,12 @@
 msgid "%(number)s/4 hour"
 msgid_plural "%(number)s/4 hours"
 msgstr[0] "un quart d'heure"
-msgstr[1] "trois quarts d'heure"
+msgstr[1] "%(number)s/4 d'heures"
 
 #: ../roundup/date.py:744
 #, python-format
 msgid "%s ago"
-msgstr "il y a %s"
+msgstr "Il y a %s"
 
 #: ../roundup/date.py:746
 #, python-format
@@ -1741,16 +1734,14 @@
 "WARNING: directory '%s'\n"
 "\tcontains old-style template - ignored"
 msgstr ""
-"ATTENTION: le répertoire '%s'\n"
+"ATTENTION : le répertoire '%s'\n"
 "\tcontient des modèles obsolètes - ignoré"
 
 #: ../roundup/mailgw.py:586
 msgid ""
 "\n"
 "Emails to Roundup trackers must include a Subject: line!\n"
-msgstr ""
-"\n"
-"Les emails envoyés au gestionnaire de ticket doivent comporter un sujet !\n"
+msgstr "\nLes courriels envoyés au gestionnaire de ticket doivent comporter un sujet !\n"
 
 #: ../roundup/mailgw.py:674
 #, python-format
@@ -1771,15 +1762,16 @@
 "\n"
 "Le sujet du message que vous avez envoyé au gestionnaire de ticket n'était\n"
 "pas correct. Le sujet doit contenir le nom d'une classe ou d'un objet. Par\n"
-"exemple : \n"
-"   Subject: [issue] un nouveau ticket\n"
-"     - Cela créera dans le gestionnaire un nouveau ticket dont le titre \n"
-"       sera 'un nouveau ticket'.\n"
-"   Subject: [issue1234] réponse au ticket 1234\n"
-"     - Cela ajoutera le corps du message au ticket 1234 déjà présent dans \n"
+"exemple : \n"
+"   Sujet: [issue] Un nouveau ticket\n"
+"     - créera dans le gestionnaire un nouveau ticket dont le titre\n"
+"       sera « Un nouveau ticket ».\n"
+"\n"
+"   Sujet: [issue1234] Réponse au ticket 1234\n"
+"     - ajoutera le corps du message au ticket 1234 déjà présent dans \n"
 "       le gestionnaire.\n"
 "\n"
-"Sujet original : '%(subject)s'\n"
+"Sujet original : '%(subject)s'\n"
 
 #: ../roundup/mailgw.py:705
 #, python-format
@@ -1793,11 +1785,11 @@
 "Subject was: \"%(subject)s\"\n"
 msgstr ""
 "\n"
-"Le nom de la classe identifiée par dans le sujet (\"%(classname)s\")\n"
+"Le nom de la classe identifiée dans le sujet (« %(classname)s »)\n"
 "n'existe pas dans la base de données.\n"
 "\n"
-"Les noms de classes valides sont : %(validname)s\n"
-"Sujet original : '%(subject)s'\n"
+"Les noms de classes valides sont : %(validname)s\n"
+"Sujet original : « %(subject)s »\n"
 
 #: ../roundup/mailgw.py:733
 #, python-format
@@ -1811,11 +1803,11 @@
 msgstr ""
 "\n"
 "Impossible d'associer votre message à un objet de la base de données.\n"
-"Vous devez soit fournir un le nom d'une classe avec un numéro (par exemple\n"
-"\"[issue123]\"), soit garder le sujet du précédent message tel quel pour\n"
-"pouvoir effectuer la correspondance.\n"
+"Vous devez soit fournir soit le nom d'une classe avec un numéro (par\n"
+"exemple \"[issue123]\"), soit garder le sujet du précédent message tel\n"
+"quel pour pouvoir effectuer la correspondance.\n"
 "\n"
-"Sujet original : '%(subject)s'\n"
+"Sujet original : « %(subject)s »\n"
 
 #: ../roundup/mailgw.py:766
 #, python-format
@@ -1827,10 +1819,10 @@
 "Subject was: \"%(subject)s\"\n"
 msgstr ""
 "\n"
-"L'objet spécifié dans le sujet de votre message (\"%nodeid)s\")\n"
+"L'objet indiqué dans le sujet de votre message (« %(nodeid)s »)\n"
 " n'existe pas.\n"
 "\n"
-"Sujet original : '%(subject)s'\n"
+"Sujet original : « %(subject)s »\n"
 
 #: ../roundup/mailgw.py:794
 #, python-format
@@ -1840,10 +1832,11 @@
 "%(mailadmin)s and have them fix the incorrect class specified as:\n"
 "  %(current_class)s\n"
 msgstr ""
-"La passerelle mail ne fonctionne pas correctement. Veuillez contacter\n"
-"%(mailadmin)s pour régler ce problème.\n"
 "\n"
-"classe incorrecte :  %(current_class)s\n"
+"La passerelle courriel ne fonctionne pas correctement. Contactez\n"
+"%(mailadmin)s afin qu'il corrige la classe incorrecte qui a été\n"
+"indiquée comme : \n"
+"  %(current_class)s\n"
 
 #: ../roundup/mailgw.py:817
 #, python-format
@@ -1853,10 +1846,11 @@
 "%(mailadmin)s and have them fix the incorrect properties:\n"
 "  %(errors)s\n"
 msgstr ""
-"La passerelle mail ne fonctionne pas correctement. Veuillez contacter\n"
-"%(mailadmin)s pour régler ce problème.\n"
 "\n"
-"propriétés incorrectes :  %(errors)s\n"
+"La passerelle courriel ne fonctionne pas correctement. Contactez\n"
+"%(mailadmin)s afin que les propriétés incorrectes suivantes soient\n"
+"corrigés :\n"
+"  %(errors)s\n"
 
 #: ../roundup/mailgw.py:847
 #, python-format
@@ -1867,14 +1861,13 @@
 "Unknown address: %(from_address)s\n"
 msgstr ""
 "\n"
-"Vous n'êtes pas enregistrés.\n"
+"Vous n'êtes pas un utilisateur inscrit.\n"
 "\n"
-"addresse inconnue : %(from_address)s\n"
+"Addresse inconnue : %(from_address)s\n"
 
 #: ../roundup/mailgw.py:855
-#, fuzzy
 msgid "You are not permitted to access this tracker."
-msgstr "Vous n'&ecirc;tes pas autoris&eacute; &agrave; voir cette page."
+msgstr "Vous n'êtes pas autorisé à accéder à ce pisteur."
 
 #: ../roundup/mailgw.py:862
 #, python-format
@@ -1895,9 +1888,11 @@
 "\n"
 "Subject was: \"%(subject)s\"\n"
 msgstr ""
-"Une erreur s'est produite lors du traitement du sujet :- %(errors)s\n"
 "\n"
-"Sujet original : '%(subject)s'\n"
+"Une erreur s'est produite lors du traitement de la liste de sujets :\n"
+"- %(errors)s\n"
+"\n"
+"Le sujet était « %(subject)s »\n"
 
 #: ../roundup/mailgw.py:942
 msgid ""
@@ -1905,13 +1900,13 @@
 "Roundup requires the submission to be plain text. The message parser could\n"
 "not find a text/plain part to use.\n"
 msgstr ""
-"Le message soumis doit être en texte brut. L'analyse du message n'a pas "
-"trouvé\n"
-"trouvé de partie en text/plain.\n"
+"\n"
+"Le message soumis doit être en texte brut. L'analyse du message n'a pas trouvé\n"
+"de partie text/plain à utiliser.\n"
 
 #: ../roundup/mailgw.py:964
 msgid "You are not permitted to create files."
-msgstr "Vous n'êtes pas autorisé à créer des fichier"
+msgstr "Vous n'êtes pas autorisé à créer des fichiers."
 
 #: ../roundup/mailgw.py:978
 #, python-format
@@ -1955,7 +1950,7 @@
 "   %(message)s\n"
 msgstr ""
 "\n"
-"Il y a eu un problème lors de la réception de votre email :\n"
+"Un problème a eu lieu à l'envoi de votre message :\n"
 "   %(message)s\n"
 
 #: ../roundup/mailgw.py:1069
@@ -1984,7 +1979,7 @@
 
 #: ../roundup/roundupdb.py:143
 msgid "assignedto"
-msgstr "assigné_à"
+msgstr "affecté_à"
 
 #: ../roundup/roundupdb.py:143
 msgid "priority"
@@ -2019,29 +2014,26 @@
 #: ../roundup/roundupdb.py:304
 #, python-format
 msgid "New submission from %(authname)s%(authaddr)s:"
-msgstr "Nouvelle soumission de %(authname)s%(authaddr)s:"
+msgstr "Nouvel envoi de %(authname)s%(authaddr)s :"
 
 #: ../roundup/roundupdb.py:307
 #, python-format
 msgid "%(authname)s%(authaddr)s added the comment:"
-msgstr "%(authname)s%(authaddr)s a ajouté le commentaire:"
+msgstr "%(authname)s%(authaddr)s a ajouté le commentaire :"
 
 #: ../roundup/roundupdb.py:310
-#, fuzzy
 msgid "System message:"
-msgstr "Nouveau message"
+msgstr "Message système :"
 
 #: ../roundup/scripts/roundup_demo.py:32
 #, python-format
 msgid "Enter directory path to create demo tracker [%s]: "
-msgstr ""
-"Saisissez le chemin du répertoire dans lequel créer le pisteur de "
-"démonstration [%s]: "
+msgstr "Saisissez le chemin du répertoire où créer le pisteur de démonstration [%s] : "
 
 #: ../roundup/scripts/roundup_gettext.py:22
 #, python-format
 msgid "Usage: %(program)s <tracker home>"
-msgstr "Usage: %(program)s <répertoire du pisteur>"
+msgstr "Utilisation : %(program)s <répertoire du pisteur>"
 
 #: ../roundup/scripts/roundup_gettext.py:37
 #, python-format
@@ -2116,98 +2108,89 @@
 "    imaps username:password at server [mailbox]\n"
 "\n"
 msgstr ""
-"Usage: %(program)s [-v] [[-C classe] -S champ=valeur]* <base de l'instance> "
-"[méthode]\n"
+"Utilisation : %(program)s [-v] [[-C classe] -S champ=valeur]* <base de l'instance> [méthode]\n"
 "\n"
-"Options:\n"
-" -v: imprime la version et quitte\n"
-" -c: classe de l'élement à créer (par défaut, la classe MAIL_DEFAULT_CLASS)\n"
-" -C / -S: voir ci-après\n"
+"Options :\n"
+" -v : imprime la version et quitte.\n"
+" -c : classe de l'élement à créer (par défaut, la classe MAIL_DEFAULT_CLASS).\n"
+" -C / -S : voir ci-dessous.\n"
 "\n"
-"La passerelle de messagerie de Roundup peut être appelée de quatre "
-"manières:\n"
+"La passerelle de messagerie de Roundup peut être appelée de quatre façons :\n"
 " . avec le répertoire de base d'une instance comme seul argument,\n"
-" . avec à la fois un répertoire de base et un fichier d'attente de "
-"messagerie,\n"
-" . avec à la fois un répertoire de base et un compte de serveur POP/APOP, "
-"ou\n"
+" . avec à la fois un répertoire de base et un fichier d'attente de messagerie,\n"
+" . avec à la fois un répertoire de base et un compte de serveur POP/APOP, ou\n"
 " . avec à la fois un répertoire de base et un compte de serveur IMAP/IMAPS.\n"
 "\n"
-"Elle accepte également les options -C et -S qui vous permettent d'assigner\n"
-"des champs pour une classe créée par roundup-mailgw. La classe par défaut, "
-"si\n"
-"elle n'est pas spécifiée, est \"msg\", mais les autres classes: \"issue\",\n"
-"\"file\", \"user\" peuvent également être utilisées. Les options -S ou --"
-"set \n"
-"utilisent la même notation propriété=valeur[;propriété=valeur] acceptée par "
-"la\n"
-"ligne de commande de  Roundup ou par les commandes qui peuvent être données\n"
-"dans l'objet d'un courriel.\n"
-"\n"
-"Elle vous permet également de spécifier le type de message pour chaque "
-"adresse\n"
-"de messagerie.\n"
+"Elle accepte également les options -C et -S qui vous permettent\n"
+"d'assigner des champs pour une classe créée par roundup-mailgw. La\n"
+"classe par défaut, si elle n'est pas spécifiée, est « msg », mais les\n"
+"autres classes : « issue » (anomalie), « file » (fichier), « user »\n"
+"(utilisateur) peuvent également être utilisées. Les options -S ou\n"
+"--set utilisent la même notation propriété=valeur[;propriété=valeur]\n"
+"acceptée par la ligne de commande de Roundup ou par les commandes qui\n"
+"peuvent être données dans l'objet d'un courriel.\n"
 "\n"
-"PIPE:\n"
-" Dans le premier cas, la passerelle de messagerie lit un seul message "
-"venant\n"
+"Elle vous permet également de spécifier le type de message pour chaque\n"
+"adresse de messagerie.\n"
+"\n"
+"PIPE :\n"
+" Dans le premier cas, la passerelle de messagerie lit un seul message venant\n"
 " de l'entrée standard et le soumet au module roundup.mailgw.\n"
 "\n"
-"UNIX mailbox:\n"
+"UNIX mailbox :\n"
 " Dans le second cas, la passerelle lit tout les messages venant du fichier\n"
 " d'attente de messagerie et les soumet chacun à leur tour au module\n"
-" roundup.mailgw. Le fichier est vidé une fois que tout les messages ont été\n"
-" traités avec succés. Le fichier est spécifié comme:\n"
+" roundup.mailgw. Le fichier est vidé une fois que tous les messages ont été\n"
+" traités avec succès. Le fichier est indiqué comme:\n"
 "   mailbox /chemin/vers/mailbox\n"
 "\n"
-"POP:\n"
+"POP :\n"
 " Dans le troisième cas, la passerelle lit tout les messages du serveur POP\n"
-" spécifié et les soumet chacun à leur tour au module roundup.mailgw. Le\n"
-" serveur est spécifié comme suit:\n"
+" indiqué et les soumet chacun à leur tour au module roundup.mailgw. Le\n"
+" serveur est renseigné comme suit :\n"
 "    pop nom-d'utilisateur:mot-de-passe at serveur\n"
-" Le nom d'utilisateur et le mot de passe peuvent être omis:\n"
+" Le nom d'utilisateur et le mot de passe peuvent être omis :\n"
 "    pop nom-d'utilisateur at serveur\n"
 "    pop server\n"
 " sont tous deux valides. Le nom d'utilisateur et/ou le mot de passe seront\n"
 " demandés s'ils ne sont pas fournis dans la ligne de commande.\n"
 "\n"
-"APOP:\n"
-" Identique à POP, mais utilisant le POP authentifié:\n"
+"APOP :\n"
+" Identique à POP, mais utilisant le POP authentifié :\n"
 "    apop nom-d'utilisateur:mot-de-passe at serveur\n"
 "\n"
-"IMAP:\n"
-" Se connecte à un serveur IMAP. Supporte la même notation que pour la\n"
-" messagerie POP\n"
+"IMAP :\n"
+" Se connecte à un serveur IMAP. Il prend en charge la même notation\n"
+" que pour la messagerie POP\n"
+"\n"
 "    imap nom-d'utilisateur:mot-de-passe at serveur\n"
-" Vous permet également de spécifier une boîte aux lettres spécifique, autre\n"
-" que INBOX, en utilisant ce format:\n"
+" Il permet également d'indiquer une boîte aux lettres spécifique, autre\n"
+" que INBOX, en utilisant ce format :\n"
 "    imap nom-d'utilisateur:mot-de-passe at serveur boîte-aux-lettres\n"
 "\n"
-"IMAPS:\n"
+"IMAPS :\n"
 " Se connecte avec SSL à un serveur IMAP.\n"
-" Supporte la même notation que IMAP.\n"
+" Prend en charge la même notation que IMAP.\n"
 "    imaps nom-d'utilisateur:mot-de-passe at serveur [boîte-aux-lettres]\n"
 "\n"
 
 #: ../roundup/scripts/roundup_mailgw.py:147
 msgid "Error: not enough source specification information"
-msgstr "Erreur: pas assez d'informations dans la spécification de la source"
+msgstr "Erreur : pas suffisament d'informations dans la spécification de la source"
 
 #: ../roundup/scripts/roundup_mailgw.py:163
 msgid "Error: pop specification not valid"
-msgstr "Erreur: la spécification pop n'est pas valide"
+msgstr "Erreur : la spécification pop n'est pas valide"
 
 #: ../roundup/scripts/roundup_mailgw.py:170
 msgid "Error: apop specification not valid"
-msgstr "Erreur: la spécification apop n'est pas valide"
+msgstr "Erreur : la spécification apop n'est pas valide"
 
 #: ../roundup/scripts/roundup_mailgw.py:184
 msgid ""
 "Error: The source must be either \"mailbox\", \"pop\", \"apop\", \"imap\" or "
 "\"imaps\""
-msgstr ""
-"Erreur: la source doit être \"mailbox\", \"pop\", \"apop\", \"imap\" ou "
-"\"imaps\""
+msgstr "Erreur : la source doit être « mailbox », « pop », « apop », « imap » ou « imaps »"
 
 #: ../roundup/scripts/roundup_server.py:157
 msgid ""
@@ -2220,13 +2203,11 @@
 #: ../roundup/scripts/roundup_server.py:287
 #, python-format
 msgid "Error: %s: %s"
-msgstr "Erreur: %s: %s"
+msgstr "Erreur : %s: %s"
 
 #: ../roundup/scripts/roundup_server.py:297
 msgid "WARNING: ignoring \"-g\" argument, not root"
-msgstr ""
-"ATTENTION: le paramètre \"-g\" est ignoré, vous n'êtes pas superutilisateur "
-"(\"root\")"
+msgstr "ATTENTION : le paramètre « -g » est ignoré, vous n'êtes pas superutilisateur (« root »)"
 
 #: ../roundup/scripts/roundup_server.py:303
 msgid "Can't change groups - no grp module"
@@ -2284,7 +2265,7 @@
 "               préciser les répertoires des pisteurs.\n"
 "               L'option Logfile est requise pour exécuter le service\n"
 "               RoundUp Tracker.\n"
-"               La commande \"roundup-server -c help\" donne les\n"
+"               La commande « roundup-server -c help » donne les\n"
 "               spécificités du service Windows."
 
 #: ../roundup/scripts/roundup_server.py:569
@@ -2360,6 +2341,59 @@
 "   pairs on the command-line. Make sure the name part doesn't include\n"
 "   any url-unsafe characters like spaces, as these confuse IE.\n"
 msgstr ""
+"%(message)sUtilisation : roundup-server [options] [name=tracker home]*\n"
+"\n"
+"Options :\n"
+" -v             affiche le numéro de version Roundup et quitte\n"
+" -h             affiche ce texte et quitte\n"
+" -S             crée ou met à jour le fichier de configuration et quitte\n"
+" -C <fichier>   utilise le fichier de configuration <fichier>\n"
+" -n <nom>       définit le nom d'hôte de l'instance Web Roundup\n"
+" -p <port>      définit le port d'écoute (par défaut %(port)s)\n"
+" -l <fichier>   historise dans le fichier indiqué par <fichier> au lieu de \n"
+"                stderr/stdout\n"
+" -N             historise le nom d'hôte client au lieu des adresses IP \n"
+"                (beaucoup plus lent)\n"
+" -t <mode>      mode multi-processus (par défaut %(mp_def)s). Valeurs\n"
+"                utilisées : %(mp_types)s.\n"
+"\n"
+"%(os_part)s\n"
+"\n"
+"Options longues :\n"
+" --version            affiche le numéro de version Roundup et quitte\n"
+" --help               affiche ce texte et quitte\n"
+" --save-config        crée ou met à jour le fichier de configuration et quitte\n"
+" --config <fichier>   utilise le fichier de configuration <fichier>\n"
+"\n"
+"Exemples :\n"
+" roundup-server -S -C /opt/roundup/etc/roundup-server.ini\n"
+"    -n localhost -p 8917 -l /var/log/roundup.log\n"
+"    support=/var/spool/roundup-trackers/support\n"
+"\n"
+" roundup-server -C /opt/roundup/etc/roundup-server.ini\n"
+"\n"
+" roundup-server support=/var/spool/roundup-trackers/support\n"
+"\n"
+" roundup-server -d /var/run/roundup.pid -l /var/log/roundup.log\n"
+"    support=/var/spool/roundup-trackers/support\n"
+"\n"
+"Format du fichier de configuration :\n"
+"\n"
+"   Le fichier de configuration du serveur Roundup utilise le format de\n"
+"   fichier commun .ini. Le fichier de configuration créé par\n"
+"   « roundup-server -S » contient les explications détaillées de\n"
+"   chaque option. Consultez ce fichier pour la description des\n"
+"   options.\n"
+"\n"
+"Utilisation de « name=racine du pisteur » :\n"
+"   \n"
+"    Ces arguments définissent la racine du pisteur à utiliser. Le nom\n"
+"    est celui utilisé pour identifier le pisteur dans l'URL (première\n"
+"    partie du chemin de l'URL). La racine du pisteur est le répertoire\n"
+"    qui a été identifié quand vous exécutez « roundup-admin list ». Il\n"
+"    est possible de fournir autant de paires « name=racine » que\n"
+"    souhaité. Assurez-vous que « name » ne contienne pas de caractères\n"
+"    inappropriés pour une URL, comme les espaces qui perturbe IE.\n"
 
 #: ../roundup/scripts/roundup_server.py:723
 msgid "Instances must be name=home"
@@ -2384,12 +2418,12 @@
 #: ../templates/classic/html/_generic.collision.html:4
 #: ../templates/minimal/html/_generic.collision.html:4
 msgid "${class} Edit Collision - ${tracker}"
-msgstr "&Eacute;dition des collisions pour ${class} - ${tracker}"
+msgstr "Modification des collisions pour ${class} - ${tracker}"
 
 #: ../templates/classic/html/_generic.collision.html:7
 #: ../templates/minimal/html/_generic.collision.html:7
 msgid "${class} Edit Collision"
-msgstr "&Eacute;dition des collisions pour ${class}"
+msgstr "Modification des collisions pour ${class}"
 
 #: ../templates/classic/html/_generic.collision.html:14
 #: ../templates/minimal/html/_generic.collision.html:14
@@ -2400,15 +2434,15 @@
 "  the node and review your edits.\n"
 msgstr ""
 "\n"
-"  Une collision s'est produite. Un autre utilisateur a mis &agrave; jour ce\n"
-"  noeud pendant que vous l'&eacute;ditiez. Veuillez <a "
-"href='${context}'>recharger</a>\n"
-"  ce noeud et v&eacute;rifier vos modifications.\n"
+"  Une collision s'est produite. Un autre utilisateur a mis &agrave;\n"
+"  jour ce noeud pendant que vous étiez en train de la\n"
+"  modifier. Veuillez <a href='${context}'>actualiser</a> ce noeud et\n"
+"  v&eacute;rifier vos modifications.\n"
 
 #: ../templates/classic/html/_generic.help.html:9
 #: ../templates/minimal/html/_generic.help.html:9
 msgid "${property} help - ${tracker}"
-msgstr "Aide concernant ${property} - ${tracker}"
+msgstr "Aide à propos de « ${property} » - ${tracker}"
 
 #: ../templates/classic/html/_generic.help.html:31
 #: ../templates/minimal/html/_generic.help.html:31
@@ -2443,14 +2477,14 @@
 #: ../templates/minimal/html/_generic.index.html:6
 #: ../templates/minimal/html/_generic.item.html:4
 msgid "${class} editing - ${tracker}"
-msgstr "&Eacute;dition de ${class} - ${tracker}"
+msgstr "Modification de ${class} - ${tracker}"
 
 #: ../templates/classic/html/_generic.index.html:9
 #: ../templates/classic/html/_generic.item.html:7
 #: ../templates/minimal/html/_generic.index.html:9
 #: ../templates/minimal/html/_generic.item.html:7
 msgid "${class} editing"
-msgstr "&Eacute;dition de ${class}"
+msgstr "Modification de ${class}"
 
 #: ../templates/classic/html/_generic.index.html:14
 #: ../templates/classic/html/_generic.item.html:12
@@ -2479,20 +2513,7 @@
 "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 ""
-"<p class=\"form-help\"> Vous pouvez &eacute;diter le contenu de la classe "
-"${classname} en utilisant ce formulaire. Les virgules, passages &agrave; la "
-"ligne guillemets doubles (\") doivent &ecirc;tre g&eacute;r&eacute;s "
-"soigneusement. Vous pouvez ins&eacute;rer des virgules et des passage "
-"&agrave; la ligne en ins&eacute;rant les valeurs dans des guillemets doubles "
-"(\"). Les guillemets doubles elles-m&ecirc;mes doivent &ecirc;tre ins&eacute;"
-"r&eacute;es en les doublant (\"\").</p><p class=\"form-help\">Les "
-"propri&eacute;t&eacute;s des liens multiples doivent s&eacute;parerleurs "
-"valeurs multiples par des double-points (\":\") (... ,\"un:deux:trois"
-"\", ...) </p><p class=\"form-help\"> Enlevez des entr&eacute;es en "
-"effa&ccedil;ant leur ligne. Ajoutez de nouvelles entr&eacute;es en les "
-"ajoutant &agrave; la fin de la table - mettez un \"X\" dans la colonne \"id"
-"\".</p>"
+msgstr "<p class=\"form-help\"> Vous pouvez modifier le contenu de la classe ${classname} en utilisant ce formulaire. Les virgules, passages &agrave; la ligne guillemets doubles (\") doivent &ecirc;tre g&eacute;r&eacute;s soigneusement. Vous pouvez ins&eacute;rer des virgules et des passage &agrave; la ligne en ins&eacute;rant les valeurs dans des guillemets doubles (\"). Les guillemets doubles eux-m&ecirc;mes doivent &ecirc;tre ins&eacute;r&eacute;s en les doublant (\"\").</p><p class=\"form-help\">Les propri&eacute;t&eacute;s des liens multiples doivent s&eacute;parer leurs valeurs multiples par des double-points « : » (... , \"un:deux:trois\", ...) </p><p class=\"form-help\"> Enlevez des entr&eacute;es en effa&ccedil;ant leur ligne. Ajoutez de nouvelles entr&eacute;es en les ajoutant &agrave; la fin de la table - mettez un « X » dans la colonne « id ».</p>"
 
 #: ../templates/classic/html/_generic.index.html:44
 #: ../templates/minimal/html/_generic.index.html:44
@@ -2555,11 +2576,11 @@
 
 #: ../templates/classic/html/issue.index.html:7
 msgid "List of issues - ${tracker}"
-msgstr "Liste des demandes - ${tracker}"
+msgstr "Liste des anomalies - ${tracker}"
 
 #: ../templates/classic/html/issue.index.html:11
 msgid "List of issues"
-msgstr "Liste des demandes"
+msgstr "Liste des anomalies"
 
 #: ../templates/classic/html/issue.index.html:22
 #: ../templates/classic/html/issue.item.html:44
@@ -2602,7 +2623,7 @@
 
 #: ../templates/classic/html/issue.index.html:31
 msgid "Assigned&nbsp;To"
-msgstr "Assign&eacute;&nbsp;&agrave;"
+msgstr "Affect&eacute;&nbsp;&agrave;"
 
 #: ../templates/classic/html/issue.index.html:97
 msgid "Download as CSV"
@@ -2610,7 +2631,8 @@
 
 #: ../templates/classic/html/issue.index.html:105
 msgid "Sort on:"
-msgstr "Trier par:"
+msgstr "Trier par :"
+
 
 #: ../templates/classic/html/issue.index.html:108
 #: ../templates/classic/html/issue.index.html:125
@@ -2624,35 +2646,35 @@
 
 #: ../templates/classic/html/issue.index.html:122
 msgid "Group on:"
-msgstr "Grouper par:"
+msgstr "Regrouper par :"
 
 #: ../templates/classic/html/issue.index.html:139
 msgid "Redisplay"
-msgstr "Ré-afficher"
+msgstr "Actualiser"
 
 #: ../templates/classic/html/issue.item.html:7
 msgid "Issue ${id}: ${title} - ${tracker}"
-msgstr "Demande ${id}: ${title} - ${tracker}"
+msgstr "Anomalie ${id} : ${title} - ${tracker}"
 
 #: ../templates/classic/html/issue.item.html:10
 msgid "New Issue - ${tracker}"
-msgstr "Nouvelle demande - ${tracker}"
+msgstr "Nouvelle anomalie - ${tracker}"
 
 #: ../templates/classic/html/issue.item.html:14
 msgid "New Issue"
-msgstr "Nouvelle demande"
+msgstr "Nouvelle anomalie"
 
 #: ../templates/classic/html/issue.item.html:16
 msgid "New Issue Editing"
-msgstr "Édition d'une nouvelle demande"
+msgstr "Création d'une nouvelle anomalie"
 
 #: ../templates/classic/html/issue.item.html:19
 msgid "Issue${id}"
-msgstr "Issue${id}"
+msgstr "Anomalie ${id}"
 
 #: ../templates/classic/html/issue.item.html:22
 msgid "Issue${id} Editing"
-msgstr "&Eacute;dition de la demande Issue${id}"
+msgstr "Modification de l'anomalie ${id}"
 
 #: ../templates/classic/html/issue.item.html:51
 msgid "Superseder"
@@ -2660,7 +2682,7 @@
 
 #: ../templates/classic/html/issue.item.html:56
 msgid "View: ${link}"
-msgstr "Voir: ${link}"
+msgstr "Voir : ${link}"
 
 #: ../templates/classic/html/issue.item.html:60
 msgid "Nosy List"
@@ -2668,7 +2690,7 @@
 
 #: ../templates/classic/html/issue.item.html:69
 msgid "Assigned To"
-msgstr "Assign&eacute; &agrave;"
+msgstr "Affect&eacute; &agrave;"
 
 #: ../templates/classic/html/issue.item.html:71
 msgid "Topics"
@@ -2732,13 +2754,13 @@
 
 #: ../templates/classic/html/issue.item.html:131
 msgid "Remove"
-msgstr "Effacer"
+msgstr "Supprimer"
 
 #: ../templates/classic/html/issue.item.html:151
 #: ../templates/classic/html/issue.item.html:172
 #: ../templates/classic/html/query.edit.html:50
 msgid "remove"
-msgstr "enlever"
+msgstr "supprimer"
 
 #: ../templates/classic/html/issue.item.html:158
 #: ../templates/classic/html/msg.index.html:9
@@ -2751,19 +2773,19 @@
 
 #: ../templates/classic/html/issue.item.html:163
 msgid "Author: ${author}"
-msgstr "Auteur: ${author}"
+msgstr "Auteur : ${author}"
 
 #: ../templates/classic/html/issue.item.html:165
 msgid "Date: ${date}"
-msgstr "Date: ${date}"
+msgstr "Date : ${date}"
 
 #: ../templates/classic/html/issue.search.html:2
 msgid "Issue searching - ${tracker}"
-msgstr "Recherche de demande - ${tracker}"
+msgstr "Recherche de l'anomalie - ${tracker}"
 
 #: ../templates/classic/html/issue.search.html:4
 msgid "Issue searching"
-msgstr "Recherche de demande"
+msgstr "Recherche de l'anomalie"
 
 #: ../templates/classic/html/issue.search.html:25
 msgid "Filter on"
@@ -2783,7 +2805,7 @@
 
 #: ../templates/classic/html/issue.search.html:32
 msgid "All text*:"
-msgstr "Tout le texte*:"
+msgstr "Tout le texte* :"
 
 #: ../templates/classic/html/issue.search.html:40
 msgid "Title:"
@@ -2799,7 +2821,7 @@
 
 #: ../templates/classic/html/issue.search.html:66
 msgid "Creation Date:"
-msgstr "Date de cr&eacute;ation:"
+msgstr "Date de cr&eacute;ation :"
 
 #: ../templates/classic/html/issue.search.html:77
 msgid "Creator:"
@@ -2811,7 +2833,7 @@
 
 #: ../templates/classic/html/issue.search.html:88
 msgid "Activity:"
-msgstr "Activit&eacute;:"
+msgstr "Activit&eacute; :"
 
 #: ../templates/classic/html/issue.search.html:99
 msgid "Actor:"
@@ -2823,7 +2845,7 @@
 
 #: ../templates/classic/html/issue.search.html:112
 msgid "Priority:"
-msgstr "Priorit&eacute;:"
+msgstr "Priorit&eacute; :"
 
 #: ../templates/classic/html/issue.search.html:114
 #: ../templates/classic/html/issue.search.html:130
@@ -2832,7 +2854,7 @@
 
 #: ../templates/classic/html/issue.search.html:125
 msgid "Status:"
-msgstr "&Eacute;tat:"
+msgstr "&Eacute;tat :"
 
 #: ../templates/classic/html/issue.search.html:128
 msgid "not resolved"
@@ -2840,39 +2862,39 @@
 
 #: ../templates/classic/html/issue.search.html:143
 msgid "Assigned to:"
-msgstr "Assign&eacute; &agrave;:"
+msgstr "Affect&eacute; &agrave; :"
 
 #: ../templates/classic/html/issue.search.html:146
 msgid "assigned to me"
-msgstr "assign&eacute; &agrave; moi"
+msgstr "affect&eacute; &agrave; moi"
 
 #: ../templates/classic/html/issue.search.html:148
 msgid "unassigned"
-msgstr "non assign&eacute;"
+msgstr "non affect&eacute;"
 
 #: ../templates/classic/html/issue.search.html:158
 msgid "No Sort or group:"
-msgstr "Aucun tri ou groupe:"
+msgstr "Aucun tri ou groupe :"
 
 #: ../templates/classic/html/issue.search.html:166
 msgid "Pagesize:"
-msgstr "Taille de la page:"
+msgstr "Taille de la page :"
 
 #: ../templates/classic/html/issue.search.html:172
 msgid "Start With:"
-msgstr "Commence par"
+msgstr "Commence par :"
 
 #: ../templates/classic/html/issue.search.html:178
 msgid "Sort Descending:"
-msgstr "Tri descendant:"
+msgstr "Tri descendant :"
 
 #: ../templates/classic/html/issue.search.html:185
 msgid "Group Descending:"
-msgstr "Groupage descendant:"
+msgstr "Groupe descendant :"
 
 #: ../templates/classic/html/issue.search.html:192
 msgid "Query name**:"
-msgstr "Nom de requête**:"
+msgstr "Nom de requête** :"
 
 #: ../templates/classic/html/issue.search.html:204
 #: ../templates/classic/html/page.html:31
@@ -2883,29 +2905,25 @@
 
 #: ../templates/classic/html/issue.search.html:209
 msgid "*: The \"all text\" field will look in message bodies and issue titles"
-msgstr ""
-"*: Le champ \"tout le texte\" recherchera dans tous les corps de message et "
-"les titres de demande"
+msgstr "* : le champ « tout le texte » recherchera dans tous les corps de message et les titres de l'anomalie"
 
 #: ../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 ""
-"**: Si vous donnez un nom, la requ&ecirc;te sera sauvegard&eacute;e et "
-"disponible comme lien dans la barre lat&eacute;rale"
+msgstr "** : si vous attribuez un nom, la requ&ecirc;te sera enregistr&eacute;e et disponible comme lien dans la barre lat&eacute;rale"
 
 #: ../templates/classic/html/keyword.item.html:3
 msgid "Keyword editing - ${tracker}"
-msgstr "&Eacute;dition de mots-cl&eacute; - ${tracker}"
+msgstr "Modification de mots cl&eacute; - ${tracker}"
 
 #: ../templates/classic/html/keyword.item.html:5
 msgid "Keyword editing"
-msgstr "&Eacute;dition de mots-cl&eacute;"
+msgstr "Modification de mot-cl&eacute;s"
 
 #: ../templates/classic/html/keyword.item.html:11
 msgid "Existing Keywords"
-msgstr "Mots-cl&eacute; existants"
+msgstr "Mot-cl&eacute;s existants"
 
 #: ../templates/classic/html/keyword.item.html:20
 msgid ""
@@ -2917,9 +2935,7 @@
 
 #: ../templates/classic/html/keyword.item.html:27
 msgid "To create a new keyword, enter it below and click \"Submit New Entry\"."
-msgstr ""
-"Pour cr&eacute;er un nouveau mot-cl&eacute;, entrez-le ci-dessous et cliquer "
-"\"Soumettre une nouvelle entr&eacute;e\"."
+msgstr "Pour cr&eacute;er un nouveau mot-cl&eacute;, saisissez-le ci-dessous et cliquer sur « Soumettre une nouvelle entr&eacute;e »."
 
 #: ../templates/classic/html/keyword.item.html:37
 msgid "Keyword"
@@ -2947,7 +2963,7 @@
 
 #: ../templates/classic/html/msg.item.html:15
 msgid "New Message Editing"
-msgstr "&Eacute;dition d'un nouveau message"
+msgstr "Modification d'un nouveau message"
 
 #: ../templates/classic/html/msg.item.html:18
 msgid "Message${id}"
@@ -2955,7 +2971,7 @@
 
 #: ../templates/classic/html/msg.item.html:21
 msgid "Message${id} Editing"
-msgstr "&Eacute;dition de Message${id}"
+msgstr "Modification du message ${id}"
 
 #: ../templates/classic/html/msg.item.html:33
 msgid "Author"
@@ -2971,12 +2987,11 @@
 
 #: ../templates/classic/html/page.html:41
 msgid "<b>Your Queries</b> (<a href=\"query?@template=edit\">edit</a>)"
-msgstr ""
-"<b>Vos requ&ecirc;tes</b> (<a href=\"query?@template=edit\">modifier</a>)"
+msgstr "<b>Vos requ&ecirc;tes</b> (<a href=\"query?@template=edit\">modifier</a>)"
 
 #: ../templates/classic/html/page.html:52
 msgid "Issues"
-msgstr "Demandes"
+msgstr "Anomalies"
 
 #: ../templates/classic/html/page.html:54
 #: ../templates/classic/html/page.html:74
@@ -2985,15 +3000,15 @@
 
 #: ../templates/classic/html/page.html:56
 msgid "Show Unassigned"
-msgstr "Montrer les non-assign&eacute;s"
+msgstr "Afficher les non-affectées"
 
 #: ../templates/classic/html/page.html:58
 msgid "Show All"
-msgstr "Montrer tout"
+msgstr "Tout afficher"
 
 #: ../templates/classic/html/page.html:61
 msgid "Show issue:"
-msgstr "Montrer la demande:"
+msgstr "Voir l'anomalie :"
 
 #: ../templates/classic/html/page.html:72
 msgid "Keywords"
@@ -3032,18 +3047,18 @@
 #: ../templates/classic/html/page.html:104
 #: ../templates/minimal/html/page.html:45
 msgid "Remember me?"
-msgstr "Se souvenir de moi ?"
+msgstr "Se souvenir"
 
 #: ../templates/classic/html/page.html:108
 #: ../templates/classic/html/user.register.html:63
 #: ../templates/minimal/html/page.html:50
 #: ../templates/minimal/html/user.register.html:58
 msgid "Register"
-msgstr "S'enregistrer"
+msgstr "S'inscrire"
 
 #: ../templates/classic/html/page.html:111
 msgid "Lost&nbsp;your&nbsp;login?"
-msgstr "Perdu&nbsp;votre&nbsp;login&nbsp;?"
+msgstr "Retrouver&nbsp;votre&nbsp;identifiant"
 
 #: ../templates/classic/html/page.html:116
 msgid "Hello, ${user}"
@@ -3051,7 +3066,7 @@
 
 #: ../templates/classic/html/page.html:118
 msgid "Your Issues"
-msgstr "Vos demandes"
+msgstr "Vos anomalies"
 
 #: ../templates/classic/html/page.html:119
 #: ../templates/minimal/html/page.html:57
@@ -3082,7 +3097,7 @@
 
 #: ../templates/classic/html/page.html:183
 msgid "------------"
-msgstr ""
+msgstr "------------"
 
 #: ../templates/classic/html/page.html:210
 msgid "no value"
@@ -3090,15 +3105,15 @@
 
 #: ../templates/classic/html/query.edit.html:4
 msgid "\"Your Queries\" Editing - ${tracker}"
-msgstr "&Eacute;dition de \"Vos requ&ecirc;tes\" - ${tracker}"
+msgstr "Modification de « Vos requ&ecirc;tes » - ${tracker}"
 
 #: ../templates/classic/html/query.edit.html:6
 msgid "\"Your Queries\" Editing"
-msgstr "&Eacute;dition de \"Vos requ&ecirc;tes\""
+msgstr "Modification de « Vos requ&ecirc;tes »"
 
 #: ../templates/classic/html/query.edit.html:11
 msgid "You are not allowed to edit queries."
-msgstr "Vous n'avez pas l'autorisation d'&eacute;diter des requ&ecirc;tes."
+msgstr "Vous n'avez pas l'autorisation de modifier des requ&ecirc;tes."
 
 #: ../templates/classic/html/query.edit.html:28
 msgid "Query"
@@ -3106,11 +3121,11 @@
 
 #: ../templates/classic/html/query.edit.html:29
 msgid "Include in \"Your Queries\""
-msgstr "Inclus dans \"Vos requ&ecirc;tes\""
+msgstr "Inclus dans « Vos requ&ecirc;tes »"
 
 #: ../templates/classic/html/query.edit.html:31
 msgid "Private to you?"
-msgstr "Priv&eacute; ?"
+msgstr "Priv&eacute; ?"
 
 #: ../templates/classic/html/query.edit.html:44
 msgid "leave out"
@@ -3126,12 +3141,12 @@
 
 #: ../templates/classic/html/query.edit.html:54
 msgid "[query is retired]"
-msgstr "[requ&ecirc;te abandonn&eacute;e]"
+msgstr "[requ&ecirc;te retir&eacute;e]"
 
 #: ../templates/classic/html/query.edit.html:67
 #: ../templates/classic/html/query.edit.html:92
 msgid "edit"
-msgstr "&eacute;diter"
+msgstr "modifier"
 
 #: ../templates/classic/html/query.edit.html:71
 msgid "yes"
@@ -3143,7 +3158,7 @@
 
 #: ../templates/classic/html/query.edit.html:79
 msgid "Delete"
-msgstr "Effacer"
+msgstr "Supprimer"
 
 #: ../templates/classic/html/query.edit.html:94
 msgid "[not yours to edit]"
@@ -3151,7 +3166,7 @@
 
 #: ../templates/classic/html/query.edit.html:102
 msgid "Save Selection"
-msgstr "Sauvegarder la sélection"
+msgstr "Enregistrer la sélection"
 
 #: ../templates/classic/html/user.forgotten.html:3
 msgid "Password reset request - ${tracker}"
@@ -3165,14 +3180,11 @@
 msgid ""
 "You have two options if you have forgotten your password. If you know the "
 "email address you registered with, enter it below."
-msgstr ""
-"Vous avez deux solutions si vous avez oubli&eacute; votre mot de passe. Si "
-"vous connaissez l'adresse de messagerie avec laquelle vous vous &ecirc;tes "
-"enregistr&eacute;, introduisez-l&agrave; ci-dessous."
+msgstr "Vous avez deux solutions si vous avez oubli&eacute; votre mot de passe. Si vous connaissez l'adresse électronique avec laquelle vous vous &ecirc;tes enregistr&eacute;, saisissez l&agrave; ci-dessous."
 
 #: ../templates/classic/html/user.forgotten.html:16
 msgid "Email Address:"
-msgstr "Adresse de messagerie:"
+msgstr "Adresse électronique :"
 
 #: ../templates/classic/html/user.forgotten.html:24
 #: ../templates/classic/html/user.forgotten.html:34
@@ -3181,21 +3193,20 @@
 
 #: ../templates/classic/html/user.forgotten.html:30
 msgid "Or, if you know your username, then enter it below."
-msgstr ""
-"ou, si vous connaissez votre nom d'utilisateur, introduisez-le ci-dessous."
+msgstr "ou, si vous connaissez votre nom d'utilisateur, saisissez-le ci-dessous."
 
 #: ../templates/classic/html/user.forgotten.html:33
 msgid "Username:"
-msgstr "Nom d'utilisateur:"
+msgstr "Nom d'utilisateur :"
 
 #: ../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 ""
-"un courriel de confirmation va vous &ecirc;tre envoy&eacute; - veuillez "
-"suivre les instructions qui y sont donn&eacute;es pour terminer le processus "
-"de r&eacute;initialisation de votre mot de passe."
+"un courriel de confirmation va vous &ecirc;tre envoy&eacute; - suivez\n"
+"les instructions qu'il contient pour terminer le processus de\n"
+"r&eacute;initialisation de votre mot de passe."
 
 #: ../templates/classic/html/user.index.html:3
 #: ../templates/minimal/html/user.index.html:3
@@ -3225,7 +3236,7 @@
 #: ../templates/classic/html/user.index.html:17
 #: ../templates/minimal/html/user.index.html:15
 msgid "Email address"
-msgstr "Adresse de messagerie"
+msgstr "Adresse électronique"
 
 #: ../templates/classic/html/user.index.html:18
 msgid "Phone number"
@@ -3233,16 +3244,16 @@
 
 #: ../templates/classic/html/user.index.html:19
 msgid "Retire"
-msgstr "Abandonner"
+msgstr "Retirer"
 
 #: ../templates/classic/html/user.index.html:32
 msgid "retire"
-msgstr "abandonner"
+msgstr "retirer"
 
 #: ../templates/classic/html/user.item.html:7
 #: ../templates/minimal/html/user.item.html:7
 msgid "User ${id}: ${title} - ${tracker}"
-msgstr "Utilisateur ${id}: ${title} - ${tracker}"
+msgstr "Utilisateur ${id} : ${title} - ${tracker}"
 
 #: ../templates/classic/html/user.item.html:10
 #: ../templates/minimal/html/user.item.html:10
@@ -3257,17 +3268,17 @@
 #: ../templates/classic/html/user.item.html:16
 #: ../templates/minimal/html/user.item.html:16
 msgid "New User Editing"
-msgstr "&Eacute;dition d'un nouvel utilisateur"
+msgstr "Création d'un nouvel utilisateur"
 
 #: ../templates/classic/html/user.item.html:19
 #: ../templates/minimal/html/user.item.html:19
 msgid "User${id}"
-msgstr "User${id}"
+msgstr "Utilisateur ${id}"
 
 #: ../templates/classic/html/user.item.html:22
 #: ../templates/minimal/html/user.item.html:22
 msgid "User${id} Editing"
-msgstr "&Eacute;dition de User${id}"
+msgstr "Modification de l'utilisateur ${id}"
 
 #: ../templates/classic/html/user.item.html:43
 #: ../templates/classic/html/user.register.html:21
@@ -3300,9 +3311,7 @@
 #: ../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 ""
-"(pour donner à l'utilisateur plus d'un r&ocirc;le, introduisez une liste,"
-"s&eacute;par&eacute;e,par,des,virgules)"
+msgstr "(pour donner à l'utilisateur plus d'un r&ocirc;le, saisissez une liste,s&eacute;par&eacute;e,par,des,virgules)"
 
 #: ../templates/classic/html/user.item.html:66
 #: ../templates/classic/html/user.register.html:41
@@ -3324,21 +3333,21 @@
 #: ../templates/minimal/html/user.item.html:63
 #: ../templates/minimal/html/user.register.html:46
 msgid "E-mail address"
-msgstr "Adresse de messagerie"
+msgstr "Adresse électronique"
 
 #: ../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 "Adresses de messagerie alternatives<br>Une adresse par ligne"
+msgstr "Adresses électronique alternatives<br>Une adresse par ligne"
 
 #: ../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 "Inscription aupr&egrave;s de ${tracker}"
+msgstr "Inscription à ${tracker}"
 
 #: ../templates/classic/html/user.rego_progress.html:4
 #: ../templates/minimal/html/user.rego_progress.html:4
@@ -3355,23 +3364,19 @@
 msgid ""
 "You will shortly receive an email to confirm your registration. To complete "
 "the registration process, visit the link indicated in the email."
-msgstr ""
-"Vous recevrez sous peu un courriel confirmant votre inscription. Pour "
-"cl&ocirc;turer le processus d'inscription, veuillez suivre le lien "
-"indiqu&eacute; dans le courriel."
+msgstr "Vous recevrez sous peu un courriel confirmant votre inscription. Pour cl&ocirc;turer le processus d'inscription, suivez le lien indiqu&eacute; dans le courriel."
 
 #: ../templates/minimal/html/home.html:2
 msgid "Tracker home - ${tracker}"
-msgstr "Base du pisteur - ${tracker}"
+msgstr "Accueil de Tracker - ${tracker}"
 
 #: ../templates/minimal/html/home.html:4
 msgid "Tracker home"
-msgstr "Base du pisteur"
+msgstr "Accueil de Tracker"
 
 #: ../templates/minimal/html/home.html:16
 msgid "Please select from one of the menu options on the left."
-msgstr ""
-"Veuillez s&eacute;lectionner l'une des options de menu &agrave; la gauche."
+msgstr "S&eacute;lectionnez l'une des options du menu de gauche."
 
 #: ../templates/minimal/html/home.html:19
 msgid "Please log in or register."
@@ -3379,4 +3384,6 @@
 
 #: ../templates/minimal/html/page.html:55
 msgid "Hello,<br>${user}"
-msgstr "Bienvenue,<br>${user}"
+msgstr "Bienvenue, <br/>${user}"
+
+ 	  	 

Added: tracker/roundup-src/locale/it.po
==============================================================================
--- (empty file)
+++ tracker/roundup-src/locale/it.po	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,3043 @@
+# Italian message file for Roundup Issue Tracker
+# Marco Ghidinelli <marco.ghidinelli at ing.unibs.it>, 2007
+#
+# $Id: it.po,v 1.2 2007-08-24 05:31:13 a1s Exp $
+#
+# roundup.pot revision 1.22
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: roundup cvs\n"
+"Report-Msgid-Bugs-To: roundup-devel at lists.sourceforge.net\n"
+"POT-Creation-Date: 2006-12-18 13:36+0200\n"
+"PO-Revision-Date: 2007-07-17 11:05+0200\n"
+"Last-Translator: Marco Ghidinelli <marco.ghidinelli at ing.unibs.it>\n"
+"Language-Team: italian <it at li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0"
+" : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+
+# ../roundup/admin.py:1052 ../roundup/admin.py:85:981 :1030:1052
+#: ../roundup/admin.py:85 ../roundup/admin.py:981 ../roundup/admin.py:1030
+#: ../roundup/admin.py:1052
+#, python-format
+msgid "no such class \"%(classname)s\""
+msgstr "classe \"%(classname)s\" mancante"
+
+# ../roundup/admin.py:95 ../roundup/admin.py:99 ../roundup/admin.py:95:99
+#: ../roundup/admin.py:95 ../roundup/admin.py:99
+#, python-format
+msgid "argument \"%(arg)s\" not propname=value"
+msgstr "argomento \"%(arg)s\" non nel formato nome=valore"
+
+#: ../roundup/admin.py:112
+#, python-format
+msgid ""
+"Problem: %(message)s\n"
+"\n"
+msgstr ""
+"Problema: %(message)s\n"
+"\n"
+
+#: ../roundup/admin.py:113
+#, 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"
+" -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"
+"                      Same as '-S \",\"'.\n"
+" -S <string>       -- when outputting lists of data, string-separate them\n"
+" -s                -- when outputting lists of data, space-separate them.\n"
+"                      Same as '-S \" \"'.\n"
+" -V                -- be verbose when importing\n"
+" -v                -- report Roundup and Python versions (and quit)\n"
+"\n"
+" Only one of -s, -c or -S can be specified.\n"
+"\n"
+"Help:\n"
+" roundup-admin -h\n"
+" roundup-admin help                       -- this help\n"
+" roundup-admin help <command>             -- command-specific help\n"
+" roundup-admin help all                   -- all available help\n"
+msgstr ""
+
+#: ../roundup/admin.py:140
+msgid "Commands:"
+msgstr "Comandi:"
+
+#: ../roundup/admin.py:147
+msgid ""
+"Commands may be abbreviated as long as the abbreviation\n"
+"matches only one command, e.g. l == li == lis == list."
+msgstr ""
+"I comandi possono essere abbreviati finchè l'abbreviazione rimane univoca\n"
+"es: l == li == lis == list."
+
+#: ../roundup/admin.py:177
+msgid ""
+"\n"
+"All commands (except help) require a tracker specifier. This is just\n"
+"the path to the roundup tracker you're working with. A roundup tracker\n"
+"is where roundup keeps the database and configuration file that defines\n"
+"an issue tracker. It may be thought of as the issue tracker's \"home\n"
+"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"
+"\n"
+"Property values are represented as strings in command arguments and in the\n"
+"printed results:\n"
+" . Strings are, well, strings.\n"
+" . Date values are printed in the full date format in the local time zone,\n"
+"   and accepted in the full format or any of the partial formats explained\n"
+"   below.\n"
+" . Link values are printed as node designators. When given as an argument,\n"
+"   node designators and key strings are both accepted.\n"
+" . Multilink values are printed as lists of node designators joined\n"
+"   by commas.  When given as an argument, node designators and key\n"
+"   strings are both accepted; an empty string, a single node, or a list\n"
+"   of nodes joined by commas is accepted.\n"
+"\n"
+"When property values must contain spaces, just surround the value with\n"
+"quotes, either ' or \". A single space may also be backslash-quoted. If a\n"
+"value must contain a quote character, it must be backslash-quoted or inside\n"
+"quotes. Examples:\n"
+"           hello world      (2 tokens: hello, world)\n"
+"           \"hello world\"    (1 token: hello world)\n"
+"           \"Roch'e\" Compaan (2 tokens: Roch'e Compaan)\n"
+"           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"
+"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"
+"nodes.\n"
+"\n"
+"When multiple results are returned by the roundup get or roundup find\n"
+"commands, they are printed one per line (default) or joined by commas (with\n"
+"the -c) option.\n"
+"\n"
+"Where the command changes data, a login name/password is required. The\n"
+"login may be specified as either \"name\" or \"name:password\".\n"
+" . ROUNDUP_LOGIN environment variable\n"
+" . the -u command-line option\n"
+"If either the name or password is not supplied, they are obtained from the\n"
+"command-line.\n"
+"\n"
+"Date format examples:\n"
+"  \"2000-04-17.03:45\" means <Date 2000-04-17.08:45:00>\n"
+"  \"2000-04-17\" means <Date 2000-04-17.00:00:00>\n"
+"  \"01-25\" means <Date yyyy-01-25.00:00:00>\n"
+"  \"08-13.22:13\" means <Date yyyy-08-14.03:13:00>\n"
+"  \"11-07.09:32:43\" means <Date yyyy-11-07.14:32:43>\n"
+"  \"14:25\" means <Date yyyy-mm-dd.19:25:00>\n"
+"  \"8:47:11\" means <Date yyyy-mm-dd.13:47:11>\n"
+"  \".\" means \"right now\"\n"
+"\n"
+"Command help:\n"
+msgstr ""
+
+#: ../roundup/admin.py:240
+#, python-format
+msgid "%s:"
+msgstr "%s:"
+
+#: ../roundup/admin.py:245
+msgid ""
+"Usage: help topic\n"
+"        Give help about topic.\n"
+"\n"
+"        commands  -- list commands\n"
+"        <command> -- help specific to a command\n"
+"        initopts  -- init command options\n"
+"        all       -- all available help\n"
+"        "
+msgstr ""
+
+#: ../roundup/admin.py:268
+#, python-format
+msgid "Sorry, no help for \"%(topic)s\""
+msgstr "Nessun aiuto per \"%(topic)s\""
+
+# ../roundup/admin.py:340 ../roundup/admin.py:396 ../roundup/admin.py:340:396
+#: ../roundup/admin.py:340 ../roundup/admin.py:396
+msgid "Templates:"
+msgstr "Modelli predefiniti:"
+
+# ../roundup/admin.py:343 ../roundup/admin.py:407 ../roundup/admin.py:343:407
+#: ../roundup/admin.py:343 ../roundup/admin.py:407
+msgid "Back ends:"
+msgstr "Back ends:"
+
+#: ../roundup/admin.py:346
+msgid ""
+"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 and backend may be specified on the command-line\n"
+"        as arguments, in that order.\n"
+"\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"
+"        whole argument in quotes if you need spaces in option value).\n"
+"\n"
+"        The initialise command must be called after this command in order\n"
+"        to initialise the tracker's database. You may edit the tracker's\n"
+"        initial database contents before running that command by editing\n"
+"        the tracker's dbinit.py module init() function.\n"
+"\n"
+"        See also initopts help.\n"
+"        "
+msgstr ""
+
+# ../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: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
+msgid "Not enough arguments supplied"
+msgstr "Non sono stati forniti abbastanza argomenti"
+
+#: ../roundup/admin.py:375
+#, python-format
+msgid "Instance home parent directory \"%(parent)s\" does not exist"
+msgstr "la directory radice dell'istanza \"%(parent)s\" non esiste"
+
+#: ../roundup/admin.py:383
+#, 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 ""
+"ATTENZIONE: È presente un tracker nella directory \"%(tracker_home)s\"!\n"
+"Se verrà reinstallata, tutti i dati precedentemente salvati andranno persi\n"
+"Cancellare la directory specificata? Y/N: "
+
+#: ../roundup/admin.py:398
+msgid "Select template [classic]: "
+msgstr "Seleziona il modello predefinito [classic]: "
+
+#: ../roundup/admin.py:409
+msgid "Select backend [anydbm]: "
+msgstr "Seleziona il backend [anydbm]: "
+
+#: ../roundup/admin.py:419
+#, python-format
+msgid "Error in configuration settings: \"%s\""
+msgstr "Erorre nei settaggi di configurazione: \"%s\""
+
+#: ../roundup/admin.py:428
+#, python-format
+msgid ""
+"\n"
+"---------------------------------------------------------------------------\n"
+" You should now edit the tracker configuration file:\n"
+"   %(config_file)s"
+msgstr ""
+
+#: ../roundup/admin.py:438
+msgid " ... at a minimum, you must set following options:"
+msgstr " ... devono essere configurate almeno le seguenti opzioni:"
+
+#: ../roundup/admin.py:443
+#, python-format
+msgid ""
+"\n"
+" If you wish to modify the database schema,\n"
+" you should also edit the schema file:\n"
+"   %(database_config_file)s\n"
+" You may also change the database initialisation file:\n"
+"   %(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"
+" the above steps.\n"
+"---------------------------------------------------------------------------\n"
+msgstr ""
+
+#: ../roundup/admin.py:461
+msgid ""
+"Usage: genconfig <filename>\n"
+"        Generate a new tracker config file (ini style) with default values\n"
+"        in <filename>.\n"
+"        "
+msgstr ""
+
+#. password
+#: ../roundup/admin.py:471
+msgid ""
+"Usage: initialise [adminpw]\n"
+"        Initialise a new Roundup tracker.\n"
+"\n"
+"        The administrator details will be set at this step.\n"
+"\n"
+"        Execute the tracker's initialisation function dbinit.init()\n"
+"        "
+msgstr ""
+
+#: ../roundup/admin.py:485
+msgid "Admin Password: "
+msgstr "Password dell'amministratore"
+
+#: ../roundup/admin.py:486
+msgid "       Confirm: "
+msgstr "       Conferma: "
+
+#: ../roundup/admin.py:490
+msgid "Instance home does not exist"
+msgstr "La home dell'istanza non esiste"
+
+#: ../roundup/admin.py:494
+msgid "Instance has not been installed"
+msgstr "L'istanza non è stata installata"
+
+#: ../roundup/admin.py:499
+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
+msgid ""
+"Usage: get property designator[,designator]*\n"
+"        Get the given property of one or more designator(s).\n"
+"\n"
+"        Retrieves the property value of the nodes specified\n"
+"        by the designators.\n"
+"        "
+msgstr ""
+
+# ../roundup/admin.py:560 ../roundup/admin.py:575 ../roundup/admin.py:560:575
+#: ../roundup/admin.py:560 ../roundup/admin.py:575
+#, python-format
+msgid "property %s is not of type Multilink or Link so -d flag does not apply."
+msgstr ""
+
+# ../roundup/admin.py:1054 ../roundup/admin.py:583:983 :1032:1054
+#: ../roundup/admin.py:583 ../roundup/admin.py:983 ../roundup/admin.py:1032
+#: ../roundup/admin.py:1054
+#, python-format
+msgid "no such %(classname)s node \"%(nodeid)s\""
+msgstr ""
+
+#: ../roundup/admin.py:585
+#, python-format
+msgid "no such %(classname)s property \"%(propname)s\""
+msgstr ""
+
+#: ../roundup/admin.py:594
+msgid ""
+"Usage: set items property=value property=value ...\n"
+"        Set the given properties of one or more items(s).\n"
+"\n"
+"        The items are specified as a class or as a comma-separated\n"
+"        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"
+"        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
+msgid ""
+"Usage: find classname propname=value ...\n"
+"        Find the nodes of the given class with a given link property value.\n"
+"\n"
+"        Find the nodes of the given class with a given link property value.\n"
+"        The value may be either the nodeid of the linked node, or its key\n"
+"        value.\n"
+"        "
+msgstr ""
+
+# ../roundup/admin.py:920 ../roundup/admin.py:701:854 :866:920
+#: ../roundup/admin.py:701 ../roundup/admin.py:854 ../roundup/admin.py:866
+#: ../roundup/admin.py:920
+#, python-format
+msgid "%(classname)s has no property \"%(propname)s\""
+msgstr "la classe %(classname)s non ha la proprietà \"%(propname)s\""
+
+#: ../roundup/admin.py:708
+msgid ""
+"Usage: specification classname\n"
+"        Show the properties for a classname.\n"
+"\n"
+"        This lists the properties for a given class.\n"
+"        "
+msgstr ""
+
+#: ../roundup/admin.py:723
+#, python-format
+msgid "%(key)s: %(value)s (key property)"
+msgstr "%(key)s %(value)s (chiave)"
+
+#: ../roundup/admin.py:725
+#, python-format
+msgid "%(key)s: %(value)s"
+msgstr "%(key)s:·%(value)s"
+
+#: ../roundup/admin.py:728
+msgid ""
+"Usage: display designator[,designator]*\n"
+"        Show the property values for the given node(s).\n"
+"\n"
+"        This lists the properties and their associated values for the given\n"
+"        node.\n"
+"        "
+msgstr ""
+
+#: ../roundup/admin.py:752
+#, python-format
+msgid "%(key)s: %(value)r"
+msgstr "%(key)s:·%(value)r"
+
+#: ../roundup/admin.py:755
+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"
+"        command.\n"
+"        "
+msgstr ""
+
+#: ../roundup/admin.py:782
+#, python-format
+msgid "%(propname)s (Password): "
+msgstr "%(propname)s·(Password):·"
+
+#: ../roundup/admin.py:784
+#, python-format
+msgid "   %(propname)s (Again): "
+msgstr "   %(propname)s (Ripeti password): "
+
+#: ../roundup/admin.py:786
+msgid "Sorry, try again..."
+msgstr "Mi dispiace, riprova..."
+
+#: ../roundup/admin.py:790
+#, python-format
+msgid "%(propname)s (%(proptype)s): "
+msgstr "%(propname)s (%(proptype)s): "
+
+#: ../roundup/admin.py:808
+#, python-format
+msgid "you must provide the \"%(propname)s\" property."
+msgstr "deve essere fornita la proprietà \"%(propname)s\"."
+
+#: ../roundup/admin.py:819
+msgid ""
+"Usage: list classname [property]\n"
+"        List the instances of a class.\n"
+"\n"
+"        Lists all instances of the given class. If the property is not\n"
+"        specified, the  \"label\" property is used. The label property is\n"
+"        tried in order: the key, \"name\", \"title\" and then the first\n"
+"        property, alphabetically.\n"
+"\n"
+"        With -c, -S or -s print a list of item id's if no property\n"
+"        specified.  If property specified, print list of that property\n"
+"        for every class instance.\n"
+"        "
+msgstr ""
+
+#: ../roundup/admin.py:832
+msgid "Too many arguments supplied"
+msgstr ""
+
+#: ../roundup/admin.py:868
+#, python-format
+msgid "%(nodeid)4s: %(value)s"
+msgstr ""
+
+#: ../roundup/admin.py:872
+msgid ""
+"Usage: table classname [property[,property]*]\n"
+"        List the instances of a class in tabular form.\n"
+"\n"
+"        Lists all instances of the given class. If the properties are not\n"
+"        specified, all properties are displayed. By default, the column\n"
+"        widths are the width of the largest value. The width may be\n"
+"        explicitly defined by defining the property as \"name:width\".\n"
+"        For example::\n"
+"\n"
+"          roundup> table priority id,name:10\n"
+"          Id Name\n"
+"          1  fatal-bug\n"
+"          2  bug\n"
+"          3  usability\n"
+"          4  feature\n"
+"\n"
+"        Also to make the width of the column the width of the label,\n"
+"        leave a trailing : without a width on the property. For example::\n"
+"\n"
+"          roundup> table priority id,name:\n"
+"          Id Name\n"
+"          1  fata\n"
+"          2  bug\n"
+"          3  usab\n"
+"          4  feat\n"
+"\n"
+"        will result in a the 4 character wide \"Name\" column.\n"
+"        "
+msgstr ""
+
+#: ../roundup/admin.py:916
+#, python-format
+msgid "\"%(spec)s\" not name:width"
+msgstr ""
+
+#: ../roundup/admin.py:966
+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"
+"        "
+msgstr ""
+
+#: ../roundup/admin.py:987
+msgid ""
+"Usage: commit\n"
+"        Commit changes made to the database during an interactive session.\n"
+"\n"
+"        The changes made during an interactive session are not\n"
+"        automatically written to the database - they must be committed\n"
+"        using this command.\n"
+"\n"
+"        One-off commands on the command-line are automatically committed if\n"
+"        they are successful.\n"
+"        "
+msgstr ""
+
+#: ../roundup/admin.py:1001
+msgid ""
+"Usage: rollback\n"
+"        Undo all changes that are pending commit to the database.\n"
+"\n"
+"        The changes made during an interactive session are not\n"
+"        automatically written to the database - they must be committed\n"
+"        manually. This command undoes all those changes, so a commit\n"
+"        immediately after would make no changes to the database.\n"
+"        "
+msgstr ""
+
+#: ../roundup/admin.py:1013
+msgid ""
+"Usage: retire designator[,designator]*\n"
+"        Retire the node specified by designator.\n"
+"\n"
+"        This action indicates that a particular node is not to be retrieved\n"
+"        by the list or find commands, and its key value may be re-used.\n"
+"        "
+msgstr ""
+
+#: ../roundup/admin.py:1036
+msgid ""
+"Usage: restore designator[,designator]*\n"
+"        Restore the retired node specified by designator.\n"
+"\n"
+"        The given nodes will become available for users again.\n"
+"        "
+msgstr ""
+
+#. grab the directory to export to
+#: ../roundup/admin.py:1058
+msgid ""
+"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:1136
+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 ""
+
+#: ../roundup/admin.py:1151
+msgid ""
+"Usage: import import_dir\n"
+"        Import a database from the directory containing CSV files,\n"
+"        two per class to import.\n"
+"\n"
+"        The files used in the import are:\n"
+"\n"
+"        <class>.csv\n"
+"          This must define the same properties as the class (including\n"
+"          having a \"header\" line with those property names.)\n"
+"        <class>-journals.csv\n"
+"          This defines the journals for the items being imported.\n"
+"\n"
+"        The imported nodes will have the same nodeid as defined in the\n"
+"        import file, thus replacing any existing content.\n"
+"\n"
+"        The new nodes are added to the existing database - if you want to\n"
+"        create a new database using the imported data, then create a new\n"
+"        database (or, tediously, retire all the old data.)\n"
+"        "
+msgstr ""
+
+#: ../roundup/admin.py:1225
+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"
+"        suffix \"w\" (for \"week\") means 7 days.\n"
+"\n"
+"              \"3y\" means three years\n"
+"              \"2y 1m\" means two years and one month\n"
+"              \"1m 25d\" means one month and 25 days\n"
+"              \"2w 3d\" means two weeks and three days\n"
+"\n"
+"        Date format is \"YYYY-MM-DD\" eg:\n"
+"            2001-01-01\n"
+"\n"
+"        "
+msgstr ""
+
+#: ../roundup/admin.py:1253
+msgid "Invalid format"
+msgstr ""
+
+#: ../roundup/admin.py:1263
+msgid ""
+"Usage: reindex [classname|designator]*\n"
+"        Re-generate a tracker's search indexes.\n"
+"\n"
+"        This will re-generate the search indexes for a tracker.\n"
+"        This will typically happen automatically.\n"
+"        "
+msgstr ""
+
+#: ../roundup/admin.py:1277
+#, python-format
+msgid "no such item \"%(designator)s\""
+msgstr ""
+
+#: ../roundup/admin.py:1287
+msgid ""
+"Usage: security [Role name]\n"
+"        Display the Permissions available to one or all Roles.\n"
+"        "
+msgstr ""
+
+#: ../roundup/admin.py:1295
+#, python-format
+msgid "No such Role \"%(role)s\""
+msgstr "Non è presente il ruolo \"%(role)s\""
+
+#: ../roundup/admin.py:1301
+#, python-format
+msgid "New Web users get the Roles \"%(role)s\""
+msgstr "I nuovi utenti Web otterranno i ruoli \"%(role)s\""
+
+#: ../roundup/admin.py:1303
+#, python-format
+msgid "New Web users get the Role \"%(role)s\""
+msgstr "I nuovi utenti Web otterranno il ruolo \"%(role)s)\""
+
+#: ../roundup/admin.py:1306
+#, python-format
+msgid "New Email users get the Roles \"%(role)s\""
+msgstr "I nuovi utenti Email otterranno i ruoli \"%(role)s)\""
+
+#: ../roundup/admin.py:1308
+#, python-format
+msgid "New Email users get the Role \"%(role)s\""
+msgstr "I nuovi utenti Email otterranno il ruolo \"%(role)s\""
+
+#: ../roundup/admin.py:1311
+#, python-format
+msgid "Role \"%(name)s\":"
+msgstr "Ruolo \"%(name)s\":"
+
+#: ../roundup/admin.py:1316
+#, python-format
+msgid " %(description)s (%(name)s for \"%(klass)s\": %(properties)s only)"
+msgstr ""
+
+#: ../roundup/admin.py:1319
+#, python-format
+msgid " %(description)s (%(name)s for \"%(klass)s\" only)"
+msgstr ""
+
+#: ../roundup/admin.py:1322
+#, python-format
+msgid " %(description)s (%(name)s)"
+msgstr ""
+
+#: ../roundup/admin.py:1351
+#, python-format
+msgid "Unknown command \"%(command)s\" (\"help commands\" for a list)"
+msgstr ""
+
+#: ../roundup/admin.py:1357
+#, python-format
+msgid "Multiple commands match \"%(command)s\": %(list)s"
+msgstr ""
+
+#: ../roundup/admin.py:1364
+msgid "Enter tracker home: "
+msgstr ""
+
+# ../roundup/admin.py:1371:1377 :1397
+#: ../roundup/admin.py:1371 ../roundup/admin.py:1377 ../roundup/admin.py:1397
+#, python-format
+msgid "Error: %(message)s"
+msgstr ""
+
+#: ../roundup/admin.py:1385
+#, python-format
+msgid "Error: Couldn't open tracker: %(message)s"
+msgstr ""
+
+#: ../roundup/admin.py:1410
+#, python-format
+msgid ""
+"Roundup %s ready for input.\n"
+"Type \"help\" for help."
+msgstr ""
+
+#: ../roundup/admin.py:1415
+msgid "Note: command history and editing not available"
+msgstr ""
+
+#: ../roundup/admin.py:1419
+msgid "roundup> "
+msgstr ""
+
+#: ../roundup/admin.py:1421
+msgid "exit..."
+msgstr ""
+
+#: ../roundup/admin.py:1431
+msgid "There are unsaved changes. Commit them (y/N)? "
+msgstr ""
+
+#: ../roundup/backends/back_anydbm.py:2000
+#, python-format
+msgid "WARNING: invalid date tuple %r"
+msgstr ""
+
+#: ../roundup/backends/rdbms_common.py:1442
+msgid "create"
+msgstr "crea"
+
+#: ../roundup/backends/rdbms_common.py:1608
+msgid "unlink"
+msgstr "collega"
+
+#: ../roundup/backends/rdbms_common.py:1612
+msgid "link"
+msgstr "scollega"
+
+#: ../roundup/backends/rdbms_common.py:1732
+msgid "set"
+msgstr "assegna"
+
+#: ../roundup/backends/rdbms_common.py:1756
+msgid "retired"
+msgstr "ritira"
+
+#: ../roundup/backends/rdbms_common.py:1786
+msgid "restored"
+msgstr "ripristina"
+
+#: ../roundup/cgi/actions.py:58
+#, python-format
+msgid "You do not have permission to %(action)s the %(classname)s class."
+msgstr "Non hai i permessi per %{action) la classe %(classname)."
+
+#: ../roundup/cgi/actions.py:89
+msgid "No type specified"
+msgstr "Non hai specificato alcun tipo"
+
+#: ../roundup/cgi/actions.py:91
+msgid "No ID entered"
+msgstr "Non hai fornito alcun ID"
+
+#: ../roundup/cgi/actions.py:97
+#, python-format
+msgid "\"%(input)s\" is not an ID (%(classname)s ID required)"
+msgstr "\"%(input)\" non è un ID (%(ID della %(classname) è obbligatorio"
+
+#: ../roundup/cgi/actions.py:117
+msgid "You may not retire the admin or anonymous user"
+msgstr "Non è possibile ritirare l'utente amministratore o l'utente anonimo"
+
+#: ../roundup/cgi/actions.py:124
+#, python-format
+msgid "%(classname)s %(itemid)s has been retired"
+msgstr "%(classname)s %(itemid)s è stato ritirato"
+
+# ../roundup/cgi/actions.py:174:202
+#: ../roundup/cgi/actions.py:174 ../roundup/cgi/actions.py:202
+msgid "You do not have permission to edit queries"
+msgstr "Non hai il permesso di modificare delle query"
+
+# ../roundup/cgi/actions.py:180:209
+#: ../roundup/cgi/actions.py:180 ../roundup/cgi/actions.py:209
+msgid "You do not have permission to store queries"
+msgstr "Non hai il permesso di archiviare delle query"
+
+#: ../roundup/cgi/actions.py:298
+#, python-format
+msgid "Not enough values on line %(line)s"
+msgstr "Non abbastanza valori alla riga %(line)"
+
+#: ../roundup/cgi/actions.py:345
+msgid "Items edited OK"
+msgstr "Item modificato correttamente"
+
+#: ../roundup/cgi/actions.py:405
+#, python-format
+msgid "%(class)s %(id)s %(properties)s edited ok"
+msgstr "%(class)s %(id)s %(properties)s modificata correttamente"
+
+#: ../roundup/cgi/actions.py:408
+#, python-format
+msgid "%(class)s %(id)s - nothing changed"
+msgstr "%(class)s %(id)s - nessuna modifica"
+
+#: ../roundup/cgi/actions.py:420
+#, python-format
+msgid "%(class)s %(id)s created"
+msgstr "%(class)s %(id)s creata"
+
+#: ../roundup/cgi/actions.py:452
+#, python-format
+msgid "You do not have permission to edit %(class)s"
+msgstr "Non hai i permessi per modificare i $(class)s"
+
+#: ../roundup/cgi/actions.py:464
+#, python-format
+msgid "You do not have permission to create %(class)s"
+msgstr "Non hai il permesso per creare $(class)s"
+
+#: ../roundup/cgi/actions.py:488
+msgid "You do not have permission to edit user roles"
+msgstr "Non hai i permessi per modificare l ruoli dell'utente"
+
+#: ../roundup/cgi/actions.py:538
+#, 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 ""
+"Errore di Modifica: qualcun'altro ha modificato questo %s (%s). Visualizza "
+"<a target=\"new\" href=\"%s%s\">le sue modifiche</a> in una nuova finestra."
+
+#: ../roundup/cgi/actions.py:566
+#, python-format
+msgid "Edit Error: %s"
+msgstr "Errore di modifica: %s"
+
+# ../roundup/cgi/actions.py:597:608 :779:798
+#: ../roundup/cgi/actions.py:597 ../roundup/cgi/actions.py:608
+#: ../roundup/cgi/actions.py:779 ../roundup/cgi/actions.py:798
+#, python-format
+msgid "Error: %s"
+msgstr "Errore: %s"
+
+#: ../roundup/cgi/actions.py:634
+msgid ""
+"Invalid One Time Key!\n"
+"(a Mozilla bug may cause this message to show up erroneously, please check "
+"your email)"
+msgstr ""
+"One Time Key invalido!\n"
+"(un bug di Mozilla può causare la erronea presenza di questo messaggio, (per "
+"favore controlla la tua email)"
+
+#: ../roundup/cgi/actions.py:676
+#, python-format
+msgid "Password reset and email sent to %s"
+msgstr "Password modificata e mandata in email a %s"
+
+#: ../roundup/cgi/actions.py:685
+msgid "Unknown username"
+msgstr "Nome Utente sconosciuto"
+
+#: ../roundup/cgi/actions.py:693
+msgid "Unknown email address"
+msgstr "Indirizzo di email sconosciuto"
+
+#: ../roundup/cgi/actions.py:698
+msgid "You need to specify a username or address"
+msgstr "È necessario specificare un Nome Utente o un indirizzo email"
+
+#: ../roundup/cgi/actions.py:723
+#, python-format
+msgid "Email sent to %s"
+msgstr "Email inviata a %s"
+
+#: ../roundup/cgi/actions.py:742
+msgid "You are now registered, welcome!"
+msgstr "Ora sei un utente registrato, benvenuto!"
+
+#: ../roundup/cgi/actions.py:787
+msgid "It is not permitted to supply roles at registration."
+msgstr "Non è permesso fornire ruoli in fase di registrazione."
+
+#: ../roundup/cgi/actions.py:879
+msgid "You are logged out"
+msgstr "Disconnesso"
+
+#: ../roundup/cgi/actions.py:896
+msgid "Username required"
+msgstr "È richiesto il Nome Utente"
+
+# ../roundup/cgi/actions.py:931:935
+#: ../roundup/cgi/actions.py:931 ../roundup/cgi/actions.py:935
+msgid "Invalid login"
+msgstr "Login invalida"
+
+#: ../roundup/cgi/actions.py:941
+msgid "You do not have permission to login"
+msgstr "Non hai il permesso per eseguire la login"
+
+#: ../roundup/cgi/cgitb.py:49
+#, python-format
+msgid ""
+"<h1>Templating Error</h1>\n"
+"<p><b>%(exc_type)s</b>: %(exc_value)s</p>\n"
+"<p class=\"help\">Debugging information follows</p>"
+msgstr ""
+"<h1>Errore del sistema di Templating</h1>\n"
+"<p><b>%(exc_type)s</b>: %(exc_value)s</p>\n"
+"<p class=\"help\">Debugging information follows</p>"
+
+#: ../roundup/cgi/cgitb.py:64
+#, python-format
+msgid "<li>\"%(name)s\" (%(info)s)</li>"
+msgstr "<li>\"%(name)s\" (%(info)s)</li>"
+
+#: ../roundup/cgi/cgitb.py:67
+#, python-format
+msgid "<li>Looking for \"%(name)s\", current path:<ol>%(path)s</ol></li>"
+msgstr ""
+"<li>Sto cercando \"%(name)s\", percorso corrente:<ol>%(path)s</ol></li>"
+
+#: ../roundup/cgi/cgitb.py:71
+#, python-format
+msgid "<li>In %s</li>"
+msgstr "<li>In %s</li>"
+
+#: ../roundup/cgi/cgitb.py:76
+#, python-format
+msgid "A problem occurred in your template \"%s\"."
+msgstr "È occorso un problema nel tuo template"
+
+#: ../roundup/cgi/cgitb.py:84
+#, python-format
+msgid ""
+"\n"
+"<li>While evaluating the %(info)r expression on line %(line)d\n"
+"<table class=\"otherinfo\" style=\"font-size: 90%%\">\n"
+" <tr><th colspan=\"2\" class=\"header\">Current variables:</th></tr>\n"
+" %(globals)s\n"
+" %(locals)s\n"
+"</table></li>\n"
+msgstr ""
+"\n"
+"<li>Mentre veniva valutato la espressione %(info)r alla riga %(line)d\n"
+"<table class=\"otherinfo\" style=\"font-size: 90%%\">\n"
+" <tr><th colspan=\"2\" class=\"header\">Variabile Corrente:</th></tr>\n"
+" %(globals)s\n"
+" %(locals)s\n"
+"</table></li>\n"
+
+#: ../roundup/cgi/cgitb.py:103
+msgid "Full traceback:"
+msgstr "Traceback completo:"
+
+#: ../roundup/cgi/cgitb.py:116
+#, python-format
+msgid "<font size=+1><strong>%(exc_type)s</strong>: %(exc_value)s</font>"
+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 ""
+"<p>Un problema è occorso mentre veniva eseguito uno script Python. Questa è "
+"la sequenza di chiamate di sistema che hanno condotto a questo errore, con "
+"la più recente (la più interna) prima. Le eccezioni sono:"
+
+#: ../roundup/cgi/cgitb.py:129
+msgid "&lt;file is None - probably inside <tt>eval</tt> or <tt>exec</tt>&gt;"
+msgstr ""
+
+#: ../roundup/cgi/cgitb.py:138
+#, python-format
+msgid "in <strong>%s</strong>"
+msgstr "in <strong>%s</strong>"
+
+# ../roundup/cgi/cgitb.py:172:178
+#: ../roundup/cgi/cgitb.py:172 ../roundup/cgi/cgitb.py:178
+msgid "<em>undefined</em>"
+msgstr "<em>indefinito</em>"
+
+#: ../roundup/cgi/client.py:49
+msgid ""
+"<html><head><title>An error has occurred</title></head>\n"
+"<body><h1>An error has occurred</h1>\n"
+"<p>A problem was encountered processing your request.\n"
+"The tracker maintainers have been notified of the problem.</p>\n"
+"</body></html>"
+msgstr ""
+"<html><head><title>È accaduto un errore</title></head>\n"
+"<body><h1>È accaduto un errore</h1>\n"
+"<p>È accaduto un errore mentre veniva processata la richiesta.\n"
+"La notifica del problema è stata notificata al manutentore del tracker.</p>\n"
+"</body></html>"
+
+#: ../roundup/cgi/client.py:326
+msgid "Form Error: "
+msgstr "Errore nella Form: "
+
+#: ../roundup/cgi/client.py:381
+#, python-format
+msgid "Unrecognized charset: %r"
+msgstr "Codice di carattere sconosciuto: %r"
+
+#: ../roundup/cgi/client.py:509
+msgid "Anonymous users are not allowed to use the web interface"
+msgstr ""
+"Gli utenti anonimi non hanno il permesso di utilizzare l'interfaccia web"
+
+#: ../roundup/cgi/client.py:664
+msgid "You are not allowed to view this file."
+msgstr "Non si dispone dei permessi per visualizzare questo file."
+
+#: ../roundup/cgi/client.py:758
+#, python-format
+msgid "%(starttag)sTime elapsed: %(seconds)fs%(endtag)s\n"
+msgstr "%(starttag)sTempo trascorso: %(seconds)fs%(endtad)s\n"
+
+#: ../roundup/cgi/client.py:762
+#, 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 ""
+
+#: ../roundup/cgi/form_parser.py:283
+#, python-format
+msgid "link \"%(key)s\" value \"%(entry)s\" not a designator"
+msgstr "il collegamento \"%(key)s\" valore \"%(entry)s\" non è il designatore"
+
+#: ../roundup/cgi/form_parser.py:301
+#, python-format
+msgid "%(class)s %(property)s is not a link or multilink property"
+msgstr ""
+"%(class)s %(property)s non è una proprietà di tipo collegamento o "
+"multicollegamento"
+
+#: ../roundup/cgi/form_parser.py:313
+#, python-format
+msgid ""
+"The form action claims to require property \"%(property)s\" which doesn't "
+"exist"
+msgstr ""
+"L'azione assiociata alla form richiede la proprietà \"%(property)s\" che non "
+"esiste"
+
+#: ../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"
+msgstr ""
+"È stata inserito una azione %(action)s per la proprietà \"%(property)s\" che "
+"non esiste"
+
+# ../roundup/cgi/form_parser.py:354:380
+#: ../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 "È stata inserito più di un valore per la proprietà %s"
+
+# ../roundup/cgi/form_parser.py:377:383
+#: ../roundup/cgi/form_parser.py:377 ../roundup/cgi/form_parser.py:383
+msgid "Password and confirmation text do not match"
+msgstr "La password e il testo di conferma non coincidono"
+
+#: ../roundup/cgi/form_parser.py:418
+#, python-format
+msgid "property \"%(propname)s\": \"%(value)s\" not currently in list"
+msgstr ""
+"la proprietà \"%(propname)s\": \"%(value)s\" non è al momento nella lista"
+
+#: ../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] "La proprietà %(class)s %(property)s non è stata fornita"
+msgstr[1] "Le proprietà %(class)s %(property)s non sono state fornite"
+
+#: ../roundup/cgi/form_parser.py:574
+msgid "File is empty"
+msgstr "Il file è vuoto"
+
+#: ../roundup/cgi/templating.py:73
+#, python-format
+msgid "You are not allowed to %(action)s items of class %(class)s"
+msgstr "Non si dispone dei permessi per %(action)s item della classe %(class)s"
+
+#: ../roundup/cgi/templating.py:645
+msgid "(list)"
+msgstr "(elenco)"
+
+#: ../roundup/cgi/templating.py:714
+msgid "Submit New Entry"
+msgstr "Crea Nuovo"
+
+# ../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: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
+msgid "[hidden]"
+msgstr "[nascosto]"
+
+#: ../roundup/cgi/templating.py:729
+msgid "New node - no history"
+msgstr "Nuovo nodo - nessuno storico"
+
+#: ../roundup/cgi/templating.py:844
+msgid "Submit Changes"
+msgstr "Inserisci Modifiche"
+
+#: ../roundup/cgi/templating.py:926
+msgid "<em>The indicated property no longer exists</em>"
+msgstr "<em>La caratteristica indicata non esiste</em>"
+
+#: ../roundup/cgi/templating.py:927
+#, python-format
+msgid "<em>%s: %s</em>\n"
+msgstr "<em>%s: %s</em>\n"
+
+#: ../roundup/cgi/templating.py:940
+#, python-format
+msgid "The linked class %(classname)s no longer exists"
+msgstr "La classe collegata %(classname)s non esiste più"
+
+# ../roundup/cgi/templating.py:973:997
+#: ../roundup/cgi/templating.py:973 ../roundup/cgi/templating.py:997
+msgid "<strike>The linked node no longer exists</strike>"
+msgstr "<strike>Il Nodo collegato non esiste più</strike>"
+
+#: ../roundup/cgi/templating.py:1050
+#, python-format
+msgid "%s: (no value)"
+msgstr "%s: (nessun valore)"
+
+#: ../roundup/cgi/templating.py:1062
+msgid ""
+"<strong><em>This event is not handled by the history display!</em></strong>"
+msgstr ""
+"<strong><em>Questo evento non è gestito dal visualizzatore dello storico!</"
+"em></strong>"
+
+#: ../roundup/cgi/templating.py:1074
+msgid "<tr><td colspan=4><strong>Note:</strong></td></tr>"
+msgstr "<tr><td colspan=4><strong>Note:</strong></td></tr>"
+
+#: ../roundup/cgi/templating.py:1083
+msgid "History"
+msgstr "Storico"
+
+#: ../roundup/cgi/templating.py:1085
+msgid "<th>Date</th>"
+msgstr "<th>Data</th>"
+
+#: ../roundup/cgi/templating.py:1086
+msgid "<th>User</th>"
+msgstr "<th>Utente</th>"
+
+#: ../roundup/cgi/templating.py:1087
+msgid "<th>Action</th>"
+msgstr "<th>Azione</th>"
+
+#: ../roundup/cgi/templating.py:1088
+msgid "<th>Args</th>"
+msgstr "<th>Argomenti</th>"
+
+#: ../roundup/cgi/templating.py:1130
+#, python-format
+msgid "Copy of %(class)s %(id)s"
+msgstr "Copia di %(class)s %(id)s"
+
+#: ../roundup/cgi/templating.py:1391
+msgid "*encrypted*"
+msgstr "*crittato*"
+
+# ../roundup/cgi/templating.py:1491 ../roundup/cgi/templating.py:1039:1464
+# :1485:1491
+#: ../roundup/cgi/templating.py:1464 ../roundup/cgi/templating.py:1485
+#: ../roundup/cgi/templating.py:1491 ../roundup/cgi/templating.py:1039
+msgid "No"
+msgstr "No"
+
+# ../roundup/cgi/templating.py:1488 ../roundup/cgi/templating.py:1039:1464
+# :1483:1488
+#: ../roundup/cgi/templating.py:1464 ../roundup/cgi/templating.py:1483
+#: ../roundup/cgi/templating.py:1488 ../roundup/cgi/templating.py:1039
+msgid "Yes"
+msgstr "Sì"
+
+#: ../roundup/cgi/templating.py:1577
+msgid ""
+"default value for DateHTMLProperty must be either DateHTMLProperty or string "
+"date representation."
+msgstr ""
+"Il valore predefinito per DateHTMLProperty deve essere  DateHTMLProperty "
+"oppure una stringa rappresentante una data."
+
+#: ../roundup/cgi/templating.py:1737
+#, python-format
+msgid "Attempt to look up %(attr)s on a missing value"
+msgstr "Tentativo di visualizzare %(attr)s con un valore mancante"
+
+#: ../roundup/cgi/templating.py:1810
+#, python-format
+msgid "<option %svalue=\"-1\">- no selection -</option>"
+msgstr "<option %svalue=\"-1\">- nessuna selezione -</option>"
+
+#: ../roundup/date.py:301
+msgid ""
+"Not a date spec: \"yyyy-mm-dd\", \"mm-dd\", \"HH:MM\", \"HH:MM:SS\" or "
+"\"yyyy-mm-dd.HH:MM:SS.SSS\""
+msgstr ""
+"Non specifica una data: \"yyyy-mm-dd\", \"mm-dd\", \"HH:MM\", \"HH:MM:SS\" o "
+"\"yyyy-mm-dd.HH:MM:SS.SSS\""
+
+#: ../roundup/date.py:363
+#, 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 non specifica una data / momento \"yyyy-mm-dd\", \"mm-dd\", \"HH:MM\", "
+"\"HH:MM:SS\" o \"yyyy-mm-dd.HH:MM:SS.SSS\""
+
+#: ../roundup/date.py:662
+msgid ""
+"Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [date spec]"
+msgstr ""
+"Non specifica un intervallo: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [date "
+"spec]"
+
+#: ../roundup/date.py:681
+msgid "Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]"
+msgstr "Non specifica un intervallo: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]"
+
+#: ../roundup/date.py:818
+#, python-format
+msgid "%(number)s year"
+msgid_plural "%(number)s years"
+msgstr[0] "%(number)s anno"
+msgstr[1] "%(numeber)s anni"
+
+#: ../roundup/date.py:822
+#, python-format
+msgid "%(number)s month"
+msgid_plural "%(number)s months"
+msgstr[0] "%(number)s mese"
+msgstr[1] "%(number)s mesi"
+
+#: ../roundup/date.py:826
+#, python-format
+msgid "%(number)s week"
+msgid_plural "%(number)s weeks"
+msgstr[0] "%(number)s settimana"
+msgstr[1] "%(number)s settimane"
+
+#: ../roundup/date.py:830
+#, python-format
+msgid "%(number)s day"
+msgid_plural "%(number)s days"
+msgstr[0] "%(number)s giorno"
+msgstr[1] "%(number)s giorni"
+
+#: ../roundup/date.py:834
+msgid "tomorrow"
+msgstr "domani"
+
+#: ../roundup/date.py:836
+msgid "yesterday"
+msgstr "ieri"
+
+#: ../roundup/date.py:839
+#, python-format
+msgid "%(number)s hour"
+msgid_plural "%(number)s hours"
+msgstr[0] "%(number)s ora"
+msgstr[1] "%(number)s ore"
+
+#: ../roundup/date.py:843
+msgid "an hour"
+msgstr "un'ora"
+
+#: ../roundup/date.py:845
+msgid "1 1/2 hours"
+msgstr "un'ora e mezza"
+
+#: ../roundup/date.py:847
+#, python-format
+msgid "1 %(number)s/4 hours"
+msgid_plural "1 %(number)s/4 hours"
+msgstr[0] "%(number)s quarto d'ora"
+msgstr[1] "%(number)s quarti d'ora"
+
+#: ../roundup/date.py:851
+msgid "in a moment"
+msgstr "in un momento"
+
+#: ../roundup/date.py:853
+msgid "just now"
+msgstr "proprio ora"
+
+#: ../roundup/date.py:856
+msgid "1 minute"
+msgstr "un minuto"
+
+#: ../roundup/date.py:859
+#, python-format
+msgid "%(number)s minute"
+msgid_plural "%(number)s minutes"
+msgstr[0] "%(number)s minuto"
+msgstr[1] "%(number)s minuti"
+
+#: ../roundup/date.py:862
+msgid "1/2 an hour"
+msgstr "mezzora"
+
+#: ../roundup/date.py:864
+#, python-format
+msgid "%(number)s/4 hour"
+msgid_plural "%(number)s/4 hours"
+msgstr[0] "%(number)s ora"
+msgstr[1] "%(number)s ore"
+
+#: ../roundup/date.py:868
+#, python-format
+msgid "%s ago"
+msgstr "%s fa"
+
+#: ../roundup/date.py:870
+#, python-format
+msgid "in %s"
+msgstr "in %s"
+
+#: ../roundup/init.py:134
+#, python-format
+msgid ""
+"WARNING: directory '%s'\n"
+"\tcontains old-style template - ignored"
+msgstr ""
+"ATTENZIONE: La directory '%s'\n"
+"\tcontene un template old-style - ignorato"
+
+#: ../roundup/mailgw.py:583
+msgid ""
+"\n"
+"Emails to Roundup trackers must include a Subject: line!\n"
+msgstr ""
+"\n"
+"Le Email al tracker Roundup devono includere il campo Oggetto: \n"
+
+#: ../roundup/mailgw.py:673
+#, python-format
+msgid ""
+"\n"
+"The message you sent to roundup did not contain a properly formed subject\n"
+"line. The subject must contain a class name or designator to indicate the\n"
+"'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"
+"Il messaggio che hai mandato a Roundup non contiene un campo \"Subject: "
+"\" (Oggetto) ben formato. L'oggetto deve contenere un nome di una classe o "
+"un designatore per indicare l'argomento del messaggio. Per esempio:\n"
+"    Subject: [issue] Questa è una nuova richiesta\n"
+"      - crea una nuova richiesta titolata: 'Questa è una nuova richiesta'.\n"
+"    Subject: [issue1234] Questa è una risposta alla richiesta 1234\n"
+"      - aggiunge un nuovo messaggio alla richiesta 1234 esistente\n"
+"\n"
+"Il campo \"Subject: \" (Oggetto) era: '%(subject)s'\n"
+
+#: ../roundup/mailgw.py:704
+#, python-format
+msgid ""
+"\n"
+"The class name you identified in the subject line (\"%(classname)s\") does "
+"not exist in the\n"
+"database.\n"
+"\n"
+"Valid class names are: %(validname)s\n"
+"Subject was: \"%(subject)s\"\n"
+msgstr ""
+"\n"
+"La classe che hai identificato nel Subject: (\"%(classname)s\") non esiste "
+"nel database.\n"
+"\n"
+"Classi valide sono: %(validname)s\n"
+"Il campo Subject: conteneva il valore \"%(subject)s\"\n"
+
+#: ../roundup/mailgw.py:739
+#, 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"
+"previous subject title intact so I can match that.\n"
+"\n"
+"Subject was: \"%(subject)s\"\n"
+msgstr ""
+"\n"
+"Non è stato possibile far corrispondere il tuo messaggio con un nodo del "
+"database - devi specificare nel Subject (Oggetto) un designatore corretto "
+"(con un numero, per esempio [issue1234]) o alternativamente tenere il "
+"Subject precedente intatto.\n"
+"Il campo Subject: conteneva il valore \"%(subject)s\"\n"
+
+#: ../roundup/mailgw.py:772
+#, python-format
+msgid ""
+"\n"
+"The node specified by the designator in the subject of your message\n"
+"(\"%(nodeid)s\") does not exist.\n"
+"\n"
+"Subject was: \"%(subject)s\"\n"
+msgstr ""
+"\n"
+"Il nodo specificato dal desigantore nel Subject del tuo messaggio (\"%"
+"(nodeid)s\") non esiste.\n"
+"\n"
+"Il campo Subject: conteneva il valore \"%(subject)s\"\n"
+
+#: ../roundup/mailgw.py:800
+#, python-format
+msgid ""
+"\n"
+"The mail gateway is not properly set up. Please contact\n"
+"%(mailadmin)s and have them fix the incorrect class specified as:\n"
+"  %(current_class)s\n"
+msgstr ""
+"\n"
+"Il gateway mail non è configurato correttamente. Per favore contatta\n"
+"%(mailadmin)s e segnala che la classe specificata come:\n"
+"  %(current_class)s è scorretta.\n"
+
+#: ../roundup/mailgw.py:823
+#, python-format
+msgid ""
+"\n"
+"The mail gateway is not properly set up. Please contact\n"
+"%(mailadmin)s and have them fix the incorrect properties:\n"
+"  %(errors)s\n"
+msgstr ""
+"\n"
+"il gateway mail non è configurato correttamente. Per favore contatta\n"
+"%(mailadmin)s e segnala che la proprietà scorretta:\n"
+"  %(errors)s\n"
+
+#: ../roundup/mailgw.py:853
+#, python-format
+msgid ""
+"\n"
+"You are not a registered user.\n"
+"\n"
+"Unknown address: %(from_address)s\n"
+msgstr ""
+"\n"
+"Non sei un utente registrato.\n"
+"\n"
+"Indirizzo sconosciuto: %(from_address)s\n"
+
+#: ../roundup/mailgw.py:861
+msgid "You are not permitted to access this tracker."
+msgstr "Non si dispone di permessi sufficienti per accedere a questo tracker"
+
+#: ../roundup/mailgw.py:868
+#, python-format
+msgid "You are not permitted to edit %(classname)s."
+msgstr "Non si dispone dei permessi sufficienti per modificare %(classname)s"
+
+#: ../roundup/mailgw.py:872
+#, python-format
+msgid "You are not permitted to create %(classname)s."
+msgstr "Non si dispone dei permessi sufficienti per creare %(classname)s"
+
+#: ../roundup/mailgw.py:919
+#, python-format
+msgid ""
+"\n"
+"There were problems handling your subject line argument list:\n"
+"- %(errors)s\n"
+"\n"
+"Subject was: \"%(subject)s\"\n"
+msgstr ""
+"\n"
+"Si è verificato un problema nel gestire la lista degli argomenti nel Subject "
+"del messaggio:- %(errors)s\n"
+"\n"
+"Il Subject era: \"%(subject)s\"\n"
+
+#: ../roundup/mailgw.py:947
+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"
+"Roundup richiede che i valori immessi siano in formato testo. Il parser dei "
+"messaggi non ha trovato alcuna parte text/plain da utilizzare.\n"
+
+#: ../roundup/mailgw.py:969
+msgid "You are not permitted to create files."
+msgstr "Non si dispone dei permessi necessari a creare file."
+
+#: ../roundup/mailgw.py:983
+#, python-format
+msgid "You are not permitted to add files to %(classname)s."
+msgstr ""
+"Non si dispone dei permessi necessari per aggiungere file a %(classname)s"
+
+#: ../roundup/mailgw.py:1001
+msgid "You are not permitted to create messages."
+msgstr "Non si disponde dei permessi necessari per creare messaggi."
+
+#: ../roundup/mailgw.py:1009
+#, python-format
+msgid ""
+"\n"
+"Mail message was rejected by a detector.\n"
+"%(error)s\n"
+msgstr ""
+"\n"
+"La Mail è stata rifiutata da un detector.\n"
+"%(error)s\n"
+
+#: ../roundup/mailgw.py:1017
+#, python-format
+msgid "You are not permitted to add messages to %(classname)s."
+msgstr "Non si dispone dei permessi per aggiungere messaggi a %(classname)s"
+
+#: ../roundup/mailgw.py:1044
+#, python-format
+msgid "You are not permitted to edit property %(prop)s of class %(classname)s."
+msgstr ""
+"Non si dispone dei permessi necessari per modificare la proprietà %(prop)s "
+"della classe %(classname)s"
+
+#: ../roundup/mailgw.py:1052
+#, python-format
+msgid ""
+"\n"
+"There was a problem with the message you sent:\n"
+"   %(message)s\n"
+msgstr ""
+"\n"
+"Si è verificato un problema con il messaggio che hai inviato:\n"
+"   %(message)s\n"
+
+#: ../roundup/mailgw.py:1074
+msgid "not of form [arg=value,value,...;arg=value,value,...]"
+msgstr "Non nel formato [arg=valore,valore,...;arg=valore,valore,...]"
+
+#: ../roundup/roundupdb.py:146
+msgid "files"
+msgstr "file"
+
+#: ../roundup/roundupdb.py:146
+msgid "messages"
+msgstr "messaggi"
+
+#: ../roundup/roundupdb.py:146
+msgid "nosy"
+msgstr "ficcanaso"
+
+#: ../roundup/roundupdb.py:146
+msgid "superseder"
+msgstr "soprassiede"
+
+#: ../roundup/roundupdb.py:146
+msgid "title"
+msgstr "titolo"
+
+#: ../roundup/roundupdb.py:147
+msgid "assignedto"
+msgstr "assegnato a"
+
+#: ../roundup/roundupdb.py:147
+msgid "priority"
+msgstr "priorità"
+
+#: ../roundup/roundupdb.py:147
+msgid "status"
+msgstr "stato"
+
+#: ../roundup/roundupdb.py:147
+msgid "topic"
+msgstr "argomento"
+
+#: ../roundup/roundupdb.py:150
+msgid "activity"
+msgstr "attività"
+
+#. following properties are common for all hyperdb classes
+#. they are listed here to keep things in one place
+#: ../roundup/roundupdb.py:150
+msgid "actor"
+msgstr "attore"
+
+#: ../roundup/roundupdb.py:150
+msgid "creation"
+msgstr "creazione"
+
+#: ../roundup/roundupdb.py:150
+msgid "creator"
+msgstr "creatore"
+
+#: ../roundup/roundupdb.py:308
+#, python-format
+msgid "New submission from %(authname)s%(authaddr)s:"
+msgstr ""
+"È stata aperta una nuova richiesta di assistenza da parte di\n"
+"%(authname)s%(authaddr)s.\n"
+"Il testo della nuova richiesta è il seguente:"
+
+#: ../roundup/roundupdb.py:311
+#, python-format
+msgid "%(authname)s%(authaddr)s added the comment:"
+msgstr ""
+"È stato aggiunto un nuovo messaggio da\n"
+"%(authname)s%(authaddr)s.\n"
+"Il testo del messaggio è il seguente:"
+
+#: ../roundup/roundupdb.py:314
+msgid "System message:"
+msgstr "Messaggio dal sistema:"
+
+#: ../roundup/roundupdb.py:597
+#, 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 ""
+
+#: ../roundup/scripts/roundup_gettext.py:22
+#, python-format
+msgid "Usage: %(program)s <tracker home>"
+msgstr ""
+
+#: ../roundup/scripts/roundup_gettext.py:37
+#, python-format
+msgid "No tracker templates found in directory %s"
+msgstr ""
+
+#: ../roundup/scripts/roundup_mailgw.py:36
+#, python-format
+msgid ""
+"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 / -S: see below\n"
+"\n"
+"The roundup mail gateway may be called in one of four ways:\n"
+" . with an instance home as the only argument,\n"
+" . with both an instance home and a mail spool file,\n"
+" . with both an instance home and a POP/APOP server account, or\n"
+" . with both an instance home and a IMAP/IMAPS server account.\n"
+"\n"
+"It also supports optional -C and -S arguments that allows you to set a\n"
+"fields for a class created by the roundup-mailgw. The default class if\n"
+"not specified is msg, but the other classes: issue, file, user can\n"
+"also be used. The -S or --set options uses the same\n"
+"property=value[;property=value] notation accepted by the command line\n"
+"roundup command or the commands that can be given on the Subject line\n"
+"of an email message.\n"
+"\n"
+"It can let you set the type of the message on a per email address basis.\n"
+"\n"
+"PIPE:\n"
+" In the first case, the mail gateway reads a single message from the\n"
+" standard input and submits the message to the roundup.mailgw module.\n"
+"\n"
+"UNIX mailbox:\n"
+" In the second case, the gateway reads all messages from the mail spool\n"
+" file and submits each in turn to the roundup.mailgw module. The file is\n"
+" emptied once all messages have been successfully handled. The file is\n"
+" specified as:\n"
+"   mailbox /path/to/mailbox\n"
+"\n"
+"POP:\n"
+" In the third case, the gateway reads all messages from the POP server\n"
+" specified and submits each in turn to the roundup.mailgw module. The\n"
+" server is specified as:\n"
+"    pop username:password at server\n"
+" The username and password may be omitted:\n"
+"    pop username at server\n"
+"    pop server\n"
+" 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"
+"\n"
+"IMAP:\n"
+" Connect to an IMAP server. This supports the same notation as that of\n"
+" POP mail.\n"
+"    imap username:password at server\n"
+" It also allows you to specify a specific mailbox other than INBOX using\n"
+" this format:\n"
+"    imap username:password at server mailbox\n"
+"\n"
+"IMAPS:\n"
+" Connect to an IMAP server over ssl.\n"
+" This supports the same notation as IMAP.\n"
+"    imaps username:password at server [mailbox]\n"
+"\n"
+msgstr ""
+
+#: ../roundup/scripts/roundup_mailgw.py:151
+msgid "Error: not enough source specification information"
+msgstr "Errore: insufficienti informazioni sul sorgente"
+
+#: ../roundup/scripts/roundup_mailgw.py:167
+msgid "Error: a later version of python is required"
+msgstr "Erorre: è richiesta una versione più aggiornata di python"
+
+#: ../roundup/scripts/roundup_mailgw.py:170
+msgid "Error: pop specification not valid"
+msgstr "Errore: il pop server specificato non è valido."
+
+#: ../roundup/scripts/roundup_mailgw.py:177
+msgid "Error: apop specification not valid"
+msgstr "Errore: il apop server specificato non è valido."
+
+#: ../roundup/scripts/roundup_mailgw.py:189
+msgid ""
+"Error: The source must be either \"mailbox\", \"pop\", \"apop\", \"imap\" or "
+"\"imaps\""
+msgstr ""
+"Errore: la sorgente deve essere una tra \"mailbox\", \"pop\", \"apop\", "
+"\"imap\" o \"imaps\""
+
+#: ../roundup/scripts/roundup_server.py:157
+msgid ""
+"<html><head><title>Roundup trackers index</title></head>\n"
+"<body><h1>Roundup trackers index</h1><ol>\n"
+msgstr ""
+"<html><head><title>indice dei ticket Roundup</title></head>\n"
+"<body><h1>indice dei ticket Roundup</h1><ol>\n"
+
+#: ../roundup/scripts/roundup_server.py:293
+#, python-format
+msgid "Error: %s: %s"
+msgstr "Errore: %s: %s"
+
+#: ../roundup/scripts/roundup_server.py:303
+msgid "WARNING: ignoring \"-g\" argument, not root"
+msgstr "ATTENZIONE: ignoro il parametro \"-g\", non sei root"
+
+#: ../roundup/scripts/roundup_server.py:309
+msgid "Can't change groups - no grp module"
+msgstr "Non è possibile cambiare gruppo - nessun modulo grp"
+
+#: ../roundup/scripts/roundup_server.py:318
+#, python-format
+msgid "Group %(group)s doesn't exist"
+msgstr "Il gruppo %(group)s non esiste"
+
+#: ../roundup/scripts/roundup_server.py:329
+msgid "Can't run as root!"
+msgstr "Non può essere eseguito come root!"
+
+#: ../roundup/scripts/roundup_server.py:332
+msgid "WARNING: ignoring \"-u\" argument, not root"
+msgstr ""
+
+#: ../roundup/scripts/roundup_server.py:338
+msgid "Can't change users - no pwd module"
+msgstr "Non è possibile cambiare utente - nessun modulo pwd"
+
+#: ../roundup/scripts/roundup_server.py:347
+#, python-format
+msgid "User %(user)s doesn't exist"
+msgstr "L'utente $(user)s non esiste"
+
+#: ../roundup/scripts/roundup_server.py:481
+#, python-format
+msgid "Multiprocess mode \"%s\" is not available, switching to single-process"
+msgstr ""
+"La modalità multiprocesso non è disponibile, viene utilizzata quella a "
+"singolo processo"
+
+#: ../roundup/scripts/roundup_server.py:504
+#, python-format
+msgid "Unable to bind to port %s, port already in use."
+msgstr "Impossibile bindare alla porta %s, la porta risulta già in uso."
+
+#: ../roundup/scripts/roundup_server.py:572
+msgid ""
+" -c <Command>  Windows Service options.\n"
+"               If you want to run the server as a Windows Service, you\n"
+"               must use configuration file to specify tracker homes.\n"
+"               Logfile option is required to run Roundup Tracker service.\n"
+"               Typing \"roundup-server -c help\" shows Windows Services\n"
+"               specifics."
+msgstr ""
+
+#: ../roundup/scripts/roundup_server.py:579
+msgid ""
+" -u <UID>      runs the Roundup web server as this UID\n"
+" -g <GID>      runs the Roundup web server as this GID\n"
+" -d <PIDfile>  run the server in the background and write the server's PID\n"
+"               to the file indicated by PIDfile. The -l option *must* be\n"
+"               specified if -d is used."
+msgstr ""
+
+#: ../roundup/scripts/roundup_server.py:586
+#, python-format
+msgid ""
+"%(message)sUsage: roundup-server [options] [name=tracker home]*\n"
+"\n"
+"Options:\n"
+" -v            print the Roundup version number and exit\n"
+" -h            print this text and exit\n"
+" -S            create or update configuration file and exit\n"
+" -C <fname>    use configuration file <fname>\n"
+" -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"
+" -t <mode>     multiprocess mode (default: %(mp_def)s).\n"
+"               Allowed values: %(mp_types)s.\n"
+"%(os_part)s\n"
+"\n"
+"Long options:\n"
+" --version          print the Roundup version number and exit\n"
+" --help             print this text and exit\n"
+" --save-config      create or update configuration file and exit\n"
+" --config <fname>   use configuration file <fname>\n"
+" All settings of the [main] section of the configuration file\n"
+" also may be specified in form --<name>=<value>\n"
+"\n"
+"Examples:\n"
+"\n"
+" roundup-server -S -C /opt/roundup/etc/roundup-server.ini \\\n"
+"    -n localhost -p 8917 -l /var/log/roundup.log \\\n"
+"    support=/var/spool/roundup-trackers/support\n"
+"\n"
+" roundup-server -C /opt/roundup/etc/roundup-server.ini\n"
+"\n"
+" roundup-server support=/var/spool/roundup-trackers/support\n"
+"\n"
+" roundup-server -d /var/run/roundup.pid -l /var/log/roundup.log \\\n"
+"    support=/var/spool/roundup-trackers/support\n"
+"\n"
+"Configuration file format:\n"
+"   Roundup Server configuration file has common .ini file format.\n"
+"   Configuration file created with 'roundup-server -S' contains\n"
+"   detailed explanations for each option.  Please see that file\n"
+"   for option descriptions.\n"
+"\n"
+"How to use \"name=tracker home\":\n"
+"   These arguments set the tracker home(s) to use. The name is how the\n"
+"   tracker is identified in the URL (it's the first part of the URL path).\n"
+"   The tracker home is the directory that was identified when you did\n"
+"   \"roundup-admin init\". You may specify any number of these name=home\n"
+"   pairs on the command-line. Make sure the name part doesn't include\n"
+"   any url-unsafe characters like spaces, as these confuse IE.\n"
+msgstr ""
+
+#: ../roundup/scripts/roundup_server.py:741
+msgid "Instances must be name=home"
+msgstr "L'istanza deve essere nel formato nome=home"
+
+#: ../roundup/scripts/roundup_server.py:755
+#, python-format
+msgid "Configuration saved to %s"
+msgstr "Configurazione salvata in %s"
+
+#: ../roundup/scripts/roundup_server.py:773
+msgid "Sorry, you can't run the server as a daemon on this Operating System"
+msgstr ""
+"Spiacente, non è possibile utilizzare il server come demone su questo "
+"sistema operativo."
+
+#: ../roundup/scripts/roundup_server.py:788
+#, python-format
+msgid "Roundup server started on %(HOST)s:%(PORT)s"
+msgstr "Il server Roundup è stato attivato su %(HOST)s:%(PORT)s"
+
+#: ../templates/classic/html/_generic.collision.html:4
+#: ../templates/minimal/html/_generic.collision.html:4
+msgid "${class} Edit Collision - ${tracker}"
+msgstr "Collisione tra modifiche: ${class} - ${tracker}"
+
+#: ../templates/classic/html/_generic.collision.html:7
+#: ../templates/minimal/html/_generic.collision.html:7
+msgid "${class} Edit Collision"
+msgstr "Collisione tra modifiche: ${class}"
+
+#: ../templates/classic/html/_generic.collision.html:14
+#: ../templates/minimal/html/_generic.collision.html:14
+msgid ""
+"\n"
+"  There has been a collision. Another user updated this node\n"
+"  while you were editing. Please <a href='${context}'>reload</a>\n"
+"  the node and review your edits.\n"
+msgstr ""
+"\n"
+"  Si è verificata una collisione. Un altro utente ha modificato questo nodo\n"
+"  mentre tu lo stavi modificando. Per favore <a href='${context}'>ricarica</"
+"a>  il nodo e rivedi le tue modifiche.\n"
+
+#: ../templates/classic/html/_generic.help-empty.html:6
+msgid "Please specify your search parameters!"
+msgstr "Per favore specifica un parametro di ricerca."
+
+#: ../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 "Non si dispone dei permessi necessari per visualizzare questa pagina."
+
+#: ../templates/classic/html/_generic.help-list.html:34
+msgid "1..25 out of 50"
+msgstr "1..25 di 50"
+
+#: ../templates/classic/html/_generic.help-search.html:9
+msgid ""
+"Generic template ${template} or version for class ${classname} is not yet "
+"implemented"
+msgstr ""
+"Il template generico ${template} o la versione per la classe ${classname} "
+"non sono ancora stati implementati"
+
+#: ../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 " Annulla "
+
+#: ../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 " Applica "
+
+#: ../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 "aiuto ${property} - ${tracker}"
+
+#: ../templates/classic/html/_generic.help.html:41
+#: ../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 "&laquo; precedente"
+
+#: ../templates/classic/html/_generic.help.html:53
+#: ../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} di ${total}"
+
+#: ../templates/classic/html/_generic.help.html:57
+#: ../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 "successivo &raquo;"
+
+#: ../templates/classic/html/_generic.index.html:6
+#: ../templates/classic/html/_generic.item.html:4
+#: ../templates/minimal/html/_generic.index.html:6
+#: ../templates/minimal/html/_generic.item.html:4
+msgid "${class} editing - ${tracker}"
+msgstr "Modifica di ${class} - ${tracker}"
+
+#: ../templates/classic/html/_generic.index.html:9
+#: ../templates/classic/html/_generic.item.html:7
+#: ../templates/minimal/html/_generic.index.html:9
+#: ../templates/minimal/html/_generic.item.html:7
+msgid "${class} editing"
+msgstr "Modifica di ${class}"
+
+#: ../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.register.html:17
+msgid "Please login with your username and password."
+msgstr "Inserisci il tuo nome utente e la tua password."
+
+#: ../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:50
+#: ../templates/minimal/html/_generic.index.html:50
+msgid "Edit Items"
+msgstr "Modifica L'Oggetto"
+
+#: ../templates/classic/html/file.index.html:4
+msgid "List of files - ${tracker}"
+msgstr "Elenco dei File - ${tracker}"
+
+#: ../templates/classic/html/file.index.html:5
+msgid "List of files"
+msgstr "Elenco dei File"
+
+#: ../templates/classic/html/file.index.html:10
+msgid "Download"
+msgstr ""
+
+#: ../templates/classic/html/file.index.html:11
+#: ../templates/classic/html/file.item.html:27
+msgid "Content Type"
+msgstr ""
+
+#: ../templates/classic/html/file.index.html:12
+msgid "Uploaded By"
+msgstr "Uploadato Da"
+
+#: ../templates/classic/html/file.index.html:13
+#: ../templates/classic/html/msg.item.html:48
+msgid "Date"
+msgstr "Data"
+
+#: ../templates/classic/html/file.item.html:2
+msgid "File display - ${tracker}"
+msgstr ""
+
+#: ../templates/classic/html/file.item.html:4
+msgid "File display"
+msgstr ""
+
+#: ../templates/classic/html/file.item.html:23
+#: ../templates/classic/html/user.register.html:17
+msgid "Name"
+msgstr "Nome"
+
+#: ../templates/classic/html/file.item.html:45
+msgid "download"
+msgstr ""
+
+#: ../templates/classic/html/home.classlist.html:2
+#: ../templates/minimal/html/home.classlist.html:2
+msgid "List of classes - ${tracker}"
+msgstr "Elenco delle Classi - ${tracker}"
+
+#: ../templates/classic/html/home.classlist.html:4
+#: ../templates/minimal/html/home.classlist.html:4
+msgid "List of classes"
+msgstr "Elenco delle Classi"
+
+#: ../templates/classic/html/issue.index.html:4
+#: ../templates/classic/html/issue.index.html:10
+msgid "List of issues"
+msgstr "Elenco dei Ticket"
+
+#: ../templates/classic/html/issue.index.html:27
+#: ../templates/classic/html/issue.item.html:49
+msgid "Priority"
+msgstr "Priorità"
+
+#: ../templates/classic/html/issue.index.html:28
+msgid "ID"
+msgstr "ID"
+
+#: ../templates/classic/html/issue.index.html:29
+msgid "Creation"
+msgstr "Creazione"
+
+#: ../templates/classic/html/issue.index.html:30
+msgid "Activity"
+msgstr "Attività"
+
+#: ../templates/classic/html/issue.index.html:31
+msgid "Actor"
+msgstr "Attore"
+
+#: ../templates/classic/html/issue.index.html:32
+msgid "Topic"
+msgstr "Argomento"
+
+#: ../templates/classic/html/issue.index.html:33
+#: ../templates/classic/html/issue.item.html:44
+msgid "Title"
+msgstr "Titolo"
+
+#: ../templates/classic/html/issue.index.html:34
+#: ../templates/classic/html/issue.item.html:51
+msgid "Status"
+msgstr "Stato"
+
+#: ../templates/classic/html/issue.index.html:35
+msgid "Creator"
+msgstr "Creatore"
+
+#: ../templates/classic/html/issue.index.html:36
+msgid "Assigned&nbsp;To"
+msgstr "Assegnato&nbsp;A"
+
+#: ../templates/classic/html/issue.index.html:104
+msgid "Download as CSV"
+msgstr "Scarica come CSV"
+
+#: ../templates/classic/html/issue.index.html:114
+msgid "Sort on:"
+msgstr "Ordina per:"
+
+#: ../templates/classic/html/issue.index.html:118
+#: ../templates/classic/html/issue.index.html:139
+msgid "- nothing -"
+msgstr "- niente -"
+
+#: ../templates/classic/html/issue.index.html:126
+#: ../templates/classic/html/issue.index.html:147
+msgid "Descending:"
+msgstr "Decrescente:"
+
+#: ../templates/classic/html/issue.index.html:135
+msgid "Group on:"
+msgstr "Raggruppa per:"
+
+#: ../templates/classic/html/issue.index.html:154
+msgid "Redisplay"
+msgstr "Aggiorna"
+
+#: ../templates/classic/html/issue.item.html:7
+msgid "Issue ${id}: ${title} - ${tracker}"
+msgstr "Ticket ${id}: ${title} - ${tracker}"
+
+#: ../templates/classic/html/issue.item.html:10
+msgid "New Issue - ${tracker}"
+msgstr "Nuovo Ticket - ${tracker}"
+
+#: ../templates/classic/html/issue.item.html:14
+msgid "New Issue"
+msgstr "Nuovo Ticket"
+
+#: ../templates/classic/html/issue.item.html:16
+msgid "New Issue Editing"
+msgstr "Modifica Nuovo Ticket"
+
+#: ../templates/classic/html/issue.item.html:19
+msgid "Issue${id}"
+msgstr "Ticket${id}"
+
+#: ../templates/classic/html/issue.item.html:22
+msgid "Issue${id} Editing"
+msgstr "Modifica Ticket${id}"
+
+#: ../templates/classic/html/issue.item.html:56
+msgid "Superseder"
+msgstr "Soprassede"
+
+#: ../templates/classic/html/issue.item.html:61
+msgid "View:"
+msgstr "Visualizza:"
+
+#: ../templates/classic/html/issue.item.html:67
+msgid "Nosy List"
+msgstr "Lista Ficcanaso"
+
+#: ../templates/classic/html/issue.item.html:76
+msgid "Assigned To"
+msgstr "Assegnato a"
+
+#: ../templates/classic/html/issue.item.html:78
+msgid "Topics"
+msgstr "Argomenti"
+
+#: ../templates/classic/html/issue.item.html:86
+msgid "Change Note"
+msgstr "Modifica Nota"
+
+#: ../templates/classic/html/issue.item.html:94
+msgid "File"
+msgstr "File"
+
+#: ../templates/classic/html/issue.item.html:106
+msgid "Make a copy"
+msgstr "Crea una copia"
+
+#: ../templates/classic/html/issue.item.html:114
+#: ../templates/classic/html/user.item.html:152
+#: ../templates/classic/html/user.register.html:69
+#: ../templates/minimal/html/user.item.html:147
+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>Nota:&nbsp;i&nbsp;campi&nbsp;</td> <th class="
+"\"required\">evidenziati</th> <td>&nbsp;sono obbligatori.</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 ""
+"Creato il <b>${creation}</b> da <b>${creator}</b>, ultima modifica <b>"
+"${activity}</b> di <b>${actor}</b>."
+
+#: ../templates/classic/html/issue.item.html:132
+#: ../templates/classic/html/msg.item.html:61
+msgid "Files"
+msgstr ""
+
+#: ../templates/classic/html/issue.item.html:134
+#: ../templates/classic/html/msg.item.html:63
+msgid "File name"
+msgstr "Nome del file"
+
+#: ../templates/classic/html/issue.item.html:135
+#: ../templates/classic/html/msg.item.html:64
+msgid "Uploaded"
+msgstr "Uploadato"
+
+#: ../templates/classic/html/issue.item.html:136
+msgid "Type"
+msgstr "Tipo"
+
+#: ../templates/classic/html/issue.item.html:137
+#: ../templates/classic/html/query.edit.html:30
+msgid "Edit"
+msgstr "Modifica"
+
+#: ../templates/classic/html/issue.item.html:138
+msgid "Remove"
+msgstr "Rimuovi"
+
+#: ../templates/classic/html/issue.item.html:158
+#: ../templates/classic/html/issue.item.html:179
+#: ../templates/classic/html/query.edit.html:50
+msgid "remove"
+msgstr "rimuovi"
+
+#: ../templates/classic/html/issue.item.html:165
+#: ../templates/classic/html/msg.index.html:9
+msgid "Messages"
+msgstr "Messaggi"
+
+#: ../templates/classic/html/issue.item.html:169
+msgid "msg${id} (view)"
+msgstr "msg${id} (visualizza)"
+
+#: ../templates/classic/html/issue.item.html:170
+msgid "Author: ${author}"
+msgstr "Autore: ${author}"
+
+#: ../templates/classic/html/issue.item.html:172
+msgid "Date: ${date}"
+msgstr "Data: ${date}"
+
+#: ../templates/classic/html/issue.search.html:2
+msgid "Issue searching - ${tracker}"
+msgstr "Cerca ticket - ${tracker}"
+
+#: ../templates/classic/html/issue.search.html:4
+msgid "Issue searching"
+msgstr "Cerca Ticket"
+
+#: ../templates/classic/html/issue.search.html:31
+msgid "Filter on"
+msgstr "Filtra su"
+
+#: ../templates/classic/html/issue.search.html:32
+msgid "Display"
+msgstr "Visualizza"
+
+#: ../templates/classic/html/issue.search.html:33
+msgid "Sort on"
+msgstr "Ordina per"
+
+#: ../templates/classic/html/issue.search.html:34
+msgid "Group on"
+msgstr "Raggruppa per"
+
+#: ../templates/classic/html/issue.search.html:38
+msgid "All text*:"
+msgstr "Tutti i testi*:"
+
+#: ../templates/classic/html/issue.search.html:46
+msgid "Title:"
+msgstr "Titolo:"
+
+#: ../templates/classic/html/issue.search.html:56
+msgid "Topic:"
+msgstr "Argomento:"
+
+#: ../templates/classic/html/issue.search.html:64
+msgid "ID:"
+msgstr "ID:"
+
+#: ../templates/classic/html/issue.search.html:72
+msgid "Creation Date:"
+msgstr "Data creazione:"
+
+#: ../templates/classic/html/issue.search.html:83
+msgid "Creator:"
+msgstr "Creatore:"
+
+#: ../templates/classic/html/issue.search.html:85
+msgid "created by me"
+msgstr "creato da me"
+
+#: ../templates/classic/html/issue.search.html:94
+msgid "Activity:"
+msgstr "Attività"
+
+#: ../templates/classic/html/issue.search.html:105
+msgid "Actor:"
+msgstr "Attore:"
+
+#: ../templates/classic/html/issue.search.html:107
+msgid "done by me"
+msgstr "fatto da me"
+
+#: ../templates/classic/html/issue.search.html:118
+msgid "Priority:"
+msgstr "Priorità:"
+
+#: ../templates/classic/html/issue.search.html:120
+#: ../templates/classic/html/issue.search.html:136
+msgid "not selected"
+msgstr "non selezionato"
+
+#: ../templates/classic/html/issue.search.html:131
+msgid "Status:"
+msgstr "Stato:"
+
+#: ../templates/classic/html/issue.search.html:134
+msgid "not resolved"
+msgstr "non risolto"
+
+#: ../templates/classic/html/issue.search.html:149
+msgid "Assigned to:"
+msgstr "Assegnato a:"
+
+#: ../templates/classic/html/issue.search.html:152
+msgid "assigned to me"
+msgstr "assegnato a me"
+
+#: ../templates/classic/html/issue.search.html:154
+msgid "unassigned"
+msgstr "non assegnato"
+
+#: ../templates/classic/html/issue.search.html:164
+msgid "No Sort or group:"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:172
+msgid "Pagesize:"
+msgstr "Risposte per pagina"
+
+#: ../templates/classic/html/issue.search.html:178
+msgid "Start With:"
+msgstr "Inizia con:"
+
+#: ../templates/classic/html/issue.search.html:184
+msgid "Sort Descending:"
+msgstr "Ordinamento Decrescente:"
+
+#: ../templates/classic/html/issue.search.html:191
+msgid "Group Descending:"
+msgstr "Raggruppamento Decrescente:"
+
+#: ../templates/classic/html/issue.search.html:198
+msgid "Query name**:"
+msgstr "Nome della query**:"
+
+#: ../templates/classic/html/issue.search.html:210
+#: ../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 "Ricerca"
+
+#: ../templates/classic/html/issue.search.html:215
+msgid "*: The \"all text\" field will look in message bodies and issue titles"
+msgstr ""
+"*: Il campo \"tutti i testi\" cerca nel corpo del messaggio e nel titolo del "
+"ticket"
+
+#: ../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"
+msgstr ""
+"**: Se viene fornito un nome, la query verrà salvata e resa disponibile come "
+"link"
+
+#: ../templates/classic/html/keyword.item.html:3
+msgid "Keyword editing - ${tracker}"
+msgstr "Modifica Parole Chiave - ${tracker}"
+
+#: ../templates/classic/html/keyword.item.html:5
+msgid "Keyword editing"
+msgstr "Modifica Parole Chiave"
+
+#: ../templates/classic/html/keyword.item.html:11
+msgid "Existing Keywords"
+msgstr "Parole Chiave Inserite"
+
+#: ../templates/classic/html/keyword.item.html:20
+msgid ""
+"To edit an existing keyword (for spelling or typing errors), click on its "
+"entry above."
+msgstr ""
+"Per modificare una parola chiave esistente (per errori di pronuncia o "
+"digitazione), clicca sul link sopra."
+
+#: ../templates/classic/html/keyword.item.html:27
+msgid "To create a new keyword, enter it below and click \"Submit New Entry\"."
+msgstr ""
+"Per creane una nuova parola chiave, inseriscila sotto e clicca su "
+"\"Inserisci nuovo\""
+
+#: ../templates/classic/html/keyword.item.html:37
+msgid "Keyword"
+msgstr "Parola chiave"
+
+#: ../templates/classic/html/msg.index.html:3
+msgid "List of messages - ${tracker}"
+msgstr "Elenco Messaggi - ${tracker}"
+
+#: ../templates/classic/html/msg.index.html:5
+msgid "Message listing"
+msgstr "Elenco Messaggi"
+
+#: ../templates/classic/html/msg.item.html:6
+msgid "Message ${id} - ${tracker}"
+msgstr "Messaggio ${id} - ${tracker}"
+
+#: ../templates/classic/html/msg.item.html:9
+msgid "New Message - ${tracker}"
+msgstr "Nuovo Messaggio - ${tracker}"
+
+#: ../templates/classic/html/msg.item.html:13
+msgid "New Message"
+msgstr "Nuovo Messagggio"
+
+#: ../templates/classic/html/msg.item.html:15
+msgid "New Message Editing"
+msgstr "Modifica Nuovo Messaggio"
+
+#: ../templates/classic/html/msg.item.html:18
+msgid "Message${id}"
+msgstr "Messaggio${id}"
+
+#: ../templates/classic/html/msg.item.html:21
+msgid "Message${id} Editing"
+msgstr "Modifica Messaggio${id}"
+
+#: ../templates/classic/html/msg.item.html:38
+msgid "Author"
+msgstr "Autore"
+
+#: ../templates/classic/html/msg.item.html:43
+msgid "Recipients"
+msgstr "Destinatario"
+
+#: ../templates/classic/html/msg.item.html:54
+msgid "Content"
+msgstr "Contenuto"
+
+#: ../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>Le tue Query</b> (<a href=\"query?@template=edit\">modifica</a>)"
+
+#: ../templates/classic/html/page.html:65
+#: ../templates/minimal/html/page.html:64
+msgid "Issues"
+msgstr "Ticket"
+
+#: ../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 "Crea Nuovo"
+
+#: ../templates/classic/html/page.html:69
+#: ../templates/minimal/html/page.html:68
+msgid "Show Unassigned"
+msgstr "Mostra Non Assegnati"
+
+#: ../templates/classic/html/page.html:81
+#: ../templates/minimal/html/page.html:80
+msgid "Show All"
+msgstr "Mostra Tutti"
+
+#: ../templates/classic/html/page.html:93
+#: ../templates/minimal/html/page.html:92
+msgid "Show issue:"
+msgstr "Mostra ticket:"
+
+#: ../templates/classic/html/page.html:103
+#: ../templates/minimal/html/page.html:102
+msgid "Keywords"
+msgstr "Parole chiave"
+
+#: ../templates/classic/html/page.html:108
+#: ../templates/minimal/html/page.html:107
+msgid "Edit Existing"
+msgstr "Modifica"
+
+#: ../templates/classic/html/page.html:114
+#: ../templates/minimal/html/page.html:113
+msgid "Administration"
+msgstr "Amministrazione"
+
+#: ../templates/classic/html/page.html:116
+#: ../templates/minimal/html/page.html:115
+msgid "Class List"
+msgstr "Elenco Classi"
+
+#: ../templates/classic/html/page.html:120
+#: ../templates/minimal/html/page.html:119
+msgid "User List"
+msgstr "Elenco Utenti"
+
+#: ../templates/classic/html/page.html:122
+#: ../templates/minimal/html/page.html:121
+msgid "Add User"
+msgstr "Aggiungi Utente"
+
+#: ../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 "Login"
+
+#: ../templates/classic/html/page.html:134
+#: ../templates/minimal/html/page.html:133
+msgid "Remember me?"
+msgstr "Ricorda la mia login"
+
+#: ../templates/classic/html/page.html:138
+#: ../templates/classic/html/user.register.html:63
+#: ../templates/minimal/html/page.html:137
+#: ../templates/minimal/html/user.register.html:61
+msgid "Register"
+msgstr "Registra"
+
+#: ../templates/classic/html/page.html:141
+#: ../templates/minimal/html/page.html:140
+msgid "Lost&nbsp;your&nbsp;login?"
+msgstr "Dimenticato&nbsp;la&nbsp;tua&nbsp;Login?"
+
+#: ../templates/classic/html/page.html:146
+#: ../templates/minimal/html/page.html:145
+msgid "Hello, ${user}"
+msgstr "Ciao, ${user}"
+
+#: ../templates/classic/html/page.html:148
+msgid "Your Issues"
+msgstr "I Tuoi Ticket"
+
+#: ../templates/classic/html/page.html:160
+#: ../templates/minimal/html/page.html:147
+msgid "Your Details"
+msgstr "I Tuoi Dettagli"
+
+#: ../templates/classic/html/page.html:162
+#: ../templates/minimal/html/page.html:149
+msgid "Logout"
+msgstr ""
+
+#: ../templates/classic/html/page.html:166
+#: ../templates/minimal/html/page.html:153
+msgid "Help"
+msgstr "Aiuto"
+
+#: ../templates/classic/html/page.html:167
+#: ../templates/minimal/html/page.html:154
+msgid "Roundup docs"
+msgstr "Documentazione su Roundup"
+
+#: ../templates/classic/html/page.html:177
+#: ../templates/minimal/html/page.html:164
+msgid "clear this message"
+msgstr "non visualizzare questo messaggio"
+
+#: ../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 "non importa"
+
+#: ../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:299
+#: ../templates/minimal/html/page.html:286
+msgid "no value"
+msgstr "nessun valore"
+
+#: ../templates/classic/html/query.edit.html:4
+msgid "\"Your Queries\" Editing - ${tracker}"
+msgstr "Modifica de \"Le tue Query\" - ${tracker}"
+
+#: ../templates/classic/html/query.edit.html:6
+msgid "\"Your Queries\" Editing"
+msgstr "Modifica de \"Le tue Query\""
+
+#: ../templates/classic/html/query.edit.html:11
+msgid "You are not allowed to edit queries."
+msgstr "Non si dispone dei permessi necessari per modificare delle query."
+
+#: ../templates/classic/html/query.edit.html:28
+msgid "Query"
+msgstr "Query"
+
+#: ../templates/classic/html/query.edit.html:29
+msgid "Include in \"Your Queries\""
+msgstr "Includi in \"Le tue Query\""
+
+#: ../templates/classic/html/query.edit.html:31
+msgid "Private to you?"
+msgstr "Personale?"
+
+#: ../templates/classic/html/query.edit.html:44
+msgid "leave out"
+msgstr "lascia fuori"
+
+#: ../templates/classic/html/query.edit.html:45
+msgid "include"
+msgstr "includi"
+
+#: ../templates/classic/html/query.edit.html:49
+msgid "leave in"
+msgstr "lascia dentro"
+
+#: ../templates/classic/html/query.edit.html:54
+msgid "[query is retired]"
+msgstr "[la query è stata ritirata]"
+
+#: ../templates/classic/html/query.edit.html:67
+#: ../templates/classic/html/query.edit.html:92
+msgid "edit"
+msgstr "modifica"
+
+#: ../templates/classic/html/query.edit.html:71
+msgid "yes"
+msgstr "sì"
+
+#: ../templates/classic/html/query.edit.html:73
+msgid "no"
+msgstr "no"
+
+#: ../templates/classic/html/query.edit.html:79
+msgid "Delete"
+msgstr "Elimina"
+
+#: ../templates/classic/html/query.edit.html:94
+msgid "[not yours to edit]"
+msgstr "[non sei il proprietario della query"
+
+#: ../templates/classic/html/query.edit.html:102
+msgid "Save Selection"
+msgstr "Salva la selezione"
+
+#: ../templates/classic/html/user.forgotten.html:3
+msgid "Password reset request - ${tracker}"
+msgstr "Richiesta nuova password - %{tracker}"
+
+#: ../templates/classic/html/user.forgotten.html:5
+msgid "Password reset request"
+msgstr "Richiesta nuova password"
+
+#: ../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 ""
+
+#: ../templates/classic/html/user.forgotten.html:16
+msgid "Email Address:"
+msgstr "Indirizzo di Email:"
+
+#: ../templates/classic/html/user.forgotten.html:24
+#: ../templates/classic/html/user.forgotten.html:34
+msgid "Request password reset"
+msgstr "Richiedi una nuova password"
+
+#: ../templates/classic/html/user.forgotten.html:30
+msgid "Or, if you know your username, then enter it below."
+msgstr ""
+
+#: ../templates/classic/html/user.forgotten.html:33
+msgid "Username:"
+msgstr "Nome Utente:"
+
+#: ../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 ""
+
+#: ../templates/classic/html/user.help-search.html:73
+msgid "Pagesize"
+msgstr "Dimensione della Pagina"
+
+#: ../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 ""
+"Il tuo browser non permette di utilizzare i frame; verrai rediretto "
+"immediatamente, oppure alternativamente visita ${link}."
+
+#: ../templates/classic/html/user.index.html:3
+#: ../templates/minimal/html/user.index.html:3
+msgid "User listing - ${tracker}"
+msgstr "Elenco Utenti - ${tracker}"
+
+#: ../templates/classic/html/user.index.html:5
+#: ../templates/minimal/html/user.index.html:5
+msgid "User listing"
+msgstr "Elenco Utenti"
+
+#: ../templates/classic/html/user.index.html:19
+#: ../templates/minimal/html/user.index.html:19
+msgid "Username"
+msgstr "Nome Utente"
+
+#: ../templates/classic/html/user.index.html:20
+msgid "Real name"
+msgstr "Nome Completo"
+
+#: ../templates/classic/html/user.index.html:21
+#: ../templates/classic/html/user.register.html:45
+msgid "Organisation"
+msgstr "Organizzazione"
+
+#: ../templates/classic/html/user.index.html:22
+#: ../templates/minimal/html/user.index.html:20
+msgid "Email address"
+msgstr "Indirizzo di Email"
+
+#: ../templates/classic/html/user.index.html:23
+msgid "Phone number"
+msgstr "Telefono"
+
+#: ../templates/classic/html/user.index.html:24
+msgid "Retire"
+msgstr "Dismetti"
+
+#: ../templates/classic/html/user.index.html:37
+msgid "retire"
+msgstr "dismetti"
+
+#: ../templates/classic/html/user.item.html:9
+#: ../templates/minimal/html/user.item.html:9
+msgid "User ${id}: ${title} - ${tracker}"
+msgstr "Utente ${id}: ${title} - ${tracker}"
+
+#: ../templates/classic/html/user.item.html:12
+#: ../templates/minimal/html/user.item.html:12
+msgid "New User - ${tracker}"
+msgstr "Nuovo Utente - ${tracker}"
+
+#: ../templates/classic/html/user.item.html:21
+#: ../templates/minimal/html/user.item.html:21
+msgid "New User"
+msgstr "Nuovo Utente"
+
+#: ../templates/classic/html/user.item.html:23
+#: ../templates/minimal/html/user.item.html:23
+msgid "New User Editing"
+msgstr "Modifica Nuovo Utente"
+
+#: ../templates/classic/html/user.item.html:26
+#: ../templates/minimal/html/user.item.html:26
+msgid "User${id}"
+msgstr "Utente${id}"
+
+#: ../templates/classic/html/user.item.html:29
+#: ../templates/minimal/html/user.item.html:29
+msgid "User${id} Editing"
+msgstr "Modifica Utente${id}"
+
+#: ../templates/classic/html/user.item.html:79
+#: ../templates/classic/html/user.register.html:33
+#: ../templates/minimal/html/user.item.html:74
+#: ../templates/minimal/html/user.register.html:41
+msgid "Roles"
+msgstr "Ruoli"
+
+#: ../templates/classic/html/user.item.html:87
+#: ../templates/minimal/html/user.item.html:82
+msgid "(to give the user more than one role, enter a comma,separated,list)"
+msgstr ""
+"(per fornire ad un utente più di un ruolo, inserire l'elenco separato da "
+"spazi)"
+
+#: ../templates/classic/html/user.item.html:108
+#: ../templates/minimal/html/user.item.html:103
+msgid "(this is a numeric hour offset, the default is ${zone})"
+msgstr ""
+"(questo è la differenza in numero di ore, il valore predefinito è ${zone})"
+
+#: ../templates/classic/html/user.item.html:129
+#: ../templates/classic/html/user.register.html:53
+#: ../templates/minimal/html/user.item.html:124
+#: ../templates/minimal/html/user.register.html:53
+msgid "Alternate E-mail addresses<br>One address per line"
+msgstr "Indirizzi di mail alternativi<br>Un indirizzo per linea"
+
+#: ../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 "Registrando con ${tracker}"
+
+#: ../templates/classic/html/user.register.html:21
+#: ../templates/minimal/html/user.register.html:29
+msgid "Login Name"
+msgstr "Nome Utente"
+
+#: ../templates/classic/html/user.register.html:25
+#: ../templates/minimal/html/user.register.html:33
+msgid "Login Password"
+msgstr "Password"
+
+#: ../templates/classic/html/user.register.html:29
+#: ../templates/minimal/html/user.register.html:37
+msgid "Confirm Password"
+msgstr "Conferma Password"
+
+#: ../templates/classic/html/user.register.html:41
+msgid "Phone"
+msgstr "Telefono"
+
+#: ../templates/classic/html/user.register.html:49
+#: ../templates/minimal/html/user.register.html:49
+msgid "E-mail address"
+msgstr "Indirizzo di Email"
+
+#: ../templates/classic/html/user.rego_progress.html:4
+#: ../templates/minimal/html/user.rego_progress.html:4
+msgid "Registration in progress - ${tracker}"
+msgstr "Registrazione in atto - ${tracker}"
+
+#: ../templates/classic/html/user.rego_progress.html:6
+#: ../templates/minimal/html/user.rego_progress.html:6
+msgid "Registration in progress..."
+msgstr "Registrazione in atto..."
+
+#: ../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 ""
+"Riceverai a breve una mail di conferma della tua registrazione. Per "
+"completare il processo di registrazione, visita il link indicato nella mail."
+
+#: ../templates/classic/initial_data.py:5
+msgid "critical"
+msgstr "critico"
+
+#: ../templates/classic/initial_data.py:6
+msgid "urgent"
+msgstr "urgente"
+
+#: ../templates/classic/initial_data.py:7
+msgid "bug"
+msgstr "bug"
+
+#: ../templates/classic/initial_data.py:8
+msgid "feature"
+msgstr ""
+
+#: ../templates/classic/initial_data.py:9
+msgid "wish"
+msgstr ""
+
+#: ../templates/classic/initial_data.py:12
+msgid "unread"
+msgstr ""
+
+#: ../templates/classic/initial_data.py:13
+msgid "deferred"
+msgstr ""
+
+#: ../templates/classic/initial_data.py:14
+msgid "chatting"
+msgstr ""
+
+#: ../templates/classic/initial_data.py:15
+msgid "need-eg"
+msgstr ""
+
+#: ../templates/classic/initial_data.py:16
+msgid "in-progress"
+msgstr ""
+
+#: ../templates/classic/initial_data.py:17
+msgid "testing"
+msgstr ""
+
+#: ../templates/classic/initial_data.py:18
+msgid "done-cbb"
+msgstr ""
+
+#: ../templates/classic/initial_data.py:19
+msgid "resolved"
+msgstr ""
+
+#: ../templates/minimal/html/home.html:2
+msgid "Tracker home - ${tracker}"
+msgstr ""
+
+#: ../templates/minimal/html/home.html:4
+msgid "Tracker home"
+msgstr ""
+
+#: ../templates/minimal/html/home.html:16
+msgid "Please select from one of the menu options on the left."
+msgstr "Per favore seleziona una delle opzioni dal menù a sinistra"
+
+#: ../templates/minimal/html/home.html:19
+msgid "Please log in or register."
+msgstr "Per favore esegui la login o la registrazione."

Modified: tracker/roundup-src/locale/lt.po
==============================================================================
--- tracker/roundup-src/locale/lt.po	(original)
+++ tracker/roundup-src/locale/lt.po	Sun Mar 15 22:43:30 2009
@@ -1,7 +1,7 @@
 # Lithuanian message file for Roundup Issue Tracker
 # Aiste Kesminaite <aiste at pov.lt>, 2005
 #
-# $Id: lt.po,v 1.8 2007/01/04 17:14:29 a1s Exp $
+# $Id: lt.po,v 1.8 2007-01-04 17:14:29 a1s Exp $
 #
 # roundup.pot revision 1.20
 #

Modified: tracker/roundup-src/locale/roundup.pot
==============================================================================
--- tracker/roundup-src/locale/roundup.pot	(original)
+++ tracker/roundup-src/locale/roundup.pot	Sun Mar 15 22:43:30 2009
@@ -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: 2007-09-27 11:18+0300\n"
+"POT-Creation-Date: 2009-03-12 11:58+0200\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,34 @@
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
 
-#: ../roundup/admin.py:86 ../roundup/admin.py:989 ../roundup/admin.py:1040
-#: ../roundup/admin.py:1063 ../roundup/admin.py:86:989 :1040:1063
+#: ../roundup/actions.py:53 ../roundup/cgi/actions.py:120
+msgid "You may not retire the admin or anonymous user"
+msgstr ""
+
+#: ../roundup/actions.py:66 ../roundup/cgi/actions.py:57
+#, python-format
+msgid "You do not have permission to %(action)s the %(classname)s class."
+msgstr ""
+
+#: ../roundup/admin.py:83 ../roundup/admin.py:986 ../roundup/admin.py:1037
+#: ../roundup/admin.py:1060 ../roundup/admin.py:83:986 :1037:1060
 #, python-format
 msgid "no such class \"%(classname)s\""
 msgstr ""
 
-#: ../roundup/admin.py:96 ../roundup/admin.py:100 ../roundup/admin.py:96:100
+#: ../roundup/admin.py:93 ../roundup/admin.py:97 ../roundup/admin.py:93:97
 #, python-format
 msgid "argument \"%(arg)s\" not propname=value"
 msgstr ""
 
-#: ../roundup/admin.py:113
+#: ../roundup/admin.py:110
 #, python-format
 msgid ""
 "Problem: %(message)s\n"
 "\n"
 msgstr ""
 
-#: ../roundup/admin.py:114
+#: ../roundup/admin.py:111
 #, python-format
 msgid ""
 "%(message)sUsage: roundup-admin [options] [<command> <arguments>]\n"
@@ -62,17 +71,17 @@
 " roundup-admin help all                   -- all available help\n"
 msgstr ""
 
-#: ../roundup/admin.py:141
+#: ../roundup/admin.py:138
 msgid "Commands:"
 msgstr ""
 
-#: ../roundup/admin.py:148
+#: ../roundup/admin.py:145
 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:178
+#: ../roundup/admin.py:175
 msgid ""
 "\n"
 "All commands (except help) require a tracker specifier. This is just\n"
@@ -137,12 +146,12 @@
 "Command help:\n"
 msgstr ""
 
-#: ../roundup/admin.py:241
+#: ../roundup/admin.py:238
 #, python-format
 msgid "%s:"
 msgstr ""
 
-#: ../roundup/admin.py:246
+#: ../roundup/admin.py:243
 msgid ""
 "Usage: help topic\n"
 "        Give help about topic.\n"
@@ -154,20 +163,20 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:269
+#: ../roundup/admin.py:266
 #, python-format
 msgid "Sorry, no help for \"%(topic)s\""
 msgstr ""
 
-#: ../roundup/admin.py:346 ../roundup/admin.py:402 ../roundup/admin.py:346:402
+#: ../roundup/admin.py:343 ../roundup/admin.py:399 ../roundup/admin.py:343:399
 msgid "Templates:"
 msgstr ""
 
-#: ../roundup/admin.py:349 ../roundup/admin.py:413 ../roundup/admin.py:349:413
+#: ../roundup/admin.py:346 ../roundup/admin.py:410 ../roundup/admin.py:346:410
 msgid "Back ends:"
 msgstr ""
 
-#: ../roundup/admin.py:352
+#: ../roundup/admin.py:349
 msgid ""
 "Usage: install [template [backend [key=val[,key=val]]]]\n"
 "        Install a new Roundup tracker.\n"
@@ -193,22 +202,22 @@
 "        "
 msgstr ""
 
-#: ../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
+#: ../roundup/admin.py:372 ../roundup/admin.py:469 ../roundup/admin.py:530
+#: ../roundup/admin.py:609 ../roundup/admin.py:660 ../roundup/admin.py:718
+#: ../roundup/admin.py:739 ../roundup/admin.py:767 ../roundup/admin.py:839
+#: ../roundup/admin.py:906 ../roundup/admin.py:977 ../roundup/admin.py:1027
+#: ../roundup/admin.py:1050 ../roundup/admin.py:1081 ../roundup/admin.py:1177
+#: ../roundup/admin.py:1250 ../roundup/admin.py:372:469 :1027:1050 :1081:1177
+#: :1250 :530:609 :660:718 :739:767 :839:906 :977
 msgid "Not enough arguments supplied"
 msgstr ""
 
-#: ../roundup/admin.py:381
+#: ../roundup/admin.py:378
 #, python-format
 msgid "Instance home parent directory \"%(parent)s\" does not exist"
 msgstr ""
 
-#: ../roundup/admin.py:389
+#: ../roundup/admin.py:386
 #, python-format
 msgid ""
 "WARNING: There appears to be a tracker in \"%(tracker_home)s\"!\n"
@@ -216,20 +225,20 @@
 "Erase it? Y/N: "
 msgstr ""
 
-#: ../roundup/admin.py:404
+#: ../roundup/admin.py:401
 msgid "Select template [classic]: "
 msgstr ""
 
-#: ../roundup/admin.py:415
+#: ../roundup/admin.py:412
 msgid "Select backend [anydbm]: "
 msgstr ""
 
-#: ../roundup/admin.py:425
+#: ../roundup/admin.py:422
 #, python-format
 msgid "Error in configuration settings: \"%s\""
 msgstr ""
 
-#: ../roundup/admin.py:434
+#: ../roundup/admin.py:431
 #, python-format
 msgid ""
 "\n"
@@ -238,11 +247,11 @@
 "   %(config_file)s"
 msgstr ""
 
-#: ../roundup/admin.py:444
+#: ../roundup/admin.py:441
 msgid " ... at a minimum, you must set following options:"
 msgstr ""
 
-#: ../roundup/admin.py:449
+#: ../roundup/admin.py:446
 #, python-format
 msgid ""
 "\n"
@@ -258,7 +267,7 @@
 "---------------------------------------------------------------------------\n"
 msgstr ""
 
-#: ../roundup/admin.py:467
+#: ../roundup/admin.py:464
 msgid ""
 "Usage: genconfig <filename>\n"
 "        Generate a new tracker config file (ini style) with default values\n"
@@ -267,7 +276,7 @@
 msgstr ""
 
 #. password
-#: ../roundup/admin.py:477
+#: ../roundup/admin.py:474
 msgid ""
 "Usage: initialise [adminpw]\n"
 "        Initialise a new Roundup tracker.\n"
@@ -278,30 +287,30 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:491
+#: ../roundup/admin.py:488
 msgid "Admin Password: "
 msgstr ""
 
-#: ../roundup/admin.py:492
+#: ../roundup/admin.py:489
 msgid "       Confirm: "
 msgstr ""
 
-#: ../roundup/admin.py:496
+#: ../roundup/admin.py:493
 msgid "Instance home does not exist"
 msgstr ""
 
-#: ../roundup/admin.py:500
+#: ../roundup/admin.py:497
 msgid "Instance has not been installed"
 msgstr ""
 
-#: ../roundup/admin.py:505
+#: ../roundup/admin.py:502
 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:526
+#: ../roundup/admin.py:523
 msgid ""
 "Usage: get property designator[,designator]*\n"
 "        Get the given property of one or more designator(s).\n"
@@ -311,23 +320,23 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:566 ../roundup/admin.py:581 ../roundup/admin.py:566:581
+#: ../roundup/admin.py:563 ../roundup/admin.py:578 ../roundup/admin.py:563:578
 #, python-format
 msgid "property %s is not of type Multilink or Link so -d flag does not apply."
 msgstr ""
 
-#: ../roundup/admin.py:589 ../roundup/admin.py:991 ../roundup/admin.py:1042
-#: ../roundup/admin.py:1065 ../roundup/admin.py:589:991 :1042:1065
+#: ../roundup/admin.py:586 ../roundup/admin.py:988 ../roundup/admin.py:1039
+#: ../roundup/admin.py:1062 ../roundup/admin.py:586:988 :1039:1062
 #, python-format
 msgid "no such %(classname)s node \"%(nodeid)s\""
 msgstr ""
 
-#: ../roundup/admin.py:591
+#: ../roundup/admin.py:588
 #, python-format
 msgid "no such %(classname)s property \"%(propname)s\""
 msgstr ""
 
-#: ../roundup/admin.py:600
+#: ../roundup/admin.py:597
 msgid ""
 "Usage: set items property=value property=value ...\n"
 "        Set the given properties of one or more items(s).\n"
@@ -342,7 +351,7 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:655
+#: ../roundup/admin.py:652
 msgid ""
 "Usage: find classname propname=value ...\n"
 "        Find the nodes of the given class with a given link property value.\n"
@@ -353,13 +362,13 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:708 ../roundup/admin.py:862 ../roundup/admin.py:874
-#: ../roundup/admin.py:928 ../roundup/admin.py:708:862 :874:928
+#: ../roundup/admin.py:705 ../roundup/admin.py:859 ../roundup/admin.py:871
+#: ../roundup/admin.py:925 ../roundup/admin.py:705:859 :871:925
 #, python-format
 msgid "%(classname)s has no property \"%(propname)s\""
 msgstr ""
 
-#: ../roundup/admin.py:715
+#: ../roundup/admin.py:712
 msgid ""
 "Usage: specification classname\n"
 "        Show the properties for a classname.\n"
@@ -368,17 +377,17 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:730
+#: ../roundup/admin.py:727
 #, python-format
 msgid "%(key)s: %(value)s (key property)"
 msgstr ""
 
-#: ../roundup/admin.py:732 ../roundup/admin.py:759 ../roundup/admin.py:732:759
+#: ../roundup/admin.py:729 ../roundup/admin.py:756 ../roundup/admin.py:729:756
 #, python-format
 msgid "%(key)s: %(value)s"
 msgstr ""
 
-#: ../roundup/admin.py:735
+#: ../roundup/admin.py:732
 msgid ""
 "Usage: display designator[,designator]*\n"
 "        Show the property values for the given node(s).\n"
@@ -388,7 +397,7 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:762
+#: ../roundup/admin.py:759
 msgid ""
 "Usage: create classname property=value ...\n"
 "        Create a new entry of a given class.\n"
@@ -400,31 +409,31 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:789
+#: ../roundup/admin.py:786
 #, python-format
 msgid "%(propname)s (Password): "
 msgstr ""
 
-#: ../roundup/admin.py:791
+#: ../roundup/admin.py:788
 #, python-format
 msgid "   %(propname)s (Again): "
 msgstr ""
 
-#: ../roundup/admin.py:793
+#: ../roundup/admin.py:790
 msgid "Sorry, try again..."
 msgstr ""
 
-#: ../roundup/admin.py:797
+#: ../roundup/admin.py:794
 #, python-format
 msgid "%(propname)s (%(proptype)s): "
 msgstr ""
 
-#: ../roundup/admin.py:815
+#: ../roundup/admin.py:812
 #, python-format
 msgid "you must provide the \"%(propname)s\" property."
 msgstr ""
 
-#: ../roundup/admin.py:827
+#: ../roundup/admin.py:824
 msgid ""
 "Usage: list classname [property]\n"
 "        List the instances of a class.\n"
@@ -440,16 +449,16 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:840
+#: ../roundup/admin.py:837
 msgid "Too many arguments supplied"
 msgstr ""
 
-#: ../roundup/admin.py:876
+#: ../roundup/admin.py:873
 #, python-format
 msgid "%(nodeid)4s: %(value)s"
 msgstr ""
 
-#: ../roundup/admin.py:880
+#: ../roundup/admin.py:877
 msgid ""
 "Usage: table classname [property[,property]*]\n"
 "        List the instances of a class in tabular form.\n"
@@ -481,12 +490,12 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:924
+#: ../roundup/admin.py:921
 #, python-format
 msgid "\"%(spec)s\" not name:width"
 msgstr ""
 
-#: ../roundup/admin.py:974
+#: ../roundup/admin.py:971
 msgid ""
 "Usage: history designator\n"
 "        Show the history entries of a designator.\n"
@@ -495,7 +504,7 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:995
+#: ../roundup/admin.py:992
 msgid ""
 "Usage: commit\n"
 "        Commit changes made to the database during an interactive session.\n"
@@ -509,7 +518,7 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:1010
+#: ../roundup/admin.py:1007
 msgid ""
 "Usage: rollback\n"
 "        Undo all changes that are pending commit to the database.\n"
@@ -521,7 +530,7 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:1023
+#: ../roundup/admin.py:1020
 msgid ""
 "Usage: retire designator[,designator]*\n"
 "        Retire the node specified by designator.\n"
@@ -531,7 +540,7 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:1047
+#: ../roundup/admin.py:1044
 msgid ""
 "Usage: restore designator[,designator]*\n"
 "        Restore the retired node specified by designator.\n"
@@ -541,7 +550,7 @@
 msgstr ""
 
 #. grab the directory to export to
-#: ../roundup/admin.py:1070
+#: ../roundup/admin.py:1067
 msgid ""
 "Usage: export [[-]class[,class]] export_dir\n"
 "        Export the database to colon-separated-value files.\n"
@@ -557,7 +566,7 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:1145
+#: ../roundup/admin.py:1142
 msgid ""
 "Usage: exporttables [[-]class[,class]] export_dir\n"
 "        Export the database to colon-separated-value files, excluding the\n"
@@ -574,7 +583,7 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:1160
+#: ../roundup/admin.py:1157
 msgid ""
 "Usage: import import_dir\n"
 "        Import a database from the directory containing CSV files,\n"
@@ -597,7 +606,7 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:1235
+#: ../roundup/admin.py:1232
 msgid ""
 "Usage: pack period | date\n"
 "\n"
@@ -619,11 +628,11 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:1263
+#: ../roundup/admin.py:1260
 msgid "Invalid format"
 msgstr ""
 
-#: ../roundup/admin.py:1274
+#: ../roundup/admin.py:1271
 msgid ""
 "Usage: reindex [classname|designator]*\n"
 "        Re-generate a tracker's search indexes.\n"
@@ -633,321 +642,354 @@
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:1288
+#: ../roundup/admin.py:1285
 #, python-format
 msgid "no such item \"%(designator)s\""
 msgstr ""
 
-#: ../roundup/admin.py:1298
+#: ../roundup/admin.py:1295
 msgid ""
 "Usage: security [Role name]\n"
 "        Display the Permissions available to one or all Roles.\n"
 "        "
 msgstr ""
 
-#: ../roundup/admin.py:1306
+#: ../roundup/admin.py:1303
 #, python-format
 msgid "No such Role \"%(role)s\""
 msgstr ""
 
-#: ../roundup/admin.py:1312
+#: ../roundup/admin.py:1309
 #, python-format
 msgid "New Web users get the Roles \"%(role)s\""
 msgstr ""
 
-#: ../roundup/admin.py:1314
+#: ../roundup/admin.py:1311
 #, python-format
 msgid "New Web users get the Role \"%(role)s\""
 msgstr ""
 
-#: ../roundup/admin.py:1317
+#: ../roundup/admin.py:1314
 #, python-format
 msgid "New Email users get the Roles \"%(role)s\""
 msgstr ""
 
-#: ../roundup/admin.py:1319
+#: ../roundup/admin.py:1316
 #, python-format
 msgid "New Email users get the Role \"%(role)s\""
 msgstr ""
 
-#: ../roundup/admin.py:1322
+#: ../roundup/admin.py:1319
 #, python-format
 msgid "Role \"%(name)s\":"
 msgstr ""
 
-#: ../roundup/admin.py:1327
+#: ../roundup/admin.py:1324
 #, python-format
 msgid " %(description)s (%(name)s for \"%(klass)s\": %(properties)s only)"
 msgstr ""
 
-#: ../roundup/admin.py:1330
+#: ../roundup/admin.py:1327
 #, python-format
 msgid " %(description)s (%(name)s for \"%(klass)s\" only)"
 msgstr ""
 
-#: ../roundup/admin.py:1333
+#: ../roundup/admin.py:1330
 #, python-format
 msgid " %(description)s (%(name)s)"
 msgstr ""
 
-#: ../roundup/admin.py:1362
+#: ../roundup/admin.py:1335
+msgid ""
+"Usage: migrate\n"
+"        Update a tracker's database to be compatible with the Roundup\n"
+"        codebase.\n"
+"\n"
+"        You should run the \"migrate\" command for your tracker once you've\n"
+"        installed the latest codebase. \n"
+"\n"
+"        Do this before you use the web, command-line or mail interface and\n"
+"        before any users access the tracker.\n"
+"\n"
+"        This command will respond with either \"Tracker updated\" (if you've\n"
+"        not previously run it on an RDBMS backend) or \"No migration action\n"
+"        required\" (if you have run it, or have used another interface to "
+"the\n"
+"        tracker, or possibly because you are using anydbm).\n"
+"\n"
+"        It's safe to run this even if it's not required, so just get into\n"
+"        the habit.\n"
+"        "
+msgstr ""
+
+#: ../roundup/admin.py:1354
+msgid "Tracker updated"
+msgstr ""
+
+#: ../roundup/admin.py:1357
+msgid "No migration action required"
+msgstr ""
+
+#: ../roundup/admin.py:1386
 #, python-format
 msgid "Unknown command \"%(command)s\" (\"help commands\" for a list)"
 msgstr ""
 
-#: ../roundup/admin.py:1368
+#: ../roundup/admin.py:1392
 #, python-format
 msgid "Multiple commands match \"%(command)s\": %(list)s"
 msgstr ""
 
-#: ../roundup/admin.py:1375
+#: ../roundup/admin.py:1399
 msgid "Enter tracker home: "
 msgstr ""
 
-#: ../roundup/admin.py:1382 ../roundup/admin.py:1388 ../roundup/admin.py:1408
-#: ../roundup/admin.py:1382:1388 :1408
+#: ../roundup/admin.py:1406 ../roundup/admin.py:1412 ../roundup/admin.py:1432
+#: ../roundup/admin.py:1406:1412 :1432
 #, python-format
 msgid "Error: %(message)s"
 msgstr ""
 
-#: ../roundup/admin.py:1396
+#: ../roundup/admin.py:1420
 #, python-format
 msgid "Error: Couldn't open tracker: %(message)s"
 msgstr ""
 
-#: ../roundup/admin.py:1421
+#: ../roundup/admin.py:1445
 #, python-format
 msgid ""
 "Roundup %s ready for input.\n"
 "Type \"help\" for help."
 msgstr ""
 
-#: ../roundup/admin.py:1426
+#: ../roundup/admin.py:1450
 msgid "Note: command history and editing not available"
 msgstr ""
 
-#: ../roundup/admin.py:1430
+#: ../roundup/admin.py:1454
 msgid "roundup> "
 msgstr ""
 
-#: ../roundup/admin.py:1432
+#: ../roundup/admin.py:1456
 msgid "exit..."
 msgstr ""
 
-#: ../roundup/admin.py:1442
+#: ../roundup/admin.py:1466
 msgid "There are unsaved changes. Commit them (y/N)? "
 msgstr ""
 
-#: ../roundup/backends/back_anydbm.py:219
+#: ../roundup/backends/back_anydbm.py:218
 #: ../roundup/backends/sessions_dbm.py:50
 msgid "Couldn't identify database type"
 msgstr ""
 
-#: ../roundup/backends/back_anydbm.py:245
+#: ../roundup/backends/back_anydbm.py:244
 #, 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
+#: ../roundup/backends/back_anydbm.py:799
+#: ../roundup/backends/back_anydbm.py:1074
+#: ../roundup/backends/back_anydbm.py:1271
+#: ../roundup/backends/back_anydbm.py:1289
+#: ../roundup/backends/back_anydbm.py:1335
+#: ../roundup/backends/back_anydbm.py:1905
+#: ../roundup/backends/back_anydbm.py:799:1074
+#: ../roundup/backends/rdbms_common.py:1396
+#: ../roundup/backends/rdbms_common.py:1625
+#: ../roundup/backends/rdbms_common.py:1831
+#: ../roundup/backends/rdbms_common.py:1851
+#: ../roundup/backends/rdbms_common.py:1904
+#: ../roundup/backends/rdbms_common.py:2512
+#: ../roundup/backends/rdbms_common.py:1396:1625 :1271:1289 :1335:1905
+#: :1831:1851 :1904:2512
 msgid "Database open read-only"
 msgstr ""
 
-#: ../roundup/backends/back_anydbm.py:2003
+#: ../roundup/backends/back_anydbm.py:2007
 #, python-format
 msgid "WARNING: invalid date tuple %r"
 msgstr ""
 
-#: ../roundup/backends/rdbms_common.py:1449
+#: ../roundup/backends/rdbms_common.py:1525
 msgid "create"
 msgstr ""
 
-#: ../roundup/backends/rdbms_common.py:1615
+#: ../roundup/backends/rdbms_common.py:1691
 msgid "unlink"
 msgstr ""
 
-#: ../roundup/backends/rdbms_common.py:1619
+#: ../roundup/backends/rdbms_common.py:1695
 msgid "link"
 msgstr ""
 
-#: ../roundup/backends/rdbms_common.py:1741
+#: ../roundup/backends/rdbms_common.py:1817
 msgid "set"
 msgstr ""
 
-#: ../roundup/backends/rdbms_common.py:1765
+#: ../roundup/backends/rdbms_common.py:1841
 msgid "retired"
 msgstr ""
 
-#: ../roundup/backends/rdbms_common.py:1795
+#: ../roundup/backends/rdbms_common.py:1871
 msgid "restored"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:58
-#, python-format
-msgid "You do not have permission to %(action)s the %(classname)s class."
-msgstr ""
-
-#: ../roundup/cgi/actions.py:89
+#: ../roundup/cgi/actions.py:88
 msgid "No type specified"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:91
+#: ../roundup/cgi/actions.py:90
 msgid "No ID entered"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:97
+#: ../roundup/cgi/actions.py:96
 #, python-format
 msgid "\"%(input)s\" is not an ID (%(classname)s ID required)"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:117
-msgid "You may not retire the admin or anonymous user"
+#: ../roundup/cgi/actions.py:108 ../roundup/cgi/actions.py:287
+#: ../roundup/cgi/actions.py:590 ../roundup/cgi/actions.py:636
+#: ../roundup/cgi/actions.py:822 ../roundup/cgi/actions.py:940
+#: ../roundup/cgi/actions.py:108:287 :590:636 :822:940
+msgid "Invalid request"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:124
+#: ../roundup/cgi/actions.py:126 ../roundup/cgi/actions.py:382
+#: ../roundup/cgi/actions.py:126:382
+#, python-format
+msgid "You do not have permission to retire %(class)s"
+msgstr ""
+
+#: ../roundup/cgi/actions.py:134
 #, python-format
 msgid "%(classname)s %(itemid)s has been retired"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:169 ../roundup/cgi/actions.py:197
-#: ../roundup/cgi/actions.py:169:197
+#: ../roundup/cgi/actions.py:175 ../roundup/cgi/actions.py:203
+#: ../roundup/cgi/actions.py:175:203
 msgid "You do not have permission to edit queries"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:175 ../roundup/cgi/actions.py:204
-#: ../roundup/cgi/actions.py:175:204
+#: ../roundup/cgi/actions.py:181 ../roundup/cgi/actions.py:210
+#: ../roundup/cgi/actions.py:181:210
 msgid "You do not have permission to store queries"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:310
+#: ../roundup/cgi/actions.py:321 ../roundup/cgi/actions.py:507
+#: ../roundup/cgi/actions.py:321:507
 #, python-format
-msgid "Not enough values on line %(line)s"
-msgstr ""
-
-#: ../roundup/cgi/actions.py:357
-msgid "Items edited OK"
+msgid "You do not have permission to create %(class)s"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:416
+#: ../roundup/cgi/actions.py:329
 #, python-format
-msgid "%(class)s %(id)s %(properties)s edited ok"
+msgid "Not enough values on line %(line)s"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:419
+#: ../roundup/cgi/actions.py:339 ../roundup/cgi/actions.py:495
+#: ../roundup/cgi/actions.py:339:495
 #, python-format
-msgid "%(class)s %(id)s - nothing changed"
+msgid "You do not have permission to edit %(class)s"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:431
-#, python-format
-msgid "%(class)s %(id)s created"
+#: ../roundup/cgi/actions.py:389
+msgid "Items edited OK"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:463
+#: ../roundup/cgi/actions.py:448
 #, python-format
-msgid "You do not have permission to edit %(class)s"
+msgid "%(class)s %(id)s %(properties)s edited ok"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:475
+#: ../roundup/cgi/actions.py:451
 #, python-format
-msgid "You do not have permission to create %(class)s"
+msgid "%(class)s %(id)s - nothing changed"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:499
-msgid "You do not have permission to edit user roles"
+#: ../roundup/cgi/actions.py:463
+#, python-format
+msgid "%(class)s %(id)s created"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:549
+#: ../roundup/cgi/actions.py:575
 #, 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:577
+#: ../roundup/cgi/actions.py:607
 #, python-format
 msgid "Edit Error: %s"
 msgstr ""
 
-#: ../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
+#: ../roundup/cgi/actions.py:642 ../roundup/cgi/actions.py:658
+#: ../roundup/cgi/actions.py:828 ../roundup/cgi/actions.py:847
+#: ../roundup/cgi/actions.py:642:658 :828:847
 #, python-format
 msgid "Error: %s"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:645
+#: ../roundup/cgi/actions.py:684
 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:687
+#: ../roundup/cgi/actions.py:726
 #, python-format
 msgid "Password reset and email sent to %s"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:696
+#: ../roundup/cgi/actions.py:735
 msgid "Unknown username"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:704
+#: ../roundup/cgi/actions.py:743
 msgid "Unknown email address"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:709
+#: ../roundup/cgi/actions.py:748
 msgid "You need to specify a username or address"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:734
+#: ../roundup/cgi/actions.py:773
 #, python-format
 msgid "Email sent to %s"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:753
+#: ../roundup/cgi/actions.py:787
 msgid "You are now registered, welcome!"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:798
+#: ../roundup/cgi/actions.py:836
 msgid "It is not permitted to supply roles at registration."
 msgstr ""
 
-#: ../roundup/cgi/actions.py:890
+#: ../roundup/cgi/actions.py:923
 msgid "You are logged out"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:907
+#: ../roundup/cgi/actions.py:944
 msgid "Username required"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:942 ../roundup/cgi/actions.py:946
-#: ../roundup/cgi/actions.py:942:946
+#: ../roundup/cgi/actions.py:978 ../roundup/cgi/actions.py:982
+#: ../roundup/cgi/actions.py:978:982
 msgid "Invalid login"
 msgstr ""
 
-#: ../roundup/cgi/actions.py:952
+#: ../roundup/cgi/actions.py:988
 msgid "You do not have permission to login"
 msgstr ""
 
+#: ../roundup/cgi/actions.py:1047
+#, python-format
+msgid "You do not have permission to view %(class)s"
+msgstr ""
+
 #: ../roundup/cgi/cgitb.py:49
 #, python-format
 msgid ""
@@ -1018,38 +1060,29 @@
 msgid "<em>undefined</em>"
 msgstr ""
 
-#: ../roundup/cgi/client.py:51
-msgid ""
-"<html><head><title>An error has occurred</title></head>\n"
-"<body><h1>An error has occurred</h1>\n"
-"<p>A problem was encountered processing your request.\n"
-"The tracker maintainers have been notified of the problem.</p>\n"
-"</body></html>"
-msgstr ""
-
-#: ../roundup/cgi/client.py:377
+#: ../roundup/cgi/client.py:517
 msgid "Form Error: "
 msgstr ""
 
-#: ../roundup/cgi/client.py:432
+#: ../roundup/cgi/client.py:575
 #, python-format
 msgid "Unrecognized charset: %r"
 msgstr ""
 
-#: ../roundup/cgi/client.py:560
+#: ../roundup/cgi/client.py:696
 msgid "Anonymous users are not allowed to use the web interface"
 msgstr ""
 
-#: ../roundup/cgi/client.py:715
+#: ../roundup/cgi/client.py:851
 msgid "You are not allowed to view this file."
 msgstr ""
 
-#: ../roundup/cgi/client.py:808
+#: ../roundup/cgi/client.py:968
 #, python-format
 msgid "%(starttag)sTime elapsed: %(seconds)fs%(endtag)s\n"
 msgstr ""
 
-#: ../roundup/cgi/client.py:812
+#: ../roundup/cgi/client.py:972
 #, python-format
 msgid ""
 "%(starttag)sCache hits: %(cache_hits)d, misses %(cache_misses)d. Loading "
@@ -1096,14 +1129,14 @@
 msgid "property \"%(propname)s\": \"%(value)s\" not currently in list"
 msgstr ""
 
-#: ../roundup/cgi/form_parser.py:551
+#: ../roundup/cgi/form_parser.py:557
 #, python-format
 msgid "Required %(class)s property %(property)s not supplied"
 msgid_plural "Required %(class)s properties %(property)s not supplied"
 msgstr[0] ""
 msgstr[1] ""
 
-#: ../roundup/cgi/form_parser.py:574
+#: ../roundup/cgi/form_parser.py:580
 msgid "File is empty"
 msgstr ""
 
@@ -1112,344 +1145,348 @@
 msgid "You are not allowed to %(action)s items of class %(class)s"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:657
+#: ../roundup/cgi/templating.py:664
 msgid "(list)"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:726
+#: ../roundup/cgi/templating.py:733
 msgid "Submit New Entry"
 msgstr ""
 
-#: ../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
+#: ../roundup/cgi/templating.py:747 ../roundup/cgi/templating.py:886
+#: ../roundup/cgi/templating.py:1358 ../roundup/cgi/templating.py:1387
+#: ../roundup/cgi/templating.py:1407 ../roundup/cgi/templating.py:1420
+#: ../roundup/cgi/templating.py:1471 ../roundup/cgi/templating.py:1494
+#: ../roundup/cgi/templating.py:1530 ../roundup/cgi/templating.py:1567
+#: ../roundup/cgi/templating.py:1620 ../roundup/cgi/templating.py:1637
+#: ../roundup/cgi/templating.py:1721 ../roundup/cgi/templating.py:1741
+#: ../roundup/cgi/templating.py:1759 ../roundup/cgi/templating.py:1791
+#: ../roundup/cgi/templating.py:1801 ../roundup/cgi/templating.py:1853
+#: ../roundup/cgi/templating.py:2069 ../roundup/cgi/templating.py:747:886
+#: :1358:1387 :1407:1420 :1471:1494 :1530:1567 :1620:1637 :1721:1741 :1759:1791
+#: :1801:1853 :2069
 msgid "[hidden]"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:741
+#: ../roundup/cgi/templating.py:748
 msgid "New node - no history"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:855
+#: ../roundup/cgi/templating.py:868
 msgid "Submit Changes"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:937
+#: ../roundup/cgi/templating.py:950
 msgid "<em>The indicated property no longer exists</em>"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:938
+#: ../roundup/cgi/templating.py:951
 #, python-format
 msgid "<em>%s: %s</em>\n"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:951
+#: ../roundup/cgi/templating.py:964
 #, python-format
 msgid "The linked class %(classname)s no longer exists"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:984 ../roundup/cgi/templating.py:1008
-#: ../roundup/cgi/templating.py:984:1008
+#: ../roundup/cgi/templating.py:998 ../roundup/cgi/templating.py:1023
+#: ../roundup/cgi/templating.py:998:1023
 msgid "<strike>The linked node no longer exists</strike>"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:1061
+#: ../roundup/cgi/templating.py:1077
 #, python-format
 msgid "%s: (no value)"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:1073
+#: ../roundup/cgi/templating.py:1089
 msgid ""
 "<strong><em>This event is not handled by the history display!</em></strong>"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:1085
+#: ../roundup/cgi/templating.py:1101
 msgid "<tr><td colspan=4><strong>Note:</strong></td></tr>"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:1094
+#: ../roundup/cgi/templating.py:1110
 msgid "History"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:1096
+#: ../roundup/cgi/templating.py:1112
 msgid "<th>Date</th>"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:1097
+#: ../roundup/cgi/templating.py:1113
 msgid "<th>User</th>"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:1098
+#: ../roundup/cgi/templating.py:1114
 msgid "<th>Action</th>"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:1099
+#: ../roundup/cgi/templating.py:1115
 msgid "<th>Args</th>"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:1141
+#: ../roundup/cgi/templating.py:1160
 #, python-format
 msgid "Copy of %(class)s %(id)s"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:1434
+#: ../roundup/cgi/templating.py:1498
 msgid "*encrypted*"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:1507 ../roundup/cgi/templating.py:1528
-#: ../roundup/cgi/templating.py:1534 ../roundup/cgi/templating.py:1050:1507
-#: :1528:1534
+#: ../roundup/cgi/templating.py:1571 ../roundup/cgi/templating.py:1592
+#: ../roundup/cgi/templating.py:1598 ../roundup/cgi/templating.py:1066:1571
+#: :1592:1598
 msgid "No"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:1507 ../roundup/cgi/templating.py:1526
-#: ../roundup/cgi/templating.py:1531 ../roundup/cgi/templating.py:1050:1507
-#: :1526:1531
+#: ../roundup/cgi/templating.py:1571 ../roundup/cgi/templating.py:1590
+#: ../roundup/cgi/templating.py:1595 ../roundup/cgi/templating.py:1066:1571
+#: :1590:1595
 msgid "Yes"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:1620
+#: ../roundup/cgi/templating.py:1684
 msgid ""
 "default value for DateHTMLProperty must be either DateHTMLProperty or string "
 "date representation."
 msgstr ""
 
-#: ../roundup/cgi/templating.py:1780
+#: ../roundup/cgi/templating.py:1844
 #, python-format
 msgid "Attempt to look up %(attr)s on a missing value"
 msgstr ""
 
-#: ../roundup/cgi/templating.py:1853
+#: ../roundup/cgi/templating.py:1929
 #, python-format
 msgid "<option %svalue=\"-1\">- no selection -</option>"
 msgstr ""
 
-#: ../roundup/date.py:300
+#: ../roundup/date.py:292
 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:359
+#: ../roundup/date.py:315
+msgid "Could not determine granularity"
+msgstr ""
+
+#: ../roundup/date.py:365
 #, 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:666
+#: ../roundup/date.py:677
 msgid ""
 "Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [date spec]"
 msgstr ""
 
-#: ../roundup/date.py:685
+#: ../roundup/date.py:699
 msgid "Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]"
 msgstr ""
 
-#: ../roundup/date.py:822
+#: ../roundup/date.py:836
 #, python-format
 msgid "%(number)s year"
 msgid_plural "%(number)s years"
 msgstr[0] ""
 msgstr[1] ""
 
-#: ../roundup/date.py:826
+#: ../roundup/date.py:840
 #, python-format
 msgid "%(number)s month"
 msgid_plural "%(number)s months"
 msgstr[0] ""
 msgstr[1] ""
 
-#: ../roundup/date.py:830
+#: ../roundup/date.py:844
 #, python-format
 msgid "%(number)s week"
 msgid_plural "%(number)s weeks"
 msgstr[0] ""
 msgstr[1] ""
 
-#: ../roundup/date.py:834
+#: ../roundup/date.py:848
 #, python-format
 msgid "%(number)s day"
 msgid_plural "%(number)s days"
 msgstr[0] ""
 msgstr[1] ""
 
-#: ../roundup/date.py:838
+#: ../roundup/date.py:852
 msgid "tomorrow"
 msgstr ""
 
-#: ../roundup/date.py:840
+#: ../roundup/date.py:854
 msgid "yesterday"
 msgstr ""
 
-#: ../roundup/date.py:843
+#: ../roundup/date.py:857
 #, python-format
 msgid "%(number)s hour"
 msgid_plural "%(number)s hours"
 msgstr[0] ""
 msgstr[1] ""
 
-#: ../roundup/date.py:847
+#: ../roundup/date.py:861
 msgid "an hour"
 msgstr ""
 
-#: ../roundup/date.py:849
+#: ../roundup/date.py:863
 msgid "1 1/2 hours"
 msgstr ""
 
-#: ../roundup/date.py:851
+#: ../roundup/date.py:865
 #, python-format
 msgid "1 %(number)s/4 hours"
 msgid_plural "1 %(number)s/4 hours"
 msgstr[0] ""
 msgstr[1] ""
 
-#: ../roundup/date.py:855
+#: ../roundup/date.py:869
 msgid "in a moment"
 msgstr ""
 
-#: ../roundup/date.py:857
+#: ../roundup/date.py:871
 msgid "just now"
 msgstr ""
 
-#: ../roundup/date.py:860
+#: ../roundup/date.py:874
 msgid "1 minute"
 msgstr ""
 
-#: ../roundup/date.py:863
+#: ../roundup/date.py:877
 #, python-format
 msgid "%(number)s minute"
 msgid_plural "%(number)s minutes"
 msgstr[0] ""
 msgstr[1] ""
 
-#: ../roundup/date.py:866
+#: ../roundup/date.py:880
 msgid "1/2 an hour"
 msgstr ""
 
-#: ../roundup/date.py:868
+#: ../roundup/date.py:882
 #, python-format
 msgid "%(number)s/4 hour"
 msgid_plural "%(number)s/4 hours"
 msgstr[0] ""
 msgstr[1] ""
 
-#: ../roundup/date.py:872
+#: ../roundup/date.py:886
 #, python-format
 msgid "%s ago"
 msgstr ""
 
-#: ../roundup/date.py:874
+#: ../roundup/date.py:888
 #, python-format
 msgid "in %s"
 msgstr ""
 
-#: ../roundup/hyperdb.py:87
+#: ../roundup/hyperdb.py:91
 #, python-format
 msgid "property %s: %s"
 msgstr ""
 
-#: ../roundup/hyperdb.py:107
+#: ../roundup/hyperdb.py:111
 #, python-format
 msgid "property %s: %r is an invalid date (%s)"
 msgstr ""
 
-#: ../roundup/hyperdb.py:124
+#: ../roundup/hyperdb.py:128
 #, python-format
 msgid "property %s: %r is an invalid date interval (%s)"
 msgstr ""
 
-#: ../roundup/hyperdb.py:219
+#: ../roundup/hyperdb.py:223
 #, python-format
 msgid "property %s: %r is not currently an element"
 msgstr ""
 
-#: ../roundup/hyperdb.py:263
+#: ../roundup/hyperdb.py:267
 #, python-format
 msgid "property %s: %r is not a number"
 msgstr ""
 
-#: ../roundup/hyperdb.py:276
+#: ../roundup/hyperdb.py:280
 #, python-format
 msgid "\"%s\" not a node designator"
 msgstr ""
 
-#: ../roundup/hyperdb.py:949 ../roundup/hyperdb.py:957
-#: ../roundup/hyperdb.py:949:957
+#: ../roundup/hyperdb.py:953 ../roundup/hyperdb.py:961
+#: ../roundup/hyperdb.py:953:961
 #, python-format
 msgid "Not a property name: %s"
 msgstr ""
 
-#: ../roundup/hyperdb.py:1240
+#: ../roundup/hyperdb.py:1244
 #, python-format
 msgid "property %s: %r is not a %s."
 msgstr ""
 
-#: ../roundup/hyperdb.py:1243
+#: ../roundup/hyperdb.py:1247
 #, python-format
 msgid "you may only enter ID values for property %s"
 msgstr ""
 
-#: ../roundup/hyperdb.py:1273
+#: ../roundup/hyperdb.py:1277
 #, python-format
 msgid "%r is not a property of %s"
 msgstr ""
 
-#: ../roundup/init.py:134
+#: ../roundup/init.py:136
 #, python-format
 msgid ""
 "WARNING: directory '%s'\n"
 "\tcontains old-style template - ignored"
 msgstr ""
 
-#: ../roundup/mailgw.py:199 ../roundup/mailgw.py:211
-#: ../roundup/mailgw.py:199:211
+#: ../roundup/mailgw.py:201 ../roundup/mailgw.py:213
+#: ../roundup/mailgw.py:201:213
 #, python-format
 msgid "Message signed with unknown key: %s"
 msgstr ""
 
-#: ../roundup/mailgw.py:202
+#: ../roundup/mailgw.py:204
 #, python-format
 msgid "Message signed with an expired key: %s"
 msgstr ""
 
-#: ../roundup/mailgw.py:205
+#: ../roundup/mailgw.py:207
 #, python-format
 msgid "Message signed with a revoked key: %s"
 msgstr ""
 
-#: ../roundup/mailgw.py:208
+#: ../roundup/mailgw.py:210
 msgid "Invalid PGP signature detected."
 msgstr ""
 
-#: ../roundup/mailgw.py:404
+#: ../roundup/mailgw.py:464
 msgid "Unknown multipart/encrypted version."
 msgstr ""
 
-#: ../roundup/mailgw.py:413
+#: ../roundup/mailgw.py:473
 msgid "Unable to decrypt your message."
 msgstr ""
 
-#: ../roundup/mailgw.py:442
+#: ../roundup/mailgw.py:502
 msgid "No PGP signature found in message."
 msgstr ""
 
-#: ../roundup/mailgw.py:749
+#: ../roundup/mailgw.py:812
 msgid ""
 "\n"
 "Emails to Roundup trackers must include a Subject: line!\n"
 msgstr ""
 
-#: ../roundup/mailgw.py:873
+#: ../roundup/mailgw.py:936
 #, python-format
 msgid ""
 "\n"
@@ -1466,7 +1503,7 @@
 "Subject was: '%(subject)s'\n"
 msgstr ""
 
-#: ../roundup/mailgw.py:911
+#: ../roundup/mailgw.py:974
 #, python-format
 msgid ""
 "\n"
@@ -1477,7 +1514,7 @@
 "Subject was: \"%(subject)s\"\n"
 msgstr ""
 
-#: ../roundup/mailgw.py:919
+#: ../roundup/mailgw.py:982
 #, python-format
 msgid ""
 "\n"
@@ -1494,7 +1531,7 @@
 "Subject was: '%(subject)s'\n"
 msgstr ""
 
-#: ../roundup/mailgw.py:960
+#: ../roundup/mailgw.py:1023
 #, python-format
 msgid ""
 "\n"
@@ -1505,7 +1542,7 @@
 "Subject was: \"%(subject)s\"\n"
 msgstr ""
 
-#: ../roundup/mailgw.py:993
+#: ../roundup/mailgw.py:1056
 #, python-format
 msgid ""
 "\n"
@@ -1515,7 +1552,7 @@
 "Subject was: \"%(subject)s\"\n"
 msgstr ""
 
-#: ../roundup/mailgw.py:1021
+#: ../roundup/mailgw.py:1084
 #, python-format
 msgid ""
 "\n"
@@ -1524,7 +1561,7 @@
 "  %(current_class)s\n"
 msgstr ""
 
-#: ../roundup/mailgw.py:1044
+#: ../roundup/mailgw.py:1107
 #, python-format
 msgid ""
 "\n"
@@ -1533,7 +1570,7 @@
 "  %(errors)s\n"
 msgstr ""
 
-#: ../roundup/mailgw.py:1084
+#: ../roundup/mailgw.py:1147
 #, python-format
 msgid ""
 "\n"
@@ -1542,21 +1579,21 @@
 "Unknown address: %(from_address)s\n"
 msgstr ""
 
-#: ../roundup/mailgw.py:1092
+#: ../roundup/mailgw.py:1155
 msgid "You are not permitted to access this tracker."
 msgstr ""
 
-#: ../roundup/mailgw.py:1099
+#: ../roundup/mailgw.py:1162
 #, python-format
 msgid "You are not permitted to edit %(classname)s."
 msgstr ""
 
-#: ../roundup/mailgw.py:1103
+#: ../roundup/mailgw.py:1166
 #, python-format
 msgid "You are not permitted to create %(classname)s."
 msgstr ""
 
-#: ../roundup/mailgw.py:1150
+#: ../roundup/mailgw.py:1213
 #, python-format
 msgid ""
 "\n"
@@ -1566,34 +1603,34 @@
 "Subject was: \"%(subject)s\"\n"
 msgstr ""
 
-#: ../roundup/mailgw.py:1203
+#: ../roundup/mailgw.py:1266
 msgid ""
 "\n"
 "This tracker has been configured to require all email be PGP signed or\n"
 "encrypted."
 msgstr ""
 
-#: ../roundup/mailgw.py:1209
+#: ../roundup/mailgw.py:1273
 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:1226
+#: ../roundup/mailgw.py:1290
 msgid "You are not permitted to create files."
 msgstr ""
 
-#: ../roundup/mailgw.py:1240
+#: ../roundup/mailgw.py:1304
 #, python-format
 msgid "You are not permitted to add files to %(classname)s."
 msgstr ""
 
-#: ../roundup/mailgw.py:1258
+#: ../roundup/mailgw.py:1322
 msgid "You are not permitted to create messages."
 msgstr ""
 
-#: ../roundup/mailgw.py:1266
+#: ../roundup/mailgw.py:1330
 #, python-format
 msgid ""
 "\n"
@@ -1601,17 +1638,17 @@
 "%(error)s\n"
 msgstr ""
 
-#: ../roundup/mailgw.py:1274
+#: ../roundup/mailgw.py:1338
 #, python-format
 msgid "You are not permitted to add messages to %(classname)s."
 msgstr ""
 
-#: ../roundup/mailgw.py:1301
+#: ../roundup/mailgw.py:1365
 #, python-format
 msgid "You are not permitted to edit property %(prop)s of class %(classname)s."
 msgstr ""
 
-#: ../roundup/mailgw.py:1309
+#: ../roundup/mailgw.py:1374
 #, python-format
 msgid ""
 "\n"
@@ -1619,85 +1656,85 @@
 "   %(message)s\n"
 msgstr ""
 
-#: ../roundup/mailgw.py:1331
+#: ../roundup/mailgw.py:1396
 msgid "not of form [arg=value,value,...;arg=value,value,...]"
 msgstr ""
 
-#: ../roundup/roundupdb.py:147
+#: ../roundup/roundupdb.py:174
 msgid "files"
 msgstr ""
 
-#: ../roundup/roundupdb.py:147
+#: ../roundup/roundupdb.py:174
 msgid "messages"
 msgstr ""
 
-#: ../roundup/roundupdb.py:147
+#: ../roundup/roundupdb.py:174
 msgid "nosy"
 msgstr ""
 
-#: ../roundup/roundupdb.py:147
+#: ../roundup/roundupdb.py:174
 msgid "superseder"
 msgstr ""
 
-#: ../roundup/roundupdb.py:147
+#: ../roundup/roundupdb.py:174
 msgid "title"
 msgstr ""
 
-#: ../roundup/roundupdb.py:148
+#: ../roundup/roundupdb.py:175
 msgid "assignedto"
 msgstr ""
 
-#: ../roundup/roundupdb.py:148
+#: ../roundup/roundupdb.py:175
 msgid "keyword"
 msgstr ""
 
-#: ../roundup/roundupdb.py:148
+#: ../roundup/roundupdb.py:175
 msgid "priority"
 msgstr ""
 
-#: ../roundup/roundupdb.py:148
+#: ../roundup/roundupdb.py:175
 msgid "status"
 msgstr ""
 
-#: ../roundup/roundupdb.py:151
+#: ../roundup/roundupdb.py:178
 msgid "activity"
 msgstr ""
 
 #. following properties are common for all hyperdb classes
 #. they are listed here to keep things in one place
-#: ../roundup/roundupdb.py:151
+#: ../roundup/roundupdb.py:178
 msgid "actor"
 msgstr ""
 
-#: ../roundup/roundupdb.py:151
+#: ../roundup/roundupdb.py:178
 msgid "creation"
 msgstr ""
 
-#: ../roundup/roundupdb.py:151
+#: ../roundup/roundupdb.py:178
 msgid "creator"
 msgstr ""
 
-#: ../roundup/roundupdb.py:309
+#: ../roundup/roundupdb.py:335
 #, python-format
 msgid "New submission from %(authname)s%(authaddr)s:"
 msgstr ""
 
-#: ../roundup/roundupdb.py:312
+#: ../roundup/roundupdb.py:338
 #, python-format
 msgid "%(authname)s%(authaddr)s added the comment:"
 msgstr ""
 
-#: ../roundup/roundupdb.py:315
+#: ../roundup/roundupdb.py:341
 #, python-format
 msgid "Change by %(authname)s%(authaddr)s:"
 msgstr ""
 
-#: ../roundup/roundupdb.py:342
+#: ../roundup/roundupdb.py:361
 #, python-format
 msgid "File '%(filename)s' not attached - you can download it from %(link)s."
 msgstr ""
 
-#: ../roundup/roundupdb.py:615
+#: ../roundup/roundupdb.py:661
 #, python-format
 msgid ""
 "\n"
@@ -1760,16 +1797,22 @@
 " specified as:\n"
 "   mailbox /path/to/mailbox\n"
 "\n"
+"In all of the following the username and password can be stored in a\n"
+"~/.netrc file. In this case only the server name need be specified on\n"
+"the command-line.\n"
+"\n"
+"The username and/or password will be prompted for if not supplied on\n"
+"the command-line or in ~/.netrc.\n"
+"\n"
 "POP:\n"
 " In the third case, the gateway reads all messages from the POP server\n"
 " specified and submits each in turn to the roundup.mailgw module. The\n"
 " server is specified as:\n"
 "    pop username:password at server\n"
-" The username and password may be omitted:\n"
+" Alternatively, one can omit one or both of username and password:\n"
 "    pop username at server\n"
 "    pop server\n"
-" are both valid. The username and/or password will be prompted for if\n"
-" not supplied on the command-line.\n"
+" are both valid.\n"
 "\n"
 "POPS:\n"
 " Connect to a POP server over ssl. This requires python 2.4 or later.\n"
@@ -1794,26 +1837,23 @@
 "\n"
 msgstr ""
 
-#: ../roundup/scripts/roundup_mailgw.py:151
+#: ../roundup/scripts/roundup_mailgw.py:157
 msgid "Error: not enough source specification information"
 msgstr ""
 
-#: ../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"
+#: ../roundup/scripts/roundup_mailgw.py:186
+#, python-format
+msgid "Error: %s specification not valid"
 msgstr ""
 
-#: ../roundup/scripts/roundup_mailgw.py:177
-msgid "Error: apop specification not valid"
+#: ../roundup/scripts/roundup_mailgw.py:192
+msgid "Error: a later version of python is required"
 msgstr ""
 
-#: ../roundup/scripts/roundup_mailgw.py:189
+#: ../roundup/scripts/roundup_mailgw.py:203
 msgid ""
-"Error: The source must be either \"mailbox\", \"pop\", \"apop\", \"imap\" or "
-"\"imaps\""
+"Error: The source must be either \"mailbox\", \"pop\", \"pops\", \"apop\", "
+"\"imap\" or \"imaps\""
 msgstr ""
 
 #: ../roundup/scripts/roundup_server.py:76
@@ -1966,18 +2006,18 @@
 msgid "Roundup server started on %(HOST)s:%(PORT)s"
 msgstr ""
 
-#: ../templates/classic/html/_generic.collision.html:4
-#: ../templates/minimal/html/_generic.collision.html:4
+#: ../share/roundup/templates/classic/html/_generic.collision.html:4
+#: ../share/roundup/templates/minimal/html/_generic.collision.html:4
 msgid "${class} Edit Collision - ${tracker}"
 msgstr ""
 
-#: ../templates/classic/html/_generic.collision.html:7
-#: ../templates/minimal/html/_generic.collision.html:7
+#: ../share/roundup/templates/classic/html/_generic.collision.html:7
+#: ../share/roundup/templates/minimal/html/_generic.collision.html:7
 msgid "${class} Edit Collision"
 msgstr ""
 
-#: ../templates/classic/html/_generic.collision.html:14
-#: ../templates/minimal/html/_generic.collision.html:14
+#: ../share/roundup/templates/classic/html/_generic.collision.html:14
+#: ../share/roundup/templates/minimal/html/_generic.collision.html:14
 msgid ""
 "\n"
 "  There has been a collision. Another user updated this node\n"
@@ -1985,108 +2025,108 @@
 "  the node and review your edits.\n"
 msgstr ""
 
-#: ../templates/classic/html/_generic.help-empty.html:6
+#: ../share/roundup/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
+#: ../share/roundup/templates/classic/html/_generic.help-list.html:20
+#: ../share/roundup/templates/classic/html/_generic.index.html:14
+#: ../share/roundup/templates/classic/html/_generic.item.html:12
+#: ../share/roundup/templates/classic/html/file.item.html:9
+#: ../share/roundup/templates/classic/html/issue.index.html:16
+#: ../share/roundup/templates/classic/html/issue.item.html:28
+#: ../share/roundup/templates/classic/html/msg.item.html:26
+#: ../share/roundup/templates/classic/html/user.index.html:9
+#: ../share/roundup/templates/classic/html/user.item.html:35
+#: ../share/roundup/templates/minimal/html/_generic.index.html:14
+#: ../share/roundup/templates/minimal/html/_generic.item.html:12
+#: ../share/roundup/templates/minimal/html/user.index.html:9
+#: ../share/roundup/templates/minimal/html/user.item.html:35
+#: ../share/roundup/templates/minimal/html/user.register.html:14
 msgid "You are not allowed to view this page."
 msgstr ""
 
-#: ../templates/classic/html/_generic.help-list.html:34
+#: ../share/roundup/templates/classic/html/_generic.help-list.html:34
 msgid "1..25 out of 50"
 msgstr ""
 
-#: ../templates/classic/html/_generic.help-search.html:9
+#: ../share/roundup/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
+#: ../share/roundup/templates/classic/html/_generic.help-submit.html:57
+#: ../share/roundup/templates/classic/html/_generic.help.html:31
+#: ../share/roundup/templates/minimal/html/_generic.help.html:31
 msgid " Cancel "
 msgstr ""
 
-#: ../templates/classic/html/_generic.help-submit.html:63
-#: ../templates/classic/html/_generic.help.html:34
-#: ../templates/minimal/html/_generic.help.html:34
+#: ../share/roundup/templates/classic/html/_generic.help-submit.html:63
+#: ../share/roundup/templates/classic/html/_generic.help.html:34
+#: ../share/roundup/templates/minimal/html/_generic.help.html:34
 msgid " Apply "
 msgstr ""
 
-#: ../templates/classic/html/_generic.help.html:9
-#: ../templates/classic/html/user.help.html:13
-#: ../templates/minimal/html/_generic.help.html:9
+#: ../share/roundup/templates/classic/html/_generic.help.html:9
+#: ../share/roundup/templates/classic/html/user.help.html:13
+#: ../share/roundup/templates/minimal/html/_generic.help.html:9
 msgid "${property} help - ${tracker}"
 msgstr ""
 
-#: ../templates/classic/html/_generic.help.html:41
-#: ../templates/classic/html/help.html:21
-#: ../templates/classic/html/issue.index.html:81
-#: ../templates/minimal/html/_generic.help.html:41
+#: ../share/roundup/templates/classic/html/_generic.help.html:41
+#: ../share/roundup/templates/classic/html/help.html:21
+#: ../share/roundup/templates/classic/html/issue.index.html:81
+#: ../share/roundup/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:89
-#: ../templates/minimal/html/_generic.help.html:53
+#: ../share/roundup/templates/classic/html/_generic.help.html:53
+#: ../share/roundup/templates/classic/html/help.html:28
+#: ../share/roundup/templates/classic/html/issue.index.html:89
+#: ../share/roundup/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:92
-#: ../templates/minimal/html/_generic.help.html:57
+#: ../share/roundup/templates/classic/html/_generic.help.html:57
+#: ../share/roundup/templates/classic/html/help.html:32
+#: ../share/roundup/templates/classic/html/issue.index.html:92
+#: ../share/roundup/templates/minimal/html/_generic.help.html:57
 msgid "next &gt;&gt;"
 msgstr ""
 
-#: ../templates/classic/html/_generic.index.html:6
-#: ../templates/classic/html/_generic.item.html:4
-#: ../templates/minimal/html/_generic.index.html:6
-#: ../templates/minimal/html/_generic.item.html:4
+#: ../share/roundup/templates/classic/html/_generic.index.html:6
+#: ../share/roundup/templates/classic/html/_generic.item.html:4
+#: ../share/roundup/templates/minimal/html/_generic.index.html:6
+#: ../share/roundup/templates/minimal/html/_generic.item.html:4
 msgid "${class} editing - ${tracker}"
 msgstr ""
 
-#: ../templates/classic/html/_generic.index.html:9
-#: ../templates/classic/html/_generic.item.html:7
-#: ../templates/minimal/html/_generic.index.html:9
-#: ../templates/minimal/html/_generic.item.html:7
+#: ../share/roundup/templates/classic/html/_generic.index.html:9
+#: ../share/roundup/templates/classic/html/_generic.item.html:7
+#: ../share/roundup/templates/minimal/html/_generic.index.html:9
+#: ../share/roundup/templates/minimal/html/_generic.item.html:7
 msgid "${class} editing"
 msgstr ""
 
-#: ../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
+#: ../share/roundup/templates/classic/html/_generic.index.html:19
+#: ../share/roundup/templates/classic/html/_generic.item.html:16
+#: ../share/roundup/templates/classic/html/file.item.html:13
+#: ../share/roundup/templates/classic/html/issue.index.html:20
+#: ../share/roundup/templates/classic/html/issue.item.html:32
+#: ../share/roundup/templates/classic/html/msg.item.html:30
+#: ../share/roundup/templates/classic/html/user.index.html:13
+#: ../share/roundup/templates/classic/html/user.item.html:39
+#: ../share/roundup/templates/minimal/html/_generic.index.html:19
+#: ../share/roundup/templates/minimal/html/_generic.item.html:17
+#: ../share/roundup/templates/minimal/html/user.index.html:13
+#: ../share/roundup/templates/minimal/html/user.item.html:39
+#: ../share/roundup/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
+#: ../share/roundup/templates/classic/html/_generic.index.html:28
+#: ../share/roundup/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 "
@@ -2098,923 +2138,922 @@
 "appending them to the table - put an X in the id column. </p>"
 msgstr ""
 
-#: ../templates/classic/html/_generic.index.html:50
-#: ../templates/minimal/html/_generic.index.html:50
+#: ../share/roundup/templates/classic/html/_generic.index.html:50
+#: ../share/roundup/templates/minimal/html/_generic.index.html:50
 msgid "Edit Items"
 msgstr ""
 
-#: ../templates/classic/html/file.index.html:4
+#: ../share/roundup/templates/classic/html/file.index.html:4
 msgid "List of files - ${tracker}"
 msgstr ""
 
-#: ../templates/classic/html/file.index.html:5
+#: ../share/roundup/templates/classic/html/file.index.html:5
 msgid "List of files"
 msgstr ""
 
-#: ../templates/classic/html/file.index.html:10
+#: ../share/roundup/templates/classic/html/file.index.html:10
 msgid "Download"
 msgstr ""
 
-#: ../templates/classic/html/file.index.html:11
-#: ../templates/classic/html/file.item.html:27
+#: ../share/roundup/templates/classic/html/file.index.html:11
+#: ../share/roundup/templates/classic/html/file.item.html:27
 msgid "Content Type"
 msgstr ""
 
-#: ../templates/classic/html/file.index.html:12
+#: ../share/roundup/templates/classic/html/file.index.html:12
 msgid "Uploaded By"
 msgstr ""
 
-#: ../templates/classic/html/file.index.html:13
-#: ../templates/classic/html/msg.item.html:48
+#: ../share/roundup/templates/classic/html/file.index.html:13
+#: ../share/roundup/templates/classic/html/msg.item.html:48
 msgid "Date"
 msgstr ""
 
-#: ../templates/classic/html/file.item.html:2
+#: ../share/roundup/templates/classic/html/file.item.html:2
 msgid "File display - ${tracker}"
 msgstr ""
 
-#: ../templates/classic/html/file.item.html:4
+#: ../share/roundup/templates/classic/html/file.item.html:4
 msgid "File display"
 msgstr ""
 
-#: ../templates/classic/html/file.item.html:23
-#: ../templates/classic/html/user.register.html:17
+#: ../share/roundup/templates/classic/html/file.item.html:23
+#: ../share/roundup/templates/classic/html/user.register.html:17
 msgid "Name"
 msgstr ""
 
-#: ../templates/classic/html/file.item.html:45
+#: ../share/roundup/templates/classic/html/file.item.html:45
 msgid "download"
 msgstr ""
 
-#: ../templates/classic/html/home.classlist.html:2
-#: ../templates/minimal/html/home.classlist.html:2
+#: ../share/roundup/templates/classic/html/home.classlist.html:2
+#: ../share/roundup/templates/minimal/html/home.classlist.html:2
 msgid "List of classes - ${tracker}"
 msgstr ""
 
-#: ../templates/classic/html/home.classlist.html:4
-#: ../templates/minimal/html/home.classlist.html:4
+#: ../share/roundup/templates/classic/html/home.classlist.html:4
+#: ../share/roundup/templates/minimal/html/home.classlist.html:4
 msgid "List of classes"
 msgstr ""
 
-#: ../templates/classic/html/issue.index.html:4
-#: ../templates/classic/html/issue.index.html:10
+#: ../share/roundup/templates/classic/html/issue.index.html:4
+#: ../share/roundup/templates/classic/html/issue.index.html:10
 msgid "List of issues"
 msgstr ""
 
-#: ../templates/classic/html/issue.index.html:27
-#: ../templates/classic/html/issue.item.html:49
+#: ../share/roundup/templates/classic/html/issue.index.html:27
+#: ../share/roundup/templates/classic/html/issue.item.html:49
 msgid "Priority"
 msgstr ""
 
-#: ../templates/classic/html/issue.index.html:28
+#: ../share/roundup/templates/classic/html/issue.index.html:28
 msgid "ID"
 msgstr ""
 
-#: ../templates/classic/html/issue.index.html:29
+#: ../share/roundup/templates/classic/html/issue.index.html:29
 msgid "Creation"
 msgstr ""
 
-#: ../templates/classic/html/issue.index.html:30
+#: ../share/roundup/templates/classic/html/issue.index.html:30
 msgid "Activity"
 msgstr ""
 
-#: ../templates/classic/html/issue.index.html:31
+#: ../share/roundup/templates/classic/html/issue.index.html:31
 msgid "Actor"
 msgstr ""
 
-#: ../templates/classic/html/issue.index.html:32
-#: ../templates/classic/html/keyword.item.html:37
+#: ../share/roundup/templates/classic/html/issue.index.html:32
+#: ../share/roundup/templates/classic/html/keyword.item.html:37
 msgid "Keyword"
 msgstr ""
 
-#: ../templates/classic/html/issue.index.html:33
-#: ../templates/classic/html/issue.item.html:44
+#: ../share/roundup/templates/classic/html/issue.index.html:33
+#: ../share/roundup/templates/classic/html/issue.item.html:44
 msgid "Title"
 msgstr ""
 
-#: ../templates/classic/html/issue.index.html:34
-#: ../templates/classic/html/issue.item.html:51
+#: ../share/roundup/templates/classic/html/issue.index.html:34
+#: ../share/roundup/templates/classic/html/issue.item.html:51
 msgid "Status"
 msgstr ""
 
-#: ../templates/classic/html/issue.index.html:35
+#: ../share/roundup/templates/classic/html/issue.index.html:35
 msgid "Creator"
 msgstr ""
 
-#: ../templates/classic/html/issue.index.html:36
+#: ../share/roundup/templates/classic/html/issue.index.html:36
 msgid "Assigned&nbsp;To"
 msgstr ""
 
-#: ../templates/classic/html/issue.index.html:105
+#: ../share/roundup/templates/classic/html/issue.index.html:105
 msgid "Download as CSV"
 msgstr ""
 
-#: ../templates/classic/html/issue.index.html:115
+#: ../share/roundup/templates/classic/html/issue.index.html:115
 msgid "Sort on:"
 msgstr ""
 
-#: ../templates/classic/html/issue.index.html:119
-#: ../templates/classic/html/issue.index.html:140
+#: ../share/roundup/templates/classic/html/issue.index.html:119
+#: ../share/roundup/templates/classic/html/issue.index.html:140
 msgid "- nothing -"
 msgstr ""
 
-#: ../templates/classic/html/issue.index.html:127
-#: ../templates/classic/html/issue.index.html:148
+#: ../share/roundup/templates/classic/html/issue.index.html:127
+#: ../share/roundup/templates/classic/html/issue.index.html:148
 msgid "Descending:"
 msgstr ""
 
-#: ../templates/classic/html/issue.index.html:136
+#: ../share/roundup/templates/classic/html/issue.index.html:136
 msgid "Group on:"
 msgstr ""
 
-#: ../templates/classic/html/issue.index.html:155
+#: ../share/roundup/templates/classic/html/issue.index.html:155
 msgid "Redisplay"
 msgstr ""
 
-#: ../templates/classic/html/issue.item.html:7
+#: ../share/roundup/templates/classic/html/issue.item.html:7
 msgid "Issue ${id}: ${title} - ${tracker}"
 msgstr ""
 
-#: ../templates/classic/html/issue.item.html:10
+#: ../share/roundup/templates/classic/html/issue.item.html:10
 msgid "New Issue - ${tracker}"
 msgstr ""
 
-#: ../templates/classic/html/issue.item.html:14
+#: ../share/roundup/templates/classic/html/issue.item.html:14
 msgid "New Issue"
 msgstr ""
 
-#: ../templates/classic/html/issue.item.html:16
+#: ../share/roundup/templates/classic/html/issue.item.html:16
 msgid "New Issue Editing"
 msgstr ""
 
-#: ../templates/classic/html/issue.item.html:19
+#: ../share/roundup/templates/classic/html/issue.item.html:19
 msgid "Issue${id}"
 msgstr ""
 
-#: ../templates/classic/html/issue.item.html:22
+#: ../share/roundup/templates/classic/html/issue.item.html:22
 msgid "Issue${id} Editing"
 msgstr ""
 
-#: ../templates/classic/html/issue.item.html:56
+#: ../share/roundup/templates/classic/html/issue.item.html:56
 msgid "Superseder"
 msgstr ""
 
-#: ../templates/classic/html/issue.item.html:61
+#: ../share/roundup/templates/classic/html/issue.item.html:61
 msgid "View:"
 msgstr ""
 
-#: ../templates/classic/html/issue.item.html:67
+#: ../share/roundup/templates/classic/html/issue.item.html:67
 msgid "Nosy List"
 msgstr ""
 
-#: ../templates/classic/html/issue.item.html:76
+#: ../share/roundup/templates/classic/html/issue.item.html:76
 msgid "Assigned To"
 msgstr ""
 
-#: ../templates/classic/html/issue.item.html:78
-#: ../templates/classic/html/page.html:103
-#: ../templates/minimal/html/page.html:102
+#: ../share/roundup/templates/classic/html/issue.item.html:78
+#: ../share/roundup/templates/classic/html/page.html:103
+#: ../share/roundup/templates/minimal/html/page.html:102
 msgid "Keywords"
 msgstr ""
 
-#: ../templates/classic/html/issue.item.html:86
+#: ../share/roundup/templates/classic/html/issue.item.html:86
 msgid "Change Note"
 msgstr ""
 
-#: ../templates/classic/html/issue.item.html:94
+#: ../share/roundup/templates/classic/html/issue.item.html:94
 msgid "File"
 msgstr ""
 
-#: ../templates/classic/html/issue.item.html:106
+#: ../share/roundup/templates/classic/html/issue.item.html:106
 msgid "Make a copy"
 msgstr ""
 
-#: ../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:153
+#: ../share/roundup/templates/classic/html/issue.item.html:114
+#: ../share/roundup/templates/classic/html/user.item.html:153
+#: ../share/roundup/templates/classic/html/user.register.html:69
+#: ../share/roundup/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 ""
 
-#: ../templates/classic/html/issue.item.html:128
+#: ../share/roundup/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>."
+"Created on ${creation} by ${creator}, last changed ${activity} by ${actor}."
 msgstr ""
 
-#: ../templates/classic/html/issue.item.html:132
-#: ../templates/classic/html/msg.item.html:61
+#: ../share/roundup/templates/classic/html/issue.item.html:132
+#: ../share/roundup/templates/classic/html/msg.item.html:61
 msgid "Files"
 msgstr ""
 
-#: ../templates/classic/html/issue.item.html:134
-#: ../templates/classic/html/msg.item.html:63
+#: ../share/roundup/templates/classic/html/issue.item.html:134
+#: ../share/roundup/templates/classic/html/msg.item.html:63
 msgid "File name"
 msgstr ""
 
-#: ../templates/classic/html/issue.item.html:135
-#: ../templates/classic/html/msg.item.html:64
+#: ../share/roundup/templates/classic/html/issue.item.html:135
+#: ../share/roundup/templates/classic/html/msg.item.html:64
 msgid "Uploaded"
 msgstr ""
 
-#: ../templates/classic/html/issue.item.html:136
+#: ../share/roundup/templates/classic/html/issue.item.html:136
 msgid "Type"
 msgstr ""
 
-#: ../templates/classic/html/issue.item.html:137
-#: ../templates/classic/html/query.edit.html:30
+#: ../share/roundup/templates/classic/html/issue.item.html:137
+#: ../share/roundup/templates/classic/html/query.edit.html:30
 msgid "Edit"
 msgstr ""
 
-#: ../templates/classic/html/issue.item.html:138
+#: ../share/roundup/templates/classic/html/issue.item.html:138
 msgid "Remove"
 msgstr ""
 
-#: ../templates/classic/html/issue.item.html:158
-#: ../templates/classic/html/issue.item.html:179
-#: ../templates/classic/html/query.edit.html:50
+#: ../share/roundup/templates/classic/html/issue.item.html:158
+#: ../share/roundup/templates/classic/html/issue.item.html:179
+#: ../share/roundup/templates/classic/html/query.edit.html:50
 msgid "remove"
 msgstr ""
 
-#: ../templates/classic/html/issue.item.html:165
-#: ../templates/classic/html/msg.index.html:9
+#: ../share/roundup/templates/classic/html/issue.item.html:165
+#: ../share/roundup/templates/classic/html/msg.index.html:9
 msgid "Messages"
 msgstr ""
 
-#: ../templates/classic/html/issue.item.html:169
+#: ../share/roundup/templates/classic/html/issue.item.html:169
 msgid "msg${id} (view)"
 msgstr ""
 
-#: ../templates/classic/html/issue.item.html:170
+#: ../share/roundup/templates/classic/html/issue.item.html:170
 msgid "Author: ${author}"
 msgstr ""
 
-#: ../templates/classic/html/issue.item.html:172
+#: ../share/roundup/templates/classic/html/issue.item.html:172
 msgid "Date: ${date}"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:2
+#: ../share/roundup/templates/classic/html/issue.search.html:2
 msgid "Issue searching - ${tracker}"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:4
+#: ../share/roundup/templates/classic/html/issue.search.html:4
 msgid "Issue searching"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:31
+#: ../share/roundup/templates/classic/html/issue.search.html:31
 msgid "Filter on"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:32
+#: ../share/roundup/templates/classic/html/issue.search.html:32
 msgid "Display"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:33
+#: ../share/roundup/templates/classic/html/issue.search.html:33
 msgid "Sort on"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:34
+#: ../share/roundup/templates/classic/html/issue.search.html:34
 msgid "Group on"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:38
+#: ../share/roundup/templates/classic/html/issue.search.html:38
 msgid "All text*:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:46
+#: ../share/roundup/templates/classic/html/issue.search.html:46
 msgid "Title:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:56
+#: ../share/roundup/templates/classic/html/issue.search.html:56
 msgid "Keyword:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:58
-#: ../templates/classic/html/issue.search.html:123
-#: ../templates/classic/html/issue.search.html:139
+#: ../share/roundup/templates/classic/html/issue.search.html:58
+#: ../share/roundup/templates/classic/html/issue.search.html:123
+#: ../share/roundup/templates/classic/html/issue.search.html:139
 msgid "not selected"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:67
+#: ../share/roundup/templates/classic/html/issue.search.html:67
 msgid "ID:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:75
+#: ../share/roundup/templates/classic/html/issue.search.html:75
 msgid "Creation Date:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:86
+#: ../share/roundup/templates/classic/html/issue.search.html:86
 msgid "Creator:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:88
+#: ../share/roundup/templates/classic/html/issue.search.html:88
 msgid "created by me"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:97
+#: ../share/roundup/templates/classic/html/issue.search.html:97
 msgid "Activity:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:108
+#: ../share/roundup/templates/classic/html/issue.search.html:108
 msgid "Actor:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:110
+#: ../share/roundup/templates/classic/html/issue.search.html:110
 msgid "done by me"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:121
+#: ../share/roundup/templates/classic/html/issue.search.html:121
 msgid "Priority:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:134
+#: ../share/roundup/templates/classic/html/issue.search.html:134
 msgid "Status:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:137
+#: ../share/roundup/templates/classic/html/issue.search.html:137
 msgid "not resolved"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:152
+#: ../share/roundup/templates/classic/html/issue.search.html:152
 msgid "Assigned to:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:155
+#: ../share/roundup/templates/classic/html/issue.search.html:155
 msgid "assigned to me"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:157
+#: ../share/roundup/templates/classic/html/issue.search.html:157
 msgid "unassigned"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:167
+#: ../share/roundup/templates/classic/html/issue.search.html:167
 msgid "No Sort or group:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:175
+#: ../share/roundup/templates/classic/html/issue.search.html:175
 msgid "Pagesize:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:181
+#: ../share/roundup/templates/classic/html/issue.search.html:181
 msgid "Start With:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:187
+#: ../share/roundup/templates/classic/html/issue.search.html:187
 msgid "Sort Descending:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:194
+#: ../share/roundup/templates/classic/html/issue.search.html:194
 msgid "Group Descending:"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:201
+#: ../share/roundup/templates/classic/html/issue.search.html:201
 msgid "Query name**:"
 msgstr ""
 
-#: ../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
+#: ../share/roundup/templates/classic/html/issue.search.html:213
+#: ../share/roundup/templates/classic/html/page.html:43
+#: ../share/roundup/templates/classic/html/page.html:92
+#: ../share/roundup/templates/classic/html/user.help-search.html:69
+#: ../share/roundup/templates/minimal/html/page.html:43
+#: ../share/roundup/templates/minimal/html/page.html:91
 msgid "Search"
 msgstr ""
 
-#: ../templates/classic/html/issue.search.html:218
+#: ../share/roundup/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:221
+#: ../share/roundup/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 ""
 
-#: ../templates/classic/html/keyword.item.html:3
+#: ../share/roundup/templates/classic/html/keyword.item.html:3
 msgid "Keyword editing - ${tracker}"
 msgstr ""
 
-#: ../templates/classic/html/keyword.item.html:5
+#: ../share/roundup/templates/classic/html/keyword.item.html:5
 msgid "Keyword editing"
 msgstr ""
 
-#: ../templates/classic/html/keyword.item.html:11
+#: ../share/roundup/templates/classic/html/keyword.item.html:11
 msgid "Existing Keywords"
 msgstr ""
 
-#: ../templates/classic/html/keyword.item.html:20
+#: ../share/roundup/templates/classic/html/keyword.item.html:20
 msgid ""
 "To edit an existing keyword (for spelling or typing errors), click on its "
 "entry above."
 msgstr ""
 
-#: ../templates/classic/html/keyword.item.html:27
+#: ../share/roundup/templates/classic/html/keyword.item.html:27
 msgid "To create a new keyword, enter it below and click \"Submit New Entry\"."
 msgstr ""
 
-#: ../templates/classic/html/msg.index.html:3
+#: ../share/roundup/templates/classic/html/msg.index.html:3
 msgid "List of messages - ${tracker}"
 msgstr ""
 
-#: ../templates/classic/html/msg.index.html:5
+#: ../share/roundup/templates/classic/html/msg.index.html:5
 msgid "Message listing"
 msgstr ""
 
-#: ../templates/classic/html/msg.item.html:6
+#: ../share/roundup/templates/classic/html/msg.item.html:6
 msgid "Message ${id} - ${tracker}"
 msgstr ""
 
-#: ../templates/classic/html/msg.item.html:9
+#: ../share/roundup/templates/classic/html/msg.item.html:9
 msgid "New Message - ${tracker}"
 msgstr ""
 
-#: ../templates/classic/html/msg.item.html:13
+#: ../share/roundup/templates/classic/html/msg.item.html:13
 msgid "New Message"
 msgstr ""
 
-#: ../templates/classic/html/msg.item.html:15
+#: ../share/roundup/templates/classic/html/msg.item.html:15
 msgid "New Message Editing"
 msgstr ""
 
-#: ../templates/classic/html/msg.item.html:18
+#: ../share/roundup/templates/classic/html/msg.item.html:18
 msgid "Message${id}"
 msgstr ""
 
-#: ../templates/classic/html/msg.item.html:21
+#: ../share/roundup/templates/classic/html/msg.item.html:21
 msgid "Message${id} Editing"
 msgstr ""
 
-#: ../templates/classic/html/msg.item.html:38
+#: ../share/roundup/templates/classic/html/msg.item.html:38
 msgid "Author"
 msgstr ""
 
-#: ../templates/classic/html/msg.item.html:43
+#: ../share/roundup/templates/classic/html/msg.item.html:43
 msgid "Recipients"
 msgstr ""
 
-#: ../templates/classic/html/msg.item.html:54
+#: ../share/roundup/templates/classic/html/msg.item.html:54
 msgid "Content"
 msgstr ""
 
-#: ../templates/classic/html/page.html:54
-#: ../templates/minimal/html/page.html:53
+#: ../share/roundup/templates/classic/html/page.html:54
+#: ../share/roundup/templates/minimal/html/page.html:53
 msgid "<b>Your Queries</b> (<a href=\"query?@template=edit\">edit</a>)"
 msgstr ""
 
-#: ../templates/classic/html/page.html:65
-#: ../templates/minimal/html/page.html:64
+#: ../share/roundup/templates/classic/html/page.html:65
+#: ../share/roundup/templates/minimal/html/page.html:64
 msgid "Issues"
 msgstr ""
 
-#: ../templates/classic/html/page.html:67
-#: ../templates/classic/html/page.html:105
-#: ../templates/minimal/html/page.html:66
-#: ../templates/minimal/html/page.html:104
+#: ../share/roundup/templates/classic/html/page.html:67
+#: ../share/roundup/templates/classic/html/page.html:105
+#: ../share/roundup/templates/minimal/html/page.html:66
+#: ../share/roundup/templates/minimal/html/page.html:104
 msgid "Create New"
 msgstr ""
 
-#: ../templates/classic/html/page.html:69
-#: ../templates/minimal/html/page.html:68
+#: ../share/roundup/templates/classic/html/page.html:69
+#: ../share/roundup/templates/minimal/html/page.html:68
 msgid "Show Unassigned"
 msgstr ""
 
-#: ../templates/classic/html/page.html:81
-#: ../templates/minimal/html/page.html:80
+#: ../share/roundup/templates/classic/html/page.html:81
+#: ../share/roundup/templates/minimal/html/page.html:80
 msgid "Show All"
 msgstr ""
 
-#: ../templates/classic/html/page.html:93
-#: ../templates/minimal/html/page.html:92
+#: ../share/roundup/templates/classic/html/page.html:93
+#: ../share/roundup/templates/minimal/html/page.html:92
 msgid "Show issue:"
 msgstr ""
 
-#: ../templates/classic/html/page.html:108
-#: ../templates/minimal/html/page.html:107
+#: ../share/roundup/templates/classic/html/page.html:108
+#: ../share/roundup/templates/minimal/html/page.html:107
 msgid "Edit Existing"
 msgstr ""
 
-#: ../templates/classic/html/page.html:114
-#: ../templates/minimal/html/page.html:113
+#: ../share/roundup/templates/classic/html/page.html:114
+#: ../share/roundup/templates/minimal/html/page.html:113
 msgid "Administration"
 msgstr ""
 
-#: ../templates/classic/html/page.html:116
-#: ../templates/minimal/html/page.html:115
+#: ../share/roundup/templates/classic/html/page.html:116
+#: ../share/roundup/templates/minimal/html/page.html:115
 msgid "Class List"
 msgstr ""
 
-#: ../templates/classic/html/page.html:120
-#: ../templates/minimal/html/page.html:119
+#: ../share/roundup/templates/classic/html/page.html:120
+#: ../share/roundup/templates/minimal/html/page.html:119
 msgid "User List"
 msgstr ""
 
-#: ../templates/classic/html/page.html:122
-#: ../templates/minimal/html/page.html:121
+#: ../share/roundup/templates/classic/html/page.html:122
+#: ../share/roundup/templates/minimal/html/page.html:121
 msgid "Add User"
 msgstr ""
 
-#: ../templates/classic/html/page.html:129
-#: ../templates/classic/html/page.html:135
-#: ../templates/minimal/html/page.html:128
-#: ../templates/minimal/html/page.html:134
+#: ../share/roundup/templates/classic/html/page.html:129
+#: ../share/roundup/templates/classic/html/page.html:135
+#: ../share/roundup/templates/minimal/html/page.html:128
+#: ../share/roundup/templates/minimal/html/page.html:134
 msgid "Login"
 msgstr ""
 
-#: ../templates/classic/html/page.html:134
-#: ../templates/minimal/html/page.html:133
+#: ../share/roundup/templates/classic/html/page.html:134
+#: ../share/roundup/templates/minimal/html/page.html:133
 msgid "Remember me?"
 msgstr ""
 
-#: ../templates/classic/html/page.html:138
-#: ../templates/classic/html/user.register.html:63
-#: ../templates/minimal/html/page.html:137
-#: ../templates/minimal/html/user.register.html:61
+#: ../share/roundup/templates/classic/html/page.html:138
+#: ../share/roundup/templates/classic/html/user.register.html:63
+#: ../share/roundup/templates/minimal/html/page.html:137
+#: ../share/roundup/templates/minimal/html/user.register.html:61
 msgid "Register"
 msgstr ""
 
-#: ../templates/classic/html/page.html:141
-#: ../templates/minimal/html/page.html:140
+#: ../share/roundup/templates/classic/html/page.html:141
+#: ../share/roundup/templates/minimal/html/page.html:140
 msgid "Lost&nbsp;your&nbsp;login?"
 msgstr ""
 
-#: ../templates/classic/html/page.html:146
-#: ../templates/minimal/html/page.html:145
+#: ../share/roundup/templates/classic/html/page.html:146
+#: ../share/roundup/templates/minimal/html/page.html:145
 msgid "Hello, ${user}"
 msgstr ""
 
-#: ../templates/classic/html/page.html:148
+#: ../share/roundup/templates/classic/html/page.html:148
 msgid "Your Issues"
 msgstr ""
 
-#: ../templates/classic/html/page.html:160
-#: ../templates/minimal/html/page.html:147
+#: ../share/roundup/templates/classic/html/page.html:160
+#: ../share/roundup/templates/minimal/html/page.html:147
 msgid "Your Details"
 msgstr ""
 
-#: ../templates/classic/html/page.html:162
-#: ../templates/minimal/html/page.html:149
+#: ../share/roundup/templates/classic/html/page.html:162
+#: ../share/roundup/templates/minimal/html/page.html:149
 msgid "Logout"
 msgstr ""
 
-#: ../templates/classic/html/page.html:166
-#: ../templates/minimal/html/page.html:153
+#: ../share/roundup/templates/classic/html/page.html:166
+#: ../share/roundup/templates/minimal/html/page.html:153
 msgid "Help"
 msgstr ""
 
-#: ../templates/classic/html/page.html:167
-#: ../templates/minimal/html/page.html:154
+#: ../share/roundup/templates/classic/html/page.html:167
+#: ../share/roundup/templates/minimal/html/page.html:154
 msgid "Roundup docs"
 msgstr ""
 
-#: ../templates/classic/html/page.html:177
-#: ../templates/minimal/html/page.html:164
+#: ../share/roundup/templates/classic/html/page.html:177
+#: ../share/roundup/templates/minimal/html/page.html:164
 msgid "clear this message"
 msgstr ""
 
-#: ../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
+#: ../share/roundup/templates/classic/html/page.html:241
+#: ../share/roundup/templates/classic/html/page.html:256
+#: ../share/roundup/templates/classic/html/page.html:270
+#: ../share/roundup/templates/minimal/html/page.html:228
+#: ../share/roundup/templates/minimal/html/page.html:243
+#: ../share/roundup/templates/minimal/html/page.html:257
 msgid "don't care"
 msgstr ""
 
-#: ../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
+#: ../share/roundup/templates/classic/html/page.html:243
+#: ../share/roundup/templates/classic/html/page.html:258
+#: ../share/roundup/templates/classic/html/page.html:271
+#: ../share/roundup/templates/minimal/html/page.html:230
+#: ../share/roundup/templates/minimal/html/page.html:245
+#: ../share/roundup/templates/minimal/html/page.html:258
 msgid "------------"
 msgstr ""
 
-#: ../templates/classic/html/page.html:299
-#: ../templates/minimal/html/page.html:286
+#: ../share/roundup/templates/classic/html/page.html:299
+#: ../share/roundup/templates/minimal/html/page.html:286
 msgid "no value"
 msgstr ""
 
-#: ../templates/classic/html/query.edit.html:4
+#: ../share/roundup/templates/classic/html/query.edit.html:4
 msgid "\"Your Queries\" Editing - ${tracker}"
 msgstr ""
 
-#: ../templates/classic/html/query.edit.html:6
+#: ../share/roundup/templates/classic/html/query.edit.html:6
 msgid "\"Your Queries\" Editing"
 msgstr ""
 
-#: ../templates/classic/html/query.edit.html:11
+#: ../share/roundup/templates/classic/html/query.edit.html:11
 msgid "You are not allowed to edit queries."
 msgstr ""
 
-#: ../templates/classic/html/query.edit.html:28
+#: ../share/roundup/templates/classic/html/query.edit.html:28
 msgid "Query"
 msgstr ""
 
-#: ../templates/classic/html/query.edit.html:29
+#: ../share/roundup/templates/classic/html/query.edit.html:29
 msgid "Include in \"Your Queries\""
 msgstr ""
 
-#: ../templates/classic/html/query.edit.html:31
+#: ../share/roundup/templates/classic/html/query.edit.html:31
 msgid "Private to you?"
 msgstr ""
 
-#: ../templates/classic/html/query.edit.html:44
+#: ../share/roundup/templates/classic/html/query.edit.html:44
 msgid "leave out"
 msgstr ""
 
-#: ../templates/classic/html/query.edit.html:45
+#: ../share/roundup/templates/classic/html/query.edit.html:45
 msgid "include"
 msgstr ""
 
-#: ../templates/classic/html/query.edit.html:49
+#: ../share/roundup/templates/classic/html/query.edit.html:49
 msgid "leave in"
 msgstr ""
 
-#: ../templates/classic/html/query.edit.html:54
+#: ../share/roundup/templates/classic/html/query.edit.html:54
 msgid "[query is retired]"
 msgstr ""
 
-#: ../templates/classic/html/query.edit.html:67
-#: ../templates/classic/html/query.edit.html:94
+#: ../share/roundup/templates/classic/html/query.edit.html:67
+#: ../share/roundup/templates/classic/html/query.edit.html:94
 msgid "edit"
 msgstr ""
 
-#: ../templates/classic/html/query.edit.html:71
+#: ../share/roundup/templates/classic/html/query.edit.html:71
 msgid "yes"
 msgstr ""
 
-#: ../templates/classic/html/query.edit.html:73
+#: ../share/roundup/templates/classic/html/query.edit.html:73
 msgid "no"
 msgstr ""
 
-#: ../templates/classic/html/query.edit.html:79
+#: ../share/roundup/templates/classic/html/query.edit.html:79
 msgid "Delete"
 msgstr ""
 
-#: ../templates/classic/html/query.edit.html:96
+#: ../share/roundup/templates/classic/html/query.edit.html:96
 msgid "[not yours to edit]"
 msgstr ""
 
-#: ../templates/classic/html/query.edit.html:104
+#: ../share/roundup/templates/classic/html/query.edit.html:104
 msgid "Save Selection"
 msgstr ""
 
-#: ../templates/classic/html/user.forgotten.html:3
+#: ../share/roundup/templates/classic/html/user.forgotten.html:3
 msgid "Password reset request - ${tracker}"
 msgstr ""
 
-#: ../templates/classic/html/user.forgotten.html:5
+#: ../share/roundup/templates/classic/html/user.forgotten.html:5
 msgid "Password reset request"
 msgstr ""
 
-#: ../templates/classic/html/user.forgotten.html:9
+#: ../share/roundup/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 ""
 
-#: ../templates/classic/html/user.forgotten.html:16
+#: ../share/roundup/templates/classic/html/user.forgotten.html:16
 msgid "Email Address:"
 msgstr ""
 
-#: ../templates/classic/html/user.forgotten.html:24
-#: ../templates/classic/html/user.forgotten.html:34
+#: ../share/roundup/templates/classic/html/user.forgotten.html:24
+#: ../share/roundup/templates/classic/html/user.forgotten.html:34
 msgid "Request password reset"
 msgstr ""
 
-#: ../templates/classic/html/user.forgotten.html:30
+#: ../share/roundup/templates/classic/html/user.forgotten.html:30
 msgid "Or, if you know your username, then enter it below."
 msgstr ""
 
-#: ../templates/classic/html/user.forgotten.html:33
+#: ../share/roundup/templates/classic/html/user.forgotten.html:33
 msgid "Username:"
 msgstr ""
 
-#: ../templates/classic/html/user.forgotten.html:39
+#: ../share/roundup/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 ""
 
-#: ../templates/classic/html/user.help-search.html:73
+#: ../share/roundup/templates/classic/html/user.help-search.html:73
 msgid "Pagesize"
 msgstr ""
 
-#: ../templates/classic/html/user.help.html:43
+#: ../share/roundup/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
+#: ../share/roundup/templates/classic/html/user.index.html:3
+#: ../share/roundup/templates/minimal/html/user.index.html:3
 msgid "User listing - ${tracker}"
 msgstr ""
 
-#: ../templates/classic/html/user.index.html:5
-#: ../templates/minimal/html/user.index.html:5
+#: ../share/roundup/templates/classic/html/user.index.html:5
+#: ../share/roundup/templates/minimal/html/user.index.html:5
 msgid "User listing"
 msgstr ""
 
-#: ../templates/classic/html/user.index.html:19
-#: ../templates/minimal/html/user.index.html:19
+#: ../share/roundup/templates/classic/html/user.index.html:19
+#: ../share/roundup/templates/minimal/html/user.index.html:19
 msgid "Username"
 msgstr ""
 
-#: ../templates/classic/html/user.index.html:20
+#: ../share/roundup/templates/classic/html/user.index.html:20
 msgid "Real name"
 msgstr ""
 
-#: ../templates/classic/html/user.index.html:21
-#: ../templates/classic/html/user.register.html:45
+#: ../share/roundup/templates/classic/html/user.index.html:21
+#: ../share/roundup/templates/classic/html/user.register.html:45
 msgid "Organisation"
 msgstr ""
 
-#: ../templates/classic/html/user.index.html:22
-#: ../templates/minimal/html/user.index.html:20
+#: ../share/roundup/templates/classic/html/user.index.html:22
+#: ../share/roundup/templates/minimal/html/user.index.html:20
 msgid "Email address"
 msgstr ""
 
-#: ../templates/classic/html/user.index.html:23
+#: ../share/roundup/templates/classic/html/user.index.html:23
 msgid "Phone number"
 msgstr ""
 
-#: ../templates/classic/html/user.index.html:24
+#: ../share/roundup/templates/classic/html/user.index.html:24
 msgid "Retire"
 msgstr ""
 
-#: ../templates/classic/html/user.index.html:37
+#: ../share/roundup/templates/classic/html/user.index.html:41
 msgid "retire"
 msgstr ""
 
-#: ../templates/classic/html/user.item.html:9
-#: ../templates/minimal/html/user.item.html:9
+#: ../share/roundup/templates/classic/html/user.item.html:9
+#: ../share/roundup/templates/minimal/html/user.item.html:9
 msgid "User ${id}: ${title} - ${tracker}"
 msgstr ""
 
-#: ../templates/classic/html/user.item.html:12
-#: ../templates/minimal/html/user.item.html:12
+#: ../share/roundup/templates/classic/html/user.item.html:12
+#: ../share/roundup/templates/minimal/html/user.item.html:12
 msgid "New User - ${tracker}"
 msgstr ""
 
-#: ../templates/classic/html/user.item.html:21
-#: ../templates/minimal/html/user.item.html:21
+#: ../share/roundup/templates/classic/html/user.item.html:21
+#: ../share/roundup/templates/minimal/html/user.item.html:21
 msgid "New User"
 msgstr ""
 
-#: ../templates/classic/html/user.item.html:23
-#: ../templates/minimal/html/user.item.html:23
+#: ../share/roundup/templates/classic/html/user.item.html:23
+#: ../share/roundup/templates/minimal/html/user.item.html:23
 msgid "New User Editing"
 msgstr ""
 
-#: ../templates/classic/html/user.item.html:26
-#: ../templates/minimal/html/user.item.html:26
+#: ../share/roundup/templates/classic/html/user.item.html:26
+#: ../share/roundup/templates/minimal/html/user.item.html:26
 msgid "User${id}"
 msgstr ""
 
-#: ../templates/classic/html/user.item.html:29
-#: ../templates/minimal/html/user.item.html:29
+#: ../share/roundup/templates/classic/html/user.item.html:29
+#: ../share/roundup/templates/minimal/html/user.item.html:29
 msgid "User${id} Editing"
 msgstr ""
 
-#: ../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
+#: ../share/roundup/templates/classic/html/user.item.html:80
+#: ../share/roundup/templates/classic/html/user.register.html:33
+#: ../share/roundup/templates/minimal/html/user.item.html:80
+#: ../share/roundup/templates/minimal/html/user.register.html:41
 msgid "Roles"
 msgstr ""
 
-#: ../templates/classic/html/user.item.html:88
-#: ../templates/minimal/html/user.item.html:88
+#: ../share/roundup/templates/classic/html/user.item.html:88
+#: ../share/roundup/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:109
-#: ../templates/minimal/html/user.item.html:109
+#: ../share/roundup/templates/classic/html/user.item.html:109
+#: ../share/roundup/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:130
-#: ../templates/classic/html/user.register.html:53
-#: ../templates/minimal/html/user.item.html:130
-#: ../templates/minimal/html/user.register.html:53
+#: ../share/roundup/templates/classic/html/user.item.html:130
+#: ../share/roundup/templates/classic/html/user.register.html:53
+#: ../share/roundup/templates/minimal/html/user.item.html:130
+#: ../share/roundup/templates/minimal/html/user.register.html:53
 msgid "Alternate E-mail addresses<br>One address per line"
 msgstr ""
 
-#: ../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
+#: ../share/roundup/templates/classic/html/user.register.html:4
+#: ../share/roundup/templates/classic/html/user.register.html:7
+#: ../share/roundup/templates/minimal/html/user.register.html:4
+#: ../share/roundup/templates/minimal/html/user.register.html:7
 msgid "Registering with ${tracker}"
 msgstr ""
 
-#: ../templates/classic/html/user.register.html:21
-#: ../templates/minimal/html/user.register.html:29
+#: ../share/roundup/templates/classic/html/user.register.html:21
+#: ../share/roundup/templates/minimal/html/user.register.html:29
 msgid "Login Name"
 msgstr ""
 
-#: ../templates/classic/html/user.register.html:25
-#: ../templates/minimal/html/user.register.html:33
+#: ../share/roundup/templates/classic/html/user.register.html:25
+#: ../share/roundup/templates/minimal/html/user.register.html:33
 msgid "Login Password"
 msgstr ""
 
-#: ../templates/classic/html/user.register.html:29
-#: ../templates/minimal/html/user.register.html:37
+#: ../share/roundup/templates/classic/html/user.register.html:29
+#: ../share/roundup/templates/minimal/html/user.register.html:37
 msgid "Confirm Password"
 msgstr ""
 
-#: ../templates/classic/html/user.register.html:41
+#: ../share/roundup/templates/classic/html/user.register.html:41
 msgid "Phone"
 msgstr ""
 
-#: ../templates/classic/html/user.register.html:49
-#: ../templates/minimal/html/user.register.html:49
+#: ../share/roundup/templates/classic/html/user.register.html:49
+#: ../share/roundup/templates/minimal/html/user.register.html:49
 msgid "E-mail address"
 msgstr ""
 
-#: ../templates/classic/html/user.rego_progress.html:4
-#: ../templates/minimal/html/user.rego_progress.html:4
+#: ../share/roundup/templates/classic/html/user.rego_progress.html:4
+#: ../share/roundup/templates/minimal/html/user.rego_progress.html:4
 msgid "Registration in progress - ${tracker}"
 msgstr ""
 
-#: ../templates/classic/html/user.rego_progress.html:6
-#: ../templates/minimal/html/user.rego_progress.html:6
+#: ../share/roundup/templates/classic/html/user.rego_progress.html:6
+#: ../share/roundup/templates/minimal/html/user.rego_progress.html:6
 msgid "Registration in progress..."
 msgstr ""
 
-#: ../templates/classic/html/user.rego_progress.html:10
-#: ../templates/minimal/html/user.rego_progress.html:10
+#: ../share/roundup/templates/classic/html/user.rego_progress.html:10
+#: ../share/roundup/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 ""
 
-#: ../templates/classic/initial_data.py:5
+#: ../share/roundup/templates/classic/initial_data.py:5
 msgid "critical"
 msgstr ""
 
-#: ../templates/classic/initial_data.py:6
+#: ../share/roundup/templates/classic/initial_data.py:6
 msgid "urgent"
 msgstr ""
 
-#: ../templates/classic/initial_data.py:7
+#: ../share/roundup/templates/classic/initial_data.py:7
 msgid "bug"
 msgstr ""
 
-#: ../templates/classic/initial_data.py:8
+#: ../share/roundup/templates/classic/initial_data.py:8
 msgid "feature"
 msgstr ""
 
-#: ../templates/classic/initial_data.py:9
+#: ../share/roundup/templates/classic/initial_data.py:9
 msgid "wish"
 msgstr ""
 
-#: ../templates/classic/initial_data.py:12
+#: ../share/roundup/templates/classic/initial_data.py:12
 msgid "unread"
 msgstr ""
 
-#: ../templates/classic/initial_data.py:13
+#: ../share/roundup/templates/classic/initial_data.py:13
 msgid "deferred"
 msgstr ""
 
-#: ../templates/classic/initial_data.py:14
+#: ../share/roundup/templates/classic/initial_data.py:14
 msgid "chatting"
 msgstr ""
 
-#: ../templates/classic/initial_data.py:15
+#: ../share/roundup/templates/classic/initial_data.py:15
 msgid "need-eg"
 msgstr ""
 
-#: ../templates/classic/initial_data.py:16
+#: ../share/roundup/templates/classic/initial_data.py:16
 msgid "in-progress"
 msgstr ""
 
-#: ../templates/classic/initial_data.py:17
+#: ../share/roundup/templates/classic/initial_data.py:17
 msgid "testing"
 msgstr ""
 
-#: ../templates/classic/initial_data.py:18
+#: ../share/roundup/templates/classic/initial_data.py:18
 msgid "done-cbb"
 msgstr ""
 
-#: ../templates/classic/initial_data.py:19
+#: ../share/roundup/templates/classic/initial_data.py:19
 msgid "resolved"
 msgstr ""
 
-#: ../templates/minimal/html/home.html:2
+#: ../share/roundup/templates/minimal/html/home.html:2
 msgid "Tracker home - ${tracker}"
 msgstr ""
 
-#: ../templates/minimal/html/home.html:4
+#: ../share/roundup/templates/minimal/html/home.html:4
 msgid "Tracker home"
 msgstr ""
 
-#: ../templates/minimal/html/home.html:16
+#: ../share/roundup/templates/minimal/html/home.html:16
 msgid "Please select from one of the menu options on the left."
 msgstr ""
 
-#: ../templates/minimal/html/home.html:19
+#: ../share/roundup/templates/minimal/html/home.html:19
 msgid "Please log in or register."
 msgstr ""

Modified: tracker/roundup-src/locale/ru.po
==============================================================================
--- tracker/roundup-src/locale/ru.po	(original)
+++ tracker/roundup-src/locale/ru.po	Sun Mar 15 22:43:30 2009
@@ -1,7 +1,7 @@
 # Russian message file for Roundup Issue Tracker
 # alexander smishlajev <alex at tycobka.lv>, 2004
 #
-# $Id: ru.po,v 1.16 2007/09/16 07:23:04 a1s Exp $
+# $Id: ru.po,v 1.16 2007-09-16 07:23:04 a1s Exp $
 #
 # roundup.pot revision 1.23
 #

Modified: tracker/roundup-src/locale/zh_CN.po
==============================================================================
--- tracker/roundup-src/locale/zh_CN.po	(original)
+++ tracker/roundup-src/locale/zh_CN.po	Sun Mar 15 22:43:30 2009
@@ -1,7 +1,7 @@
 # Chinese message file for Roundup Issue Tracker
 # limodou <limodou at gmail.com>
 #
-# $Id: zh_CN.po,v 1.3 2005/05/16 09:23:22 a1s Exp $
+# $Id: zh_CN.po,v 1.3 2005-05-16 09:23:22 a1s Exp $
 #
 # roundup.pot revision 1.10
 #

Modified: tracker/roundup-src/locale/zh_TW.po
==============================================================================
--- tracker/roundup-src/locale/zh_TW.po	(original)
+++ tracker/roundup-src/locale/zh_TW.po	Sun Mar 15 22:43:30 2009
@@ -1,7 +1,7 @@
 # Chinese Traditional message file for Roundup Issue Tracker
 # Fred Lin <gasolin at gmail.com>
 #
-# $Id: zh_TW.po,v 1.2 2005/05/16 09:31:48 a1s Exp $
+# $Id: zh_TW.po,v 1.2 2005-05-16 09:31:48 a1s Exp $
 #
 # roundup.pot revision 1.10
 #

Modified: tracker/roundup-src/roundup/__init__.py
==============================================================================
--- tracker/roundup-src/roundup/__init__.py	(original)
+++ tracker/roundup-src/roundup/__init__.py	Sun Mar 15 22:43:30 2009
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-# $Id: __init__.py,v 1.49 2007/12/23 01:52:07 richard Exp $
+# $Id: __init__.py,v 1.54 2008-09-01 01:58:32 richard Exp $
 
 '''Roundup - issue tracking for knowledge workers.
 
@@ -68,6 +68,6 @@
 '''
 __docformat__ = 'restructuredtext'
 
-__version__ = '1.4.2'
+__version__ = '1.4.7'
 
 # vim: set filetype=python ts=4 sw=4 et si

Added: tracker/roundup-src/roundup/actions.py
==============================================================================
--- (empty file)
+++ tracker/roundup-src/roundup/actions.py	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,68 @@
+#
+# Copyright (C) 2009 Stefan Seefeld
+# All rights reserved.
+# For license terms see the file COPYING.txt.
+#
+
+from roundup.exceptions import *
+from roundup import hyperdb
+from roundup.i18n import _
+
+class Action:
+    def __init__(self, db, translator):
+        self.db = db
+        self.translator = translator
+
+    def handle(self, *args):
+        """Action handler procedure"""
+        raise NotImplementedError
+
+    def execute(self, *args):
+        """Execute the action specified by this object."""
+
+        self.permission(*args)
+        return self.handle(*args)
+
+
+    def permission(self, *args):
+        """Check whether the user has permission to execute this action.
+
+        If not, raise Unauthorised."""
+
+        pass
+
+
+    def gettext(self, msgid):
+        """Return the localized translation of msgid"""
+        return self.translator.gettext(msgid)
+
+
+    _ = gettext
+
+
+class Retire(Action):
+
+    def handle(self, designator):
+
+        classname, itemid = hyperdb.splitDesignator(designator)
+
+        # make sure we don't try to retire admin or anonymous
+        if (classname == 'user' and
+            self.db.user.get(itemid, 'username') in ('admin', 'anonymous')):
+            raise ValueError, self._(
+                'You may not retire the admin or anonymous user')
+
+        # do the retire
+        self.db.getclass(classname).retire(itemid)
+        self.db.commit()
+
+
+    def permission(self, designator):
+
+        classname, itemid = hyperdb.splitDesignator(designator)
+
+        if not self.db.security.hasPermission('Edit', self.db.getuid(),
+                                              classname=classname, itemid=itemid):
+            raise Unauthorised(self._('You do not have permission to '
+                                      '%(action)s the %(classname)s class.')%info)
+            

Modified: tracker/roundup-src/roundup/admin.py
==============================================================================
--- tracker/roundup-src/roundup/admin.py	(original)
+++ tracker/roundup-src/roundup/admin.py	Sun Mar 15 22:43:30 2009
@@ -16,10 +16,9 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-# $Id: admin.py,v 1.110 2008/02/07 03:28:33 richard Exp $
 
-'''Administration commands for maintaining Roundup trackers.
-'''
+"""Administration commands for maintaining Roundup trackers.
+"""
 __docformat__ = 'restructuredtext'
 
 import csv, getopt, getpass, os, re, shutil, sys, UserDict
@@ -29,12 +28,13 @@
 import roundup.instance
 from roundup.configuration import CoreConfig
 from roundup.i18n import _
+from roundup.exceptions import UsageError
 
 class CommandDict(UserDict.UserDict):
-    '''Simple dictionary that lets us do lookups using partial keys.
+    """Simple dictionary that lets us do lookups using partial keys.
 
     Original code submitted by Engelbert Gruber.
-    '''
+    """
     _marker = []
     def get(self, key, default=_marker):
         if self.data.has_key(key):
@@ -49,11 +49,8 @@
             raise KeyError, key
         return l
 
-class UsageError(ValueError):
-    pass
-
 class AdminTool:
-    ''' A collection of methods used in maintaining Roundup trackers.
+    """ A collection of methods used in maintaining Roundup trackers.
 
         Typically these methods are accessed through the roundup-admin
         script. The main() method provided on this class gives the main
@@ -63,7 +60,7 @@
         given in the method docstring.
 
         Additional help may be supplied by help_*() methods.
-    '''
+    """
     def __init__(self):
         self.commands = CommandDict()
         for k in AdminTool.__dict__.keys():
@@ -78,18 +75,18 @@
         self.db_uncommitted = False
 
     def get_class(self, classname):
-        '''Get the class - raise an exception if it doesn't exist.
-        '''
+        """Get the class - raise an exception if it doesn't exist.
+        """
         try:
             return self.db.getclass(classname)
         except KeyError:
             raise UsageError, _('no such class "%(classname)s"')%locals()
 
     def props_from_args(self, args):
-        ''' Produce a dictionary of prop: value from the args list.
+        """ Produce a dictionary of prop: value from the args list.
 
             The args list is specified as ``prop=value prop=value ...``.
-        '''
+        """
         props = {}
         for arg in args:
             if arg.find('=') == -1:
@@ -107,11 +104,11 @@
         return props
 
     def usage(self, message=''):
-        ''' Display a simple usage message.
-        '''
+        """ Display a simple usage message.
+        """
         if message:
             message = _('Problem: %(message)s\n\n')%locals()
-        print _('''%(message)sUsage: roundup-admin [options] [<command> <arguments>]
+        print _("""%(message)sUsage: roundup-admin [options] [<command> <arguments>]
 
 Options:
  -i instance home  -- specify the issue tracker "home directory" to administer
@@ -132,12 +129,12 @@
  roundup-admin help                       -- this help
  roundup-admin help <command>             -- command-specific help
  roundup-admin help all                   -- all available help
-''')%locals()
+""")%locals()
         self.help_commands()
 
     def help_commands(self):
-        ''' List the commands available with their help summary.
-        '''
+        """List the commands available with their help summary.
+        """
         print _('Commands:'),
         commands = ['']
         for command in self.commands.values():
@@ -151,8 +148,8 @@
         print
 
     def help_commands_html(self, indent_re=re.compile(r'^(\s+)\S+')):
-        ''' Produce an HTML command list.
-        '''
+        """ Produce an HTML command list.
+        """
         commands = self.commands.values()
         def sortfun(a, b):
             return cmp(a.__name__, b.__name__)
@@ -161,10 +158,10 @@
             h = _(command.__doc__).split('\n')
             name = command.__name__[3:]
             usage = h[0]
-            print '''
+            print """
 <tr><td valign=top><strong>%(name)s</strong></td>
     <td><tt>%(usage)s</tt><p>
-<pre>''' % locals()
+<pre>""" % locals()
             indent = indent_re.match(h[3])
             if indent: indent = len(indent.group(1))
             for line in h[3:]:
@@ -175,7 +172,7 @@
             print '</pre></td></tr>\n'
 
     def help_all(self):
-        print _('''
+        print _("""
 All commands (except help) require a tracker specifier. This is just
 the path to the roundup tracker you're working with. A roundup tracker
 is where roundup keeps the database and configuration file that defines
@@ -236,21 +233,21 @@
   "." means "right now"
 
 Command help:
-''')
+""")
         for name, command in self.commands.items():
             print _('%s:')%name
             print '   ', _(command.__doc__)
 
     def do_help(self, args, nl_re=re.compile('[\r\n]'),
             indent_re=re.compile(r'^(\s+)\S+')):
-        ""'''Usage: help topic
+        ''"""Usage: help topic
         Give help about topic.
 
         commands  -- list commands
         <command> -- help specific to a command
         initopts  -- init command options
         all       -- all available help
-        '''
+        """
         if len(args)>0:
             topic = args[0]
         else:
@@ -283,7 +280,7 @@
         return 0
 
     def listTemplates(self):
-        ''' List all the available templates.
+        """ List all the available templates.
 
         Look in the following places, where the later rules take precedence:
 
@@ -299,7 +296,7 @@
             this is for when someone unpacks a 3rd-party template
          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
@@ -349,7 +346,7 @@
         print _('Back ends:'), ', '.join(backends)
 
     def do_install(self, tracker_home, args):
-        ""'''Usage: install [template [backend [key=val[,key=val]]]]
+        ''"""Usage: install [template [backend [key=val[,key=val]]]]
         Install a new Roundup tracker.
 
         The command will prompt for the tracker home directory
@@ -370,7 +367,7 @@
         the tracker's dbinit.py module init() function.
 
         See also initopts help.
-        '''
+        """
         if len(args) < 1:
             raise UsageError, _('Not enough arguments supplied')
 
@@ -464,23 +461,23 @@
         return 0
 
     def do_genconfig(self, args):
-        ""'''Usage: genconfig <filename>
+        ''"""Usage: genconfig <filename>
         Generate a new tracker config file (ini style) with default values
         in <filename>.
-        '''
+        """
         if len(args) < 1:
             raise UsageError, _('Not enough arguments supplied')
         config = CoreConfig()
         config.save(args[0])
 
     def do_initialise(self, tracker_home, args):
-        ""'''Usage: initialise [adminpw]
+        ''"""Usage: initialise [adminpw]
         Initialise a new Roundup tracker.
 
         The administrator details will be set at this step.
 
         Execute the tracker's initialisation function dbinit.init()
-        '''
+        """
         # password
         if len(args) > 1:
             adminpw = args[1]
@@ -523,12 +520,12 @@
 
 
     def do_get(self, args):
-        ""'''Usage: get property designator[,designator]*
+        ''"""Usage: get property designator[,designator]*
         Get the given property of one or more designator(s).
 
         Retrieves the property value of the nodes specified
         by the designators.
-        '''
+        """
         if len(args) < 2:
             raise UsageError, _('Not enough arguments supplied')
         propname = args[0]
@@ -597,7 +594,7 @@
 
 
     def do_set(self, args):
-        ""'''Usage: set items property=value property=value ...
+        ''"""Usage: set items property=value property=value ...
         Set the given properties of one or more items(s).
 
         The items are specified as a class or as a comma-separated
@@ -607,7 +604,7 @@
         given. If the value is missing (ie. "property=") then the property
         is un-set. If the property is a multilink, you specify the linked
         ids for the multilink as comma-separated numbers (ie "1,2,3").
-        '''
+        """
         if len(args) < 2:
             raise UsageError, _('Not enough arguments supplied')
         from roundup import hyperdb
@@ -652,13 +649,13 @@
         return 0
 
     def do_find(self, args):
-        ""'''Usage: find classname propname=value ...
+        ''"""Usage: find classname propname=value ...
         Find the nodes of the given class with a given link property value.
 
         Find the nodes of the given class with a given link property value.
         The value may be either the nodeid of the linked node, or its key
         value.
-        '''
+        """
         if len(args) < 1:
             raise UsageError, _('Not enough arguments supplied')
         classname = args[0]
@@ -712,11 +709,11 @@
         return 0
 
     def do_specification(self, args):
-        ""'''Usage: specification classname
+        ''"""Usage: specification classname
         Show the properties for a classname.
 
         This lists the properties for a given class.
-        '''
+        """
         if len(args) < 1:
             raise UsageError, _('Not enough arguments supplied')
         classname = args[0]
@@ -732,12 +729,12 @@
                 print _('%(key)s: %(value)s')%locals()
 
     def do_display(self, args):
-        ""'''Usage: display designator[,designator]*
+        ''"""Usage: display designator[,designator]*
         Show the property values for the given node(s).
 
         This lists the properties and their associated values for the given
         node.
-        '''
+        """
         if len(args) < 1:
             raise UsageError, _('Not enough arguments supplied')
 
@@ -759,13 +756,13 @@
                 print _('%(key)s: %(value)s')%locals()
 
     def do_create(self, args):
-        ""'''Usage: create classname property=value ...
+        ''"""Usage: create classname property=value ...
         Create a new entry of a given class.
 
         This creates a new entry of the given class using the property
         name=value arguments provided on the command line after the "create"
         command.
-        '''
+        """
         if len(args) < 1:
             raise UsageError, _('Not enough arguments supplied')
         from roundup import hyperdb
@@ -824,7 +821,7 @@
         return 0
 
     def do_list(self, args):
-        ""'''Usage: list classname [property]
+        ''"""Usage: list classname [property]
         List the instances of a class.
 
         Lists all instances of the given class. If the property is not
@@ -835,13 +832,13 @@
         With -c, -S or -s print a list of item id's if no property
         specified.  If property specified, print list of that property
         for every class instance.
-        '''
+        """
         if len(args) > 2:
             raise UsageError, _('Too many arguments supplied')
         if len(args) < 1:
             raise UsageError, _('Not enough arguments supplied')
         classname = args[0]
-
+        
         # get the class
         cl = self.get_class(classname)
 
@@ -853,7 +850,7 @@
 
         if self.separator:
             if len(args) == 2:
-               # create a list of propnames since user specified propname
+                # create a list of propnames since user specified propname
                 proplist=[]
                 for nodeid in cl.list():
                     try:
@@ -877,7 +874,7 @@
         return 0
 
     def do_table(self, args):
-        ""'''Usage: table classname [property[,property]*]
+        ''"""Usage: table classname [property[,property]*]
         List the instances of a class in tabular form.
 
         Lists all instances of the given class. If the properties are not
@@ -904,7 +901,7 @@
           4  feat
 
         will result in a the 4 character wide "Name" column.
-        '''
+        """
         if len(args) < 1:
             raise UsageError, _('Not enough arguments supplied')
         classname = args[0]
@@ -971,11 +968,11 @@
         return 0
 
     def do_history(self, args):
-        ""'''Usage: history designator
+        ''"""Usage: history designator
         Show the history entries of a designator.
 
         Lists the journal entries for the node identified by the designator.
-        '''
+        """
         if len(args) < 1:
             raise UsageError, _('Not enough arguments supplied')
         try:
@@ -992,7 +989,7 @@
         return 0
 
     def do_commit(self, args):
-        ""'''Usage: commit
+        ''"""Usage: commit
         Commit changes made to the database during an interactive session.
 
         The changes made during an interactive session are not
@@ -1001,31 +998,31 @@
 
         One-off commands on the command-line are automatically committed if
         they are successful.
-        '''
+        """
         self.db.commit()
         self.db_uncommitted = False
         return 0
 
     def do_rollback(self, args):
-        ""'''Usage: rollback
+        ''"""Usage: rollback
         Undo all changes that are pending commit to the database.
 
         The changes made during an interactive session are not
         automatically written to the database - they must be committed
         manually. This command undoes all those changes, so a commit
         immediately after would make no changes to the database.
-        '''
+        """
         self.db.rollback()
         self.db_uncommitted = False
         return 0
 
     def do_retire(self, args):
-        ""'''Usage: retire designator[,designator]*
+        ''"""Usage: retire designator[,designator]*
         Retire the node specified by designator.
 
         This action indicates that a particular node is not to be retrieved
         by the list or find commands, and its key value may be re-used.
-        '''
+        """
         if len(args) < 1:
             raise UsageError, _('Not enough arguments supplied')
         designators = args[0].split(',')
@@ -1044,11 +1041,11 @@
         return 0
 
     def do_restore(self, args):
-        ""'''Usage: restore designator[,designator]*
+        ''"""Usage: restore designator[,designator]*
         Restore the retired node specified by designator.
 
         The given nodes will become available for users again.
-        '''
+        """
         if len(args) < 1:
             raise UsageError, _('Not enough arguments supplied')
         designators = args[0].split(',')
@@ -1067,7 +1064,7 @@
         return 0
 
     def do_export(self, args, export_files=True):
-        ""'''Usage: export [[-]class[,class]] export_dir
+        ''"""Usage: export [[-]class[,class]] export_dir
         Export the database to colon-separated-value files.
         To exclude the files (e.g. for the msg or file class),
         use the exporttables command.
@@ -1078,7 +1075,7 @@
         This action exports the current data from the database into
         colon-separated-value files that are placed in the nominated
         destination directory.
-        '''
+        """
         # grab the directory to export to
         if len(args) < 1:
             raise UsageError, _('Not enough arguments supplied')
@@ -1142,7 +1139,7 @@
         return 0
 
     def do_exporttables(self, args):
-        ""'''Usage: exporttables [[-]class[,class]] export_dir
+        ''"""Usage: exporttables [[-]class[,class]] export_dir
         Export the database to colon-separated-value files, excluding the
         files below $TRACKER_HOME/db/files/ (which can be archived separately).
         To include the files, use the export command.
@@ -1153,11 +1150,11 @@
         This action exports the current data from the database into
         colon-separated-value files that are placed in the nominated
         destination directory.
-        '''
+        """
         return self.do_export(args, export_files=False)
 
     def do_import(self, args):
-        ""'''Usage: import import_dir
+        ''"""Usage: import import_dir
         Import a database from the directory containing CSV files,
         two per class to import.
 
@@ -1175,7 +1172,7 @@
         The new nodes are added to the existing database - if you want to
         create a new database using the imported data, then create a new
         database (or, tediously, retire all the old data.)
-        '''
+        """
         if len(args) < 1:
             raise UsageError, _('Not enough arguments supplied')
         from roundup import hyperdb
@@ -1232,7 +1229,7 @@
         return 0
 
     def do_pack(self, args):
-        ""'''Usage: pack period | date
+        ''"""Usage: pack period | date
 
         Remove journal entries older than a period of time specified or
         before a certain date.
@@ -1248,16 +1245,16 @@
         Date format is "YYYY-MM-DD" eg:
             2001-01-01
 
-        '''
+        """
         if len(args) <> 1:
             raise UsageError, _('Not enough arguments supplied')
 
         # are we dealing with a period or a date
         value = args[0]
-        date_re = re.compile(r'''
+        date_re = re.compile(r"""
               (?P<date>\d\d\d\d-\d\d?-\d\d?)? # yyyy-mm-dd
               (?P<period>(\d+y\s*)?(\d+m\s*)?(\d+d\s*)?)?
-              ''', re.VERBOSE)
+              """, re.VERBOSE)
         m = date_re.match(value)
         if not m:
             raise ValueError, _('Invalid format')
@@ -1271,12 +1268,12 @@
         return 0
 
     def do_reindex(self, args, desre=re.compile('([A-Za-z]+)([0-9]+)')):
-        ""'''Usage: reindex [classname|designator]*
+        ''"""Usage: reindex [classname|designator]*
         Re-generate a tracker's search indexes.
 
         This will re-generate the search indexes for a tracker.
         This will typically happen automatically.
-        '''
+        """
         if args:
             for arg in args:
                 m = desre.match(arg)
@@ -1295,9 +1292,9 @@
         return 0
 
     def do_security(self, args):
-        ""'''Usage: security [Role name]
+        ''"""Usage: security [Role name]
         Display the Permissions available to one or all Roles.
-        '''
+        """
         if len(args) == 1:
             role = args[0]
             try:
@@ -1335,7 +1332,7 @@
 
 
     def do_migrate(self, args):
-        '''Usage: migrate
+        ''"""Usage: migrate
         Update a tracker's database to be compatible with the Roundup
         codebase.
 
@@ -1352,7 +1349,7 @@
 
         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
@@ -1361,8 +1358,8 @@
         return 0
 
     def run_command(self, args):
-        '''Run a single command
-        '''
+        """Run a single command
+        """
         command = args[0]
 
         # handle help now
@@ -1443,8 +1440,8 @@
         return ret
 
     def interactive(self):
-        '''Run in an interactive mode
-        '''
+        """Run in an interactive mode
+        """
         print _('Roundup %s ready for input.\nType "help" for help.'
             % roundup_version)
         try:

Added: tracker/roundup-src/roundup/anypy/README.txt
==============================================================================
--- (empty file)
+++ tracker/roundup-src/roundup/anypy/README.txt	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,57 @@
+roundup.anypy package - Python version compatibility layer
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Roundup currently supports Python 2.3 to 2.6; however, some modules
+have been introduced, while others have been deprecated.  The modules
+in this package provide the functionalities which are used by Roundup
+
+- adapting the most recent Python usage
+- using new built-in functionality
+- avoiding deprecation warnings
+
+Use the modules in this package to preserve Roundup's compatibility.
+
+sets_: sets compatibility module
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Since Python 2.4, there is a built-in type 'set'; therefore, the 'sets'
+module is deprecated since version 2.6.  As far as Roundup is concerned,
+the usage is identical; see 
+http://docs.python.org/library/sets.html#comparison-to-the-built-in-set-types
+
+Uses the built-in type 'set' if available, and thus avoids
+deprecation warnings. Simple usage:
+
+Change all::
+  from sets import Set
+
+to::
+  from roundup.anypy.sets_ import set
+
+and use 'set' instead of 'Set' (or sets.Set, respectively).
+To avoid unnecessary imports, you can::
+
+  try:
+      set
+  except NameError:
+      from roundup.anypy.sets_ import set
+
+hashlib_: md5/sha/hashlib compatibility
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The md5 and sha modules are deprecated since Python 2.6; the hashlib
+module, introduced with Python 2.5, is recommended instead.
+
+Change all::
+  import md5
+  md5.md5(), md5.new()
+  import sha
+  sha.sha(), sha.new()
+
+to::
+  from roundup.anypy.hashlib_ import md5
+  md5()
+  from roundup.anypy.hashlib_ import sha1
+  sha1()
+
+# vim: si

Added: tracker/roundup-src/roundup/anypy/TODO.txt
==============================================================================
--- (empty file)
+++ tracker/roundup-src/roundup/anypy/TODO.txt	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,8 @@
+Python compatiblity TODO
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+- the popen2 module is deprecated as of Python 2.6;
+  the subprocess module is available since Python 2.4,
+  thus a roundup.anypy.subprocess_ module is needed
+
+# vim: si

Added: tracker/roundup-src/roundup/anypy/__init__.py
==============================================================================
--- (empty file)
+++ tracker/roundup-src/roundup/anypy/__init__.py	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,7 @@
+"""
+roundup.anypy - compatibility layer for any Python 2.3+
+"""
+VERSION = '.'.join(map(str,
+                       (0,
+                        1,  # hashlib_, sets_
+                        )))

Added: tracker/roundup-src/roundup/anypy/hashlib_.py
==============================================================================
--- (empty file)
+++ tracker/roundup-src/roundup/anypy/hashlib_.py	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,11 @@
+"""
+anypy.hashlib_: encapsulation of hashlib/md5/sha1/sha
+"""
+
+try:
+    from hashlib import md5, sha1 # new in Python 2.5
+except ImportError:
+    from md5 import md5           # deprecated in Python 2.6
+    from sha import sha as sha1   # deprecated in Python 2.6
+
+# vim: ts=8 sts=4 sw=4 si

Added: tracker/roundup-src/roundup/anypy/sets_.py
==============================================================================
--- (empty file)
+++ tracker/roundup-src/roundup/anypy/sets_.py	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,30 @@
+"""
+anypy.sets_: sets compatibility module
+
+uses the built-in type 'set' if available, and thus avoids
+deprecation warnings. Simple usage:
+
+Change all
+    from sets import Set
+to
+    from roundup.anypy.sets_ import set
+
+and use 'set' instead of 'Set'.
+To avoid unnecessary imports, you can:
+
+    try:
+        set
+    except NameError:
+        from roundup.anypy.sets_ import set
+
+see:
+http://docs.python.org/library/sets.html#comparison-to-the-built-in-set-types
+
+"""
+
+try:
+    set = set                     # built-in since Python 2.4
+except NameError, TypeError:
+    from sets import Set as set   # deprecated as of Python 2.6
+
+# vim: ts=8 sts=4 sw=4 si et

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 15 22:43:30 2009
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-# $Id: __init__.py,v 1.40 2007/11/07 20:47:12 richard Exp $
+# $Id: __init__.py,v 1.40 2007-11-07 20:47:12 richard Exp $
 
 '''Container for the hyperdb storage backend implementations.
 '''

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 15 22:43:30 2009
@@ -15,12 +15,11 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-#$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
+"""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
 serious bugs, and is not available)
-'''
+"""
 __docformat__ = 'restructuredtext'
 
 try:
@@ -62,16 +61,16 @@
 # Now the database
 #
 class Database(FileStorage, hyperdb.Database, roundupdb.Database):
-    '''A database for storing records containing flexible data types.
+    """A database for storing records containing flexible data types.
 
     Transaction stuff TODO:
 
     - check the timestamp of the class file and nuke the cache if it's
       modified. Do some sort of conflict checking on the dirty stuff.
     - perhaps detect write collisions (related to above)?
-    '''
+    """
     def __init__(self, config, journaltag=None):
-        '''Open a hyperdatabase given a specifier to some storage.
+        """Open a hyperdatabase given a specifier to some storage.
 
         The 'storagelocator' is obtained from config.DATABASE.
         The meaning of 'storagelocator' depends on the particular
@@ -84,7 +83,7 @@
         None, the database is opened in read-only mode: the Class.create(),
         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
@@ -107,8 +106,8 @@
         self.lockfile.flush()
 
     def post_init(self):
-        '''Called once the schema initialisation has finished.
-        '''
+        """Called once the schema initialisation has finished.
+        """
         # reindex the db if necessary
         if self.indexer.should_reindex():
             self.reindex()
@@ -146,7 +145,7 @@
     # 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
@@ -166,16 +165,16 @@
             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:
@@ -185,8 +184,8 @@
     # Class DBs
     #
     def clear(self):
-        '''Delete all database contents
-        '''
+        """Delete all database contents
+        """
         logging.getLogger('hyperdb').info('clear')
         for cn in self.classes.keys():
             for dummy in 'nodes', 'journals':
@@ -203,14 +202,14 @@
             os.remove(path+'.db')
 
     def getclassdb(self, classname, mode='r'):
-        ''' grab a connection to the class db that will be used for
+        """ grab a connection to the class db that will be used for
             multiple actions
-        '''
+        """
         return self.opendb('nodes.%s'%classname, mode)
 
     def determine_db_type(self, path):
-        ''' determine which DB wrote the class file
-        '''
+        """ determine which DB wrote the class file
+        """
         db_type = ''
         if os.path.exists(path):
             db_type = whichdb.whichdb(path)
@@ -224,9 +223,9 @@
         return db_type
 
     def opendb(self, name, mode):
-        '''Low-level database opener that gets around anydbm/dbm
+        """Low-level database opener that gets around anydbm/dbm
            eccentricities.
-        '''
+        """
         # figure the class db type
         path = os.path.join(os.getcwd(), self.dir, name)
         db_type = self.determine_db_type(path)
@@ -253,8 +252,8 @@
     # Node IDs
     #
     def newid(self, classname):
-        ''' Generate a new id for the given class
-        '''
+        """ Generate a new id for the given class
+        """
         # open the ids DB - create if if doesn't exist
         db = self.opendb('_ids', 'c')
         if db.has_key(classname):
@@ -267,8 +266,8 @@
         return newid
 
     def setid(self, classname, setid):
-        ''' Set the id counter: used during import of database
-        '''
+        """ Set the id counter: used during import of database
+        """
         # open the ids DB - create if if doesn't exist
         db = self.opendb('_ids', 'c')
         db[classname] = str(setid)
@@ -278,8 +277,8 @@
     # Nodes
     #
     def addnode(self, classname, nodeid, node):
-        ''' add the specified node to its class's db
-        '''
+        """ add the specified node to its class's db
+        """
         # we'll be supplied these props if we're doing an import
         if not node.has_key('creator'):
             # add in the "calculated" properties (dupe so we don't affect
@@ -294,8 +293,8 @@
         self.savenode(classname, nodeid, node)
 
     def setnode(self, classname, nodeid, node):
-        ''' change the specified node
-        '''
+        """ change the specified node
+        """
         self.dirtynodes.setdefault(classname, {})[nodeid] = 1
 
         # can't set without having already loaded the node
@@ -303,18 +302,18 @@
         self.savenode(classname, nodeid, node)
 
     def savenode(self, classname, nodeid, node):
-        ''' perform the saving of data specified by the set/addnode
-        '''
+        """ perform the saving of data specified by the set/addnode
+        """
         if __debug__:
             logging.getLogger('hyperdb').debug('save %s%s %r'%(classname, nodeid, node))
         self.transactions.append((self.doSaveNode, (classname, nodeid, node)))
 
     def getnode(self, classname, nodeid, db=None, cache=1):
-        ''' get a node from the database
+        """ get a node from the database
 
             Note the "cache" parameter is not used, and exists purely for
             backward compatibility!
-        '''
+        """
         # try the cache
         cache_dict = self.cache.setdefault(classname, {})
         if cache_dict.has_key(nodeid):
@@ -355,9 +354,9 @@
         return res
 
     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('destroy %s%s'%(classname, nodeid))
 
         # remove from cache and newnodes if it's there
@@ -381,9 +380,9 @@
         self.transactions.append((FileStorage.destroy, (self, classname, nodeid)))
 
     def serialise(self, classname, node):
-        '''Copy the node contents, converting non-marshallable data into
+        """Copy the node contents, converting non-marshallable data into
            marshallable data.
-        '''
+        """
         properties = self.getclass(classname).getprops()
         d = {}
         for k, v in node.items():
@@ -409,8 +408,8 @@
         return d
 
     def unserialise(self, classname, node):
-        '''Decode the marshalled node data
-        '''
+        """Decode the marshalled node data
+        """
         properties = self.getclass(classname).getprops()
         d = {}
         for k, v in node.items():
@@ -436,8 +435,8 @@
         return d
 
     def hasnode(self, classname, nodeid, db=None):
-        ''' determine if the database has a given node
-        '''
+        """ determine if the database has a given node
+        """
         # try the cache
         cache = self.cache.setdefault(classname, {})
         if cache.has_key(nodeid):
@@ -474,7 +473,7 @@
     #
     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
@@ -483,7 +482,7 @@
 
             'creator' -- the user performing the action, which defaults to
             the current user.
-        '''
+        """
         if __debug__:
             logging.getLogger('hyperdb').debug('addjournal %s%s %s %r %s %r'%(classname,
                 nodeid, action, params, creator, creation))
@@ -493,7 +492,7 @@
             action, params, creator, creation)))
 
     def setjournal(self, classname, nodeid, journal):
-        '''Set the journal to the "journal" list.'''
+        """Set the journal to the "journal" list."""
         if __debug__:
             logging.getLogger('hyperdb').debug('setjournal %s%s %r'%(classname,
                 nodeid, journal))
@@ -501,11 +500,11 @@
             journal)))
 
     def getjournal(self, classname, nodeid):
-        ''' get the journal for id
+        """ get the journal for id
 
             Raise IndexError if the node doesn't exist (as per history()'s
             API)
-        '''
+        """
         # our journal result
         res = []
 
@@ -554,8 +553,8 @@
         return res
 
     def pack(self, pack_before):
-        ''' Delete all journal entries except "create" before 'pack_before'.
-        '''
+        """ Delete all journal entries except "create" before 'pack_before'.
+        """
         pack_before = pack_before.serialise()
         for classname in self.getclasses():
             packed = 0
@@ -594,7 +593,7 @@
     # Basic transaction support
     #
     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().
@@ -604,7 +603,7 @@
         database. We don't care if there's a concurrency issue there.
 
         The only backend this seems to affect is postgres.
-        '''
+        """
         logging.getLogger('hyperdb').info('commit %s transactions'%(
             len(self.transactions)))
 
@@ -645,8 +644,8 @@
         self.transactions = []
 
     def getCachedClassDB(self, classname):
-        ''' get the class db, looking in our cache of databases for commit
-        '''
+        """ get the class db, looking in our cache of databases for commit
+        """
         # get the database handle
         db_name = 'nodes.%s'%classname
         if not self.databases.has_key(db_name):
@@ -663,8 +662,8 @@
         return (classname, nodeid)
 
     def getCachedJournalDB(self, classname):
-        ''' get the journal db, looking in our cache of databases for commit
-        '''
+        """ get the journal db, looking in our cache of databases for commit
+        """
         # get the database handle
         db_name = 'journals.%s'%classname
         if not self.databases.has_key(db_name):
@@ -726,8 +725,8 @@
             del db[nodeid]
 
     def rollback(self):
-        ''' Reverse all actions from the current transaction.
-        '''
+        """ Reverse all actions from the current transaction.
+        """
         logging.getLogger('hyperdb').info('rollback %s transactions'%(
             len(self.transactions)))
 
@@ -742,8 +741,8 @@
         self.transactions = []
 
     def close(self):
-        ''' Nothing to do
-        '''
+        """ Nothing to do
+        """
         if self.lockfile is not None:
             locking.release_lock(self.lockfile)
             self.lockfile.close()
@@ -751,22 +750,22 @@
 
 _marker = []
 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."""
 
     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.
 
@@ -784,15 +783,15 @@
 
         These operations trigger detectors and can be vetoed.  Attempts
         to modify the "creation" or "activity" properties cause a KeyError.
-        '''
+        """
         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'
 
@@ -926,7 +925,7 @@
         return 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
@@ -936,7 +935,7 @@
 
         Attempts to get the "creation" or "activity" properties should
         do the right thing.
-        '''
+        """
         if propname == 'id':
             return nodeid
 
@@ -1026,7 +1025,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.
@@ -1045,7 +1044,7 @@
 
         These operations trigger detectors and can be vetoed.  Attempts
         to modify the "creation" or "activity" properties cause a KeyError.
-        '''
+        """
         self.fireAuditors('set', nodeid, propvalues)
         oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid))
         for name,prop in self.getprops(protected=0).items():
@@ -1060,8 +1059,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
 
@@ -1257,7 +1256,7 @@
         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.
@@ -1267,7 +1266,7 @@
 
         These operations trigger detectors and can be vetoed.  Attempts
         to modify the "creation" or "activity" properties cause a KeyError.
-        '''
+        """
         if self.db.journaltag is None:
             raise hyperdb.DatabaseError, _('Database open read-only')
 
@@ -1282,10 +1281,10 @@
         self.fireReactors('retire', nodeid, None)
 
     def restore(self, nodeid):
-        '''Restpre a retired node.
+        """Restpre 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')
 
@@ -1309,15 +1308,15 @@
         self.fireReactors('restore', nodeid, None)
 
     def is_retired(self, nodeid, cldb=None):
-        '''Return true if the node is retired.
-        '''
+        """Return true if the node is retired.
+        """
         node = self.db.getnode(self.classname, nodeid, cldb)
         if node.has_key(self.db.RETIRED_FLAG):
             return 1
         return 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
@@ -1331,13 +1330,13 @@
 
         Well, I think that's enough warnings. This method exists mostly to
         support the session storage of the cgi interface.
-        '''
+        """
         if self.db.journaltag is None:
             raise hyperdb.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.
@@ -1348,43 +1347,43 @@
 
         '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. If the
         property doesn't exist, KeyError is raised.
-        '''
+        """
         prop = self.getprops()[propname]
         if not isinstance(prop, hyperdb.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
 
     # TODO: set up a separate index db file for this? profile?
     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
         cldb = self.db.getclassdb(self.classname)
@@ -1404,7 +1403,7 @@
 
     # change from spec - allows multiple props to match
     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, }
@@ -1417,7 +1416,7 @@
 
             db.issue.find(messages='1')
             db.issue.find(messages={'1':1,'3':1}, files={'7':1})
-        '''
+        """
         propspec = propspec.items()
         for propname, itemids in propspec:
             # check the prop is OK
@@ -1464,13 +1463,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.
-        '''
+        """
         for propname in requirements.keys():
             prop = self.properties[propname]
             if not isinstance(prop, hyperdb.String):
@@ -1495,8 +1494,8 @@
         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.
+        """
         l = []
         cn = self.classname
         cldb = self.db.getclassdb(cn)
@@ -1512,11 +1511,11 @@
         return l
 
     def getnodeids(self, db=None, retired=None):
-        ''' Return a list of ALL nodeids
+        """ Return a list of ALL nodeids
 
             Set retired=None to get all nodes. Otherwise it'll get all the
             retired or non-retired nodes, depending on the flag.
-        '''
+        """
         res = []
 
         # start off with the new nodes
@@ -1561,7 +1560,7 @@
         "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
+        "search_matches" is a sequence type or None
 
         The filter must match all properties specificed. If the property
         value to match is a list:
@@ -1721,7 +1720,7 @@
             if search_matches is not None:
                 k = []
                 for v in matches:
-                    if search_matches.has_key(v[0]):
+                    if v[0] in search_matches:
                         k.append(v)
                 matches = k
 
@@ -1811,18 +1810,18 @@
         return matches
 
     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.
 
@@ -1830,7 +1829,7 @@
            methods provide the "creation" and "activity" properties. If the
            "protected" flag is true, we include protected properties - those
            which may not be modified.
-        '''
+        """
         d = self.properties.copy()
         if protected:
             d['id'] = hyperdb.String()
@@ -1841,20 +1840,20 @@
         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, hyperdb.String) and propclass.indexme:
@@ -1870,9 +1869,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:
@@ -1895,13 +1894,13 @@
         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 hyperdb.DatabaseError, _('Database open read-only')
         properties = self.getprops()
@@ -1949,13 +1948,13 @@
         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():
@@ -1989,9 +1988,9 @@
         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:
@@ -2021,18 +2020,18 @@
             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'):
@@ -2040,8 +2039,8 @@
         Class.__init__(self, db, classname, **properties)
 
     def create(self, **propvalues):
-        ''' Snarf the "content" propvalue and store in a file
-        '''
+        """ Snarf the "content" 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)
@@ -2056,18 +2055,19 @@
         # do the database create
         newid = self.create_inner(**propvalues)
 
+        # store off the content as a file
+        self.db.storefile(self.classname, newid, None, content)
+
         # fire reactors
         self.fireReactors('create', newid, None)
 
-        # store off the content as a file
-        self.db.storefile(self.classname, newid, None, content)
         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 an access right configuration problem.'
         if propname == 'content':
             try:
@@ -2082,8 +2082,8 @@
             return Class.get(self, nodeid, propname)
 
     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)
 
         # create the oldvalues dict - fill in any missing values
@@ -2120,10 +2120,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:
@@ -2143,11 +2143,11 @@
 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" or "activity" property, a ValueError is raised.
-        '''
+        """
         if not properties.has_key('title'):
             properties['title'] = hyperdb.String(indexme='yes')
         if not properties.has_key('messages'):

Deleted: tracker/roundup-src/roundup/backends/back_metakit.py
==============================================================================

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 15 22:43:30 2009
@@ -1,4 +1,3 @@
-#$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>
 #
@@ -67,11 +66,10 @@
             # stupid MySQL bug requires us to drop all the tables first
             for table in tables:
                 command = 'DROP TABLE `%s`'%table[0]
-                if __debug__:
-                    logging.getLogger('hyperdb').debug(command)
+                logging.debug(command)
                 cursor.execute(command)
             command = "DROP DATABASE %s"%config.RDBMS_NAME
-            logging.getLogger('hyperdb').info(command)
+            logging.info(command)
             cursor.execute(command)
             conn.commit()
         conn.close()
@@ -85,7 +83,7 @@
     conn = MySQLdb.connect(**kwargs)
     cursor = conn.cursor()
     command = "CREATE DATABASE %s"%config.RDBMS_NAME
-    logging.getLogger('hyperdb').info(command)
+    logging.info(command)
     cursor.execute(command)
     conn.commit()
     conn.close()
@@ -140,7 +138,7 @@
 
     def sql_open_connection(self):
         kwargs = connection_dict(self.config, 'db')
-        logging.getLogger('hyperdb').info('open database %r'%(kwargs['db'],))
+        self.log_info('open database %r'%(kwargs['db'],))
         try:
             conn = MySQLdb.connect(**kwargs)
         except MySQLdb.OperationalError, message:
@@ -299,7 +297,7 @@
                     # convert to new MySQL data type
                     prop = properties[name]
                     if v is not None:
-                        e = self.hyperdb_to_sql_value[prop.__class__](v)
+                        e = self.to_sql_value(prop.__class__)(v)
                     else:
                         e = None
                     l.append(e)
@@ -351,7 +349,7 @@
 
             # re-create journal table
             self.create_journal_table(klass)
-            dc = self.hyperdb_to_sql_value[hyperdb.Date]
+            dc = self.to_sql_value(hyperdb.Date)
             for nodeid, journaldate, journaltag, action, params in olddata:
                 self.save_journal(cn, cols, nodeid, dc(journaldate),
                     journaltag, action, params)
@@ -421,9 +419,19 @@
         # 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):
+        # mysql requires sizes on TEXT indexes
+        prop = self.classes[cn].getprops()[key]
+        if isinstance(prop, String):
+            sql = '''create unique index _%s_key_retired_idx
+                on _%s(__retired__, _%s(255))'''%(cn, cn, key)
+        else:
+            sql = '''create unique index _%s_key_retired_idx
+                on _%s(__retired__, _%s)'''%(cn, cn, key)
+        self.sql(sql)
+
     def create_class_table_key_index(self, cn, key):
-        ''' create the class table for the given spec
-        '''
+        # mysql requires sizes on TEXT indexes
         prop = self.classes[cn].getprops()[key]
         if isinstance(prop, String):
             sql = 'create index _%s_%s_idx on _%s(_%s(255))'%(cn, key, cn, key)
@@ -534,7 +542,7 @@
     def sql_commit(self, fail_ok=False):
         ''' Actually commit to the database.
         '''
-        logging.getLogger('hyperdb').info('commit')
+        self.log_info('commit')
 
         # MySQL commits don't seem to ever fail, the latest update winning.
         # makes you wonder why they have transactions...
@@ -548,7 +556,7 @@
         self.sql("START TRANSACTION")
 
     def sql_close(self):
-        logging.getLogger('hyperdb').info('close')
+        self.log_info('close')
         try:
             self.conn.close()
         except MySQLdb.ProgrammingError, message:

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 15 22:43:30 2009
@@ -1,4 +1,4 @@
-#$Id: back_postgresql.py,v 1.43 2007/09/28 15:15:06 jpend Exp $
+#$Id: back_postgresql.py,v 1.44 2008-08-07 05:50:03 richard Exp $
 #
 # Copyright (c) 2003 Martynas Sklyzmantas, Andrey Lebedev <andrey at micro.lt>
 #
@@ -177,7 +177,7 @@
         self.sql('''CREATE TABLE __textids (
             _textid integer primary key, _class VARCHAR(255),
             _itemid VARCHAR(255), _prop VARCHAR(255))''')
-        self.sql('''CREATE TABLE __words (_word VARCHAR(30), 
+        self.sql('''CREATE TABLE __words (_word VARCHAR(30),
             _textid integer)''')
         self.sql('CREATE INDEX words_word_idx ON __words(_word)')
         self.sql('CREATE INDEX words_by_id ON __words (_textid)')
@@ -261,7 +261,7 @@
     def newid(self, classname):
         sql = "select nextval('_%s_ids') from dual"%classname
         self.sql(sql)
-        return self.cursor.fetchone()[0]
+        return str(self.cursor.fetchone()[0])
 
     def setid(self, classname, setid):
         sql = "select setval('_%s_ids', %s) from dual"%(classname, int(setid))

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 15 22:43:30 2009
@@ -1,12 +1,11 @@
-# $Id: back_sqlite.py,v 1.51 2007/06/21 07:35:50 schlatterbeck Exp $
-'''Implements a backend for SQLite.
+"""Implements a backend for SQLite.
 
 See https://pysqlite.sourceforge.net/ for pysqlite info
 
 
 NOTE: we use the rdbms_common table creation methods which define datatypes
 for the columns, but sqlite IGNORES these specifications.
-'''
+"""
 __docformat__ = 'restructuredtext'
 
 import os, base64, marshal, shutil, time, logging
@@ -90,10 +89,10 @@
         return 1
 
     def sql_open_connection(self):
-        '''Open a standard, non-autocommitting connection.
+        """Open a standard, non-autocommitting connection.
 
         pysqlite will automatically BEGIN TRANSACTION for us.
-        '''
+        """
         # make sure the database directory exists
         # database itself will be created by sqlite if needed
         if not os.path.isdir(self.config.DATABASE):
@@ -109,6 +108,13 @@
         else:
             conn = sqlite.connect(db, timeout=30)
             conn.row_factory = sqlite.Row
+
+        # sqlite3 wants us to store Unicode in the db but that's not what's
+        # been done historically and it's definitely not what the other
+        # backends do, so we'll stick with UTF-8
+        if sqlite_version == 3:
+            conn.text_factory = str
+
         cursor = conn.cursor()
         return (conn, cursor)
 
@@ -165,14 +171,14 @@
         pass
 
     def update_class(self, spec, old_spec, force=0, adding_v2=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.
 
             SQLite doesn't have ALTER TABLE, so we have to copy and
             regenerate the tables with the new schema.
-        '''
+        """
         new_has = spec.properties.has_key
         new_spec = spec.schema()
         new_spec[1].sort()
@@ -222,8 +228,8 @@
 
                     # re-create and populate the new table
                     self.create_multilink_table(spec, propname)
-                    sql = '''insert into %s (linkid, nodeid) values
-                        (%s, %s)'''%(tn, self.arg, self.arg)
+                    sql = """insert into %s (linkid, nodeid) values
+                        (%s, %s)"""%(tn, self.arg, self.arg)
                     for linkid, nodeid in rows:
                         self.sql(sql, (int(linkid), int(nodeid)))
             elif old_has(propname):
@@ -296,9 +302,9 @@
         return 1
 
     def sql_close(self):
-        ''' Squash any error caused by us already having closed the
+        """ Squash any error caused by us already having closed the
             connection.
-        '''
+        """
         try:
             self.conn.close()
         except sqlite.ProgrammingError, value:
@@ -306,9 +312,9 @@
                 raise
 
     def sql_rollback(self):
-        ''' Squash any error caused by us having closed the connection (and
+        """ Squash any error caused by us having closed the connection (and
             therefore not having anything to roll back)
-        '''
+        """
         try:
             self.conn.rollback()
         except sqlite.ProgrammingError, value:
@@ -319,10 +325,10 @@
         return '<roundlite 0x%x>'%id(self)
 
     def sql_commit(self, fail_ok=False):
-        ''' Actually commit to the database.
+        """ Actually commit to the database.
 
             Ignore errors if there's nothing to commit.
-        '''
+        """
         try:
             self.conn.commit()
         except sqlite.DatabaseError, error:
@@ -340,8 +346,8 @@
 
     # old-skool id generation
     def newid(self, classname):
-        ''' Generate a new id for the given class
-        '''
+        """ Generate a new id for the given class
+        """
         # get the next ID
         sql = 'select num from ids where name=%s'%self.arg
         self.sql(sql, (classname, ))
@@ -356,10 +362,10 @@
         return str(newid)
 
     def setid(self, classname, setid):
-        ''' Set the id counter: used during import of database
+        """ Set the id counter: used during import of database
 
         We add one to make it behave like the sequences in postgres.
-        '''
+        """
         sql = 'update ids set num=%s where name=%s'%(self.arg, self.arg)
         vals = (int(setid)+1, classname)
         self.sql(sql, vals)
@@ -378,8 +384,8 @@
 
     if sqlite_version in (2,3):
         def load_journal(self, classname, cols, nodeid):
-            '''We need to turn the sqlite3.Row into a tuple so it can be
-            unpacked'''
+            """We need to turn the sqlite3.Row into a tuple so it can be
+            unpacked"""
             l = rdbms_common.Database.load_journal(self,
                 classname, cols, nodeid)
             cols = range(5)
@@ -388,9 +394,9 @@
 class sqliteClass:
     def filter(self, search_matches, filterspec, sort=(None,None),
             group=(None,None)):
-        ''' If there's NO matches to a fetch, sqlite returns NULL
+        """ If there's NO matches to a fetch, sqlite returns NULL
             instead of nothing
-        '''
+        """
         return filter(None, rdbms_common.Class.filter(self, search_matches,
             filterspec, sort=sort, group=group))
 

Modified: tracker/roundup-src/roundup/backends/back_tsearch2.py
==============================================================================
--- tracker/roundup-src/roundup/backends/back_tsearch2.py	(original)
+++ tracker/roundup-src/roundup/backends/back_tsearch2.py	Sun Mar 15 22:43:30 2009
@@ -1,4 +1,4 @@
-#$Id: back_tsearch2.py,v 1.9 2005/01/08 16:16:59 jlgijsbers Exp $
+#$Id: back_tsearch2.py,v 1.9 2005-01-08 16:16:59 jlgijsbers Exp $
 
 # Note: this backend is EXPERIMENTAL. Do not use if you value your data.
 import re

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 15 22:43:30 2009
@@ -15,10 +15,9 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-#$Id: blobfiles.py,v 1.24 2008/02/07 00:57:59 richard Exp $
-'''This module exports file storage for roundup backends.
+"""This module exports file storage for roundup backends.
 Files are stored into a directory hierarchy.
-'''
+"""
 __docformat__ = 'restructuredtext'
 
 import os
@@ -232,14 +231,28 @@
 
         return filename + self.tempext
 
+    def _editInProgress(self, classname, nodeid, property):
+        """Return true if the file indicated is being edited.
+
+        returns -- True if the current transaction includes an edit to
+        the file indicated."""
+
+        for method, args in self.transactions:
+            if (method == self.doStoreFile and
+                args == (classname, nodeid, property)):
+                return True
+
+        return False
+    
+
     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
         usual place, or it could be in a temp file pre-commit *or* it
         could be in an old-style, backwards-compatible flat directory.
-        '''
+        """
         filename  = os.path.join(self.dir, 'files', classname,
                                  self.subdirFilename(classname, nodeid, property))
         # If the caller is going to create the file, return the
@@ -252,13 +265,10 @@
 
         # 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 self._editInProgress(classname, nodeid, property):
+            if not os.path.exists(tempfile):
+                raise IOError('content file for %s not found'%tempfile)
+            return tempfile
 
         if os.path.exists(filename):
             return filename
@@ -295,10 +305,10 @@
         raise IOError('content file for %s not found'%filename)
 
     def storefile(self, classname, nodeid, property, content):
-        '''Store the content of the file in the database. The property may be
+        """Store the content of the file in the database. The property may be
            None, in which case the filename does not indicate which property
            is being saved.
-        '''
+        """
         # determine the name of the file to write to
         name = self.filename(classname, nodeid, property, create=1)
 
@@ -310,7 +320,7 @@
         name = self._tempfile(name)
 
         # make sure we don't register the rename action more than once
-        if not os.path.exists(name):
+        if not self._editInProgress(classname, nodeid, property):
             # save off the rename action
             self.transactions.append((self.doStoreFile, (classname, nodeid,
                 property)))
@@ -321,8 +331,8 @@
         open(name, 'wb').write(content)
 
     def getfile(self, classname, nodeid, property):
-        '''Get the content of the file in the database.
-        '''
+        """Get the content of the file in the database.
+        """
         filename = self.filename(classname, nodeid, property)
 
         f = open(filename, 'rb')
@@ -333,14 +343,14 @@
             f.close()
 
     def numfiles(self):
-        '''Get number of files in storage, even across subdirectories.
-        '''
+        """Get number of files in storage, even across subdirectories.
+        """
         files_dir = os.path.join(self.dir, 'files')
         return files_in_dir(files_dir)
 
     def doStoreFile(self, classname, nodeid, property, **databases):
-        '''Store the file as part of a transaction commit.
-        '''
+        """Store the file as part of a transaction commit.
+        """
         # determine the name of the file to write to
         name = self.filename(classname, nodeid, property, 1)
 
@@ -364,8 +374,8 @@
         return (classname, nodeid)
 
     def rollbackStoreFile(self, classname, nodeid, property, **databases):
-        '''Remove the temp file as a part of a rollback
-        '''
+        """Remove the temp file as a part of a rollback
+        """
         # determine the name of the file to delete
         name = self.filename(classname, nodeid, property)
         if not name.endswith(self.tempext):
@@ -373,9 +383,9 @@
         os.remove(name)
 
     def isStoreFile(self, classname, nodeid):
-        '''See if there is actually any FileStorage for this node.
+        """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
@@ -383,9 +393,9 @@
             return False
 
     def destroy(self, classname, nodeid):
-        '''If there is actually FileStorage for this node
+        """If there is actually FileStorage for this node
            remove it from the filesystem
-        '''
+        """
         if self.isStoreFile(classname, nodeid):
             os.remove(self.filename(classname, nodeid))
 

Modified: tracker/roundup-src/roundup/backends/indexer_common.py
==============================================================================
--- tracker/roundup-src/roundup/backends/indexer_common.py	(original)
+++ tracker/roundup-src/roundup/backends/indexer_common.py	Sun Mar 15 22:43:30 2009
@@ -1,5 +1,7 @@
-#$Id: indexer_common.py,v 1.8 2006/11/11 03:01:54 richard Exp $
-import re, sets
+#$Id: indexer_common.py,v 1.11 2008-09-11 19:41:07 schlatterbeck Exp $
+import re
+# Python 2.3 ... 2.6 compatibility:
+from roundup.anypy.sets_ import set
 
 from roundup import hyperdb
 
@@ -8,7 +10,7 @@
     "FOR", "IF", "IN", "INTO", "IS", "IT",
     "NO", "NOT", "OF", "ON", "OR", "SUCH",
     "THAT", "THE", "THEIR", "THEN", "THERE", "THESE",
-    "THEY", "THIS", "TO", "WAS", "WILL", "WITH" 
+    "THEY", "THIS", "TO", "WAS", "WILL", "WITH"
 ]
 
 def _isLink(propclass):
@@ -17,7 +19,7 @@
 
 class Indexer:
     def __init__(self, db):
-        self.stopwords = sets.Set(STOPWORDS)
+        self.stopwords = set(STOPWORDS)
         for word in db.config[('main', 'indexer_stopwords')]:
             self.stopwords.add(word)
 
@@ -26,13 +28,13 @@
 
     def getHits(self, search_terms, klass):
         return self.find(search_terms)
-    
+
     def search(self, search_terms, klass, ignore={}):
-        '''Display search results looking for [search, terms] associated
+        """Display search results looking for [search, terms] associated
         with the hyperdb Class "klass". Ignore hits on {class: property}.
 
         "dre" is a helper, not an argument.
-        '''
+        """
         # do the index lookup
         hits = self.getHits(search_terms, klass)
         if not hits:
@@ -61,7 +63,9 @@
                 continue
 
             # if it's a property on klass, it's easy
-            nodeid = entry[1]
+            # (make sure the nodeid is str() not unicode() as returned by some
+            # backends as that can cause problems down the track)
+            nodeid = str(entry[1])
             if classname == klass.classname:
                 if not nodeids.has_key(nodeid):
                     nodeids[nodeid] = {}
@@ -81,14 +85,21 @@
                 del propspec[propname]
 
         # klass.find tells me the klass nodeids the linked nodes relate to
+        propdefs = klass.getprops()
         for resid in klass.find(**propspec):
             resid = str(resid)
-            if not nodeids.has_key(id):
-                nodeids[resid] = {}
+            if resid in nodeids:
+                continue # we ignore duplicate resids
+            nodeids[resid] = {}
             node_dict = nodeids[resid]
             # now figure out where it came from
             for linkprop in propspec.keys():
-                for nodeid in klass.get(resid, linkprop):
+                v = klass.get(resid, linkprop)
+                # the link might be a Link so deal with a single result or None
+                if isinstance(propdefs[linkprop], hyperdb.Link):
+                    if v is None: continue
+                    v = [v]
+                for nodeid in v:
                     if propspec[linkprop].has_key(nodeid):
                         # OK, this node[propname] has a winner
                         if not node_dict.has_key(linkprop):

Modified: tracker/roundup-src/roundup/backends/indexer_dbm.py
==============================================================================
--- tracker/roundup-src/roundup/backends/indexer_dbm.py	(original)
+++ tracker/roundup-src/roundup/backends/indexer_dbm.py	Sun Mar 15 22:43:30 2009
@@ -14,7 +14,7 @@
 #     that promote freedom, but obviously am giving up any rights
 #     to compel such.
 # 
-#$Id: indexer_dbm.py,v 1.9 2006/04/27 05:48:26 richard Exp $
+#$Id: indexer_dbm.py,v 1.9 2006-04-27 05:48:26 richard Exp $
 '''This module provides an indexer class, RoundupIndexer, that stores text
 indices in a roundup instance.  This class makes searching the content of
 messages, string properties and text files possible.

Modified: tracker/roundup-src/roundup/backends/indexer_rdbms.py
==============================================================================
--- tracker/roundup-src/roundup/backends/indexer_rdbms.py	(original)
+++ tracker/roundup-src/roundup/backends/indexer_rdbms.py	Sun Mar 15 22:43:30 2009
@@ -1,9 +1,11 @@
-#$Id: indexer_rdbms.py,v 1.15 2006/10/04 01:12:00 richard Exp $
-''' This implements the full-text indexer over two RDBMS tables. The first
+#$Id: indexer_rdbms.py,v 1.18 2008-09-01 00:43:02 richard Exp $
+""" This implements the full-text indexer over two RDBMS tables. The first
 is a mapping of words to occurance IDs. The second maps the IDs to (Class,
 propname, itemid) instances.
-'''
-import re, sets
+"""
+import re
+# Python 2.3 ... 2.6 compatibility:
+from roundup.anypy.sets_ import set
 
 from roundup.backends.indexer_common import Indexer as IndexerBase
 
@@ -14,30 +16,35 @@
         self.reindex = 0
 
     def close(self):
-        '''close the indexing database'''
+        """close the indexing database"""
         # just nuke the circular reference
         self.db = None
 
     def save_index(self):
-        '''Save the changes to the index.'''
+        """Save the changes to the index."""
         # not necessary - the RDBMS connection will handle this for us
         pass
 
     def force_reindex(self):
-        '''Force a reindexing of the database.  This essentially
+        """Force a reindexing of the database.  This essentially
         empties the tables ids and index and sets a flag so
-        that the databases are reindexed'''
+        that the databases are reindexed"""
         self.reindex = 1
 
     def should_reindex(self):
-        '''returns True if the indexes need to be rebuilt'''
+        """returns True if the indexes need to be rebuilt"""
         return self.reindex
 
     def add_text(self, identifier, text, mime_type='text/plain'):
-        ''' "identifier" is  (classname, itemid, property) '''
+        """ "identifier" is  (classname, itemid, property) """
         if mime_type != 'text/plain':
             return
 
+        # Ensure all elements of the identifier are strings 'cos the itemid
+        # column is varchar even if item ids may be numbers elsewhere in the
+        # code. ugh.
+        identifier = tuple(map(str, identifier))
+
         # first, find the id of the (classname, itemid, property)
         a = self.db.arg
         sql = 'select _textid from __textids where _class=%s and '\
@@ -58,9 +65,9 @@
 
         # ok, find all the unique words in the text
         text = unicode(text, "utf-8", "replace").upper()
-        wordlist = [w.encode("utf-8", "replace")
-                for w in re.findall(r'(?u)\b\w{2,25}\b', text)]
-        words = sets.Set()
+        wordlist = [w.encode("utf-8")
+            for w in re.findall(r'(?u)\b\w{2,25}\b', text)]
+        words = set()
         for word in wordlist:
             if self.is_stopword(word): continue
             if len(word) > 25: continue
@@ -72,17 +79,17 @@
         self.db.cursor.executemany(sql, words)
 
     def find(self, wordlist):
-        '''look up all the words in the wordlist.
+        """look up all the words in the wordlist.
         If none are found return an empty dictionary
         * more rules here
-        '''
+        """
         if not wordlist:
-            return {}
+            return []
 
         l = [word.upper() for word in wordlist if 26 > len(word) > 2]
 
         if not l:
-            return {}
+            return []
 
         if self.db.implements_intersect:
             # simple AND search
@@ -91,7 +98,7 @@
             self.db.cursor.execute(sql, tuple(l))
             r = self.db.cursor.fetchall()
             if not r:
-                return {}
+                return []
             a = ','.join([self.db.arg] * len(r))
             sql = 'select _class, _itemid, _prop from __textids '\
                 'where _textid in (%s)'%a
@@ -120,7 +127,7 @@
 
             r = map(lambda x: x[0], self.db.cursor.fetchall())
             if not r:
-                return {}
+                return []
 
             a = ','.join([self.db.arg] * len(r))
             sql = 'select _class, _itemid, _prop from __textids '\

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 15 22:43:30 2009
@@ -1,4 +1,4 @@
-#$Id: indexer_xapian.py,v 1.6 2007/10/25 07:02:42 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

Modified: tracker/roundup-src/roundup/backends/locking.py
==============================================================================
--- tracker/roundup-src/roundup/backends/locking.py	(original)
+++ tracker/roundup-src/roundup/backends/locking.py	Sun Mar 15 22:43:30 2009
@@ -19,7 +19,7 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
-# $Id: locking.py,v 1.8 2004/02/11 23:55:09 richard Exp $
+# $Id: locking.py,v 1.8 2004-02-11 23:55:09 richard Exp $
 
 '''This module provides a generic interface to acquire and release
 exclusive access to a file.

Modified: tracker/roundup-src/roundup/backends/portalocker.py
==============================================================================
--- tracker/roundup-src/roundup/backends/portalocker.py	(original)
+++ tracker/roundup-src/roundup/backends/portalocker.py	Sun Mar 15 22:43:30 2009
@@ -2,7 +2,7 @@
 #                  Requires python 1.5.2 or better.
 
 # ID line added by richard for Roundup file tracking
-# $Id: portalocker.py,v 1.9 2006/09/09 05:42:45 richard Exp $
+# $Id: portalocker.py,v 1.9 2006-09-09 05:42:45 richard Exp $
 
 """Cross-platform (posix/nt) API for flock-style file locking.
 

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 15 22:43:30 2009
@@ -15,7 +15,6 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-#$Id: rdbms_common.py,v 1.195 2008/02/07 05:01:42 richard Exp $
 """ Relational database (SQL) backend common code.
 
 Basics:
@@ -72,9 +71,6 @@
 from sessions_rdbms import Sessions, OneTimeKeys
 from roundup.date import Range
 
-# number of rows to keep in memory
-ROW_CACHE_SIZE = 100
-
 # dummy value meaning "argument not passed"
 _marker = []
 
@@ -109,7 +105,7 @@
 
         - 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.
+        - we keep a cache of the latest N row fetches (where N is configurable).
     """
     def __init__(self, config, journaltag=None):
         """ Open the database and load the schema from it.
@@ -126,6 +122,7 @@
 
         # keep a cache of the N most recently retrieved rows of any kind
         # (classname, nodeid) = row
+        self.cache_size = config.RDBMS_CACHE_SIZE
         self.cache = {}
         self.cache_lru = []
         self.stats = {'cache_hits': 0, 'cache_misses': 0, 'get_items': 0,
@@ -157,8 +154,7 @@
     def sql(self, sql, args=None):
         """ Execute the sql with the optional args.
         """
-        if __debug__:
-            logging.getLogger('hyperdb').debug('SQL %r %r'%(sql, args))
+        self.log_debug('SQL %r %r'%(sql, args))
         if args:
             self.cursor.execute(sql, args)
         else:
@@ -254,13 +250,15 @@
             Return boolean whether we need to save the schema.
         """
         version = self.database_schema.get('version', 1)
+        if version > self.current_db_version:
+            raise DatabaseError('attempting to run rev %d DATABASE with rev '
+                '%d CODE!'%(version, self.current_db_version))
         if version == self.current_db_version:
             # nothing to do
             return 0
 
         if version < 2:
-            if __debug__:
-                logging.getLogger('hyperdb').info('upgrade to version 2')
+            self.log_info('upgrade to version 2')
             # change the schema structure
             self.database_schema = {'tables': self.database_schema}
 
@@ -273,8 +271,7 @@
             self.create_version_2_tables()
 
         if version < 3:
-            if __debug__:
-                logging.getLogger('hyperdb').info('upgrade to version 3')
+            self.log_info('upgrade to version 3')
             self.fix_version_2_tables()
 
         if version < 4:
@@ -388,6 +385,19 @@
         hyperdb.Boolean   : 'BOOLEAN',
         hyperdb.Number    : 'REAL',
     }
+
+    def hyperdb_to_sql_datatype(self, propclass):
+
+        datatype = self.hyperdb_to_sql_datatypes.get(propclass)
+        if datatype:
+            return datatype
+        
+        for k, v in self.hyperdb_to_sql_datatypes.iteritems():
+            if issubclass(propclass, k):
+                return v
+
+        raise ValueError, '%r is not a hyperdb property class' % propclass
+    
     def determine_columns(self, properties):
         """ Figure the column names and multilink properties from the spec
 
@@ -395,10 +405,10 @@
             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]),
-            ('_creator', self.hyperdb_to_sql_datatypes[hyperdb.Link]),
-            ('_creation', self.hyperdb_to_sql_datatypes[hyperdb.Date]),
+            ('_actor', self.hyperdb_to_sql_datatype(hyperdb.Link)),
+            ('_activity', self.hyperdb_to_sql_datatype(hyperdb.Date)),
+            ('_creator', self.hyperdb_to_sql_datatype(hyperdb.Link)),
+            ('_creation', self.hyperdb_to_sql_datatype(hyperdb.Date)),
         ]
         mls = []
         # add the multilinks separately
@@ -412,7 +422,7 @@
                 #and prop.find('Multilink') != -1:
                 #mls.append(col)
 
-            datatype = self.hyperdb_to_sql_datatypes[prop.__class__]
+            datatype = self.hyperdb_to_sql_datatype(prop.__class__)
             cols.append(('_'+col, datatype))
 
             # Intervals stored as two columns
@@ -490,7 +500,7 @@
                 self.create_multilink_table(spec, propname)
             else:
                 # add the column
-                coltype = self.hyperdb_to_sql_datatypes[prop.__class__]
+                coltype = self.hyperdb_to_sql_datatype(prop.__class__)
                 sql = 'alter table _%s add column _%s %s'%(
                     spec.classname, propname, coltype)
                 self.sql(sql)
@@ -562,7 +572,7 @@
         # 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 
+        sql = '''create unique index _%s_key_retired_idx
             on _%s(__retired__, _%s)'''%(cn, cn, key)
         self.sql(sql)
 
@@ -608,7 +618,7 @@
         sql = """create table %s__journal (
             nodeid integer, date %s, tag varchar(255),
             action varchar(255), params text)""" % (spec.classname,
-            self.hyperdb_to_sql_datatypes[hyperdb.Date])
+            self.hyperdb_to_sql_datatype(hyperdb.Date))
         self.sql(sql)
         self.create_journal_table_indexes(spec)
 
@@ -769,12 +779,24 @@
         hyperdb.Number    : lambda x: x,
         hyperdb.Multilink : lambda x: x,    # used in journal marshalling
     }
+
+    def to_sql_value(self, propklass):
+
+        fn = self.hyperdb_to_sql_value.get(propklass)
+        if fn:
+            return fn
+
+        for k, v in self.hyperdb_to_sql_value.iteritems():
+            if issubclass(propklass, k):
+                return v
+
+        raise ValueError, '%r is not a hyperdb property class' % propklass
+
     def addnode(self, classname, nodeid, node):
         """ Add the specified node to its class's db.
         """
-        if __debug__:
-            logging.getLogger('hyperdb').debug('addnode %s%s %r'%(classname,
-                nodeid, node))
+        self.log_debug('addnode %s%s %r'%(classname,
+            nodeid, node))
 
         # determine the column definitions and multilink tables
         cl = self.classes[classname]
@@ -823,7 +845,7 @@
             prop = props[col[1:]]
             value = values[col[1:]]
             if value is not None:
-                value = self.hyperdb_to_sql_value[prop.__class__](value)
+                value = self.to_sql_value(prop.__class__)(value)
             vals.append(value)
         vals.append(nodeid)
         vals = tuple(vals)
@@ -847,9 +869,8 @@
     def setnode(self, classname, nodeid, values, multilink_changes={}):
         """ Change the specified node.
         """
-        if __debug__:
-            logging.getLogger('hyperdb').debug('setnode %s%s %r'
-                % (classname, nodeid, values))
+        self.log_debug('setnode %s%s %r'
+            % (classname, nodeid, values))
 
         # clear this node out of the cache if it's in there
         key = (classname, nodeid)
@@ -895,7 +916,7 @@
                 if value is None:
                     e = None
                 else:
-                    e = self.hyperdb_to_sql_value[prop.__class__](value)
+                    e = self.to_sql_value(prop.__class__)(value)
                 vals.append(e)
 
         vals.append(int(nodeid))
@@ -941,11 +962,11 @@
                     # XXX numeric ids
                     self.sql(sql, (int(nodeid), int(addid)))
             if remove:
-                sql = 'delete from %s where nodeid=%s and linkid=%s'%(tn,
-                    self.arg, self.arg)
-                for removeid in remove:
-                    # XXX numeric ids
-                    self.sql(sql, (int(nodeid), int(removeid)))
+                s = ','.join([self.arg]*len(remove))
+                sql = 'delete from %s where nodeid=%s and linkid in (%s)'%(tn,
+                    self.arg, s)
+                # XXX numeric ids
+                self.sql(sql, [int(nodeid)] + remove)
 
     sql_to_hyperdb_value = {
         hyperdb.String : str,
@@ -958,6 +979,19 @@
         hyperdb.Number    : _num_cvt,
         hyperdb.Multilink : lambda x: x,    # used in journal marshalling
     }
+
+    def to_hyperdb_value(self, propklass):
+
+        fn = self.sql_to_hyperdb_value.get(propklass)
+        if fn:
+            return fn
+
+        for k, v in self.sql_to_hyperdb_value.iteritems():
+            if issubclass(propklass, k):
+                return v
+
+        raise ValueError, '%r is not a hyperdb property class' % propklass
+
     def getnode(self, classname, nodeid):
         """ Get a node from the database.
         """
@@ -1000,7 +1034,7 @@
                 continue
             value = values[col]
             if value is not None:
-                value = self.sql_to_hyperdb_value[props[name].__class__](value)
+                value = self.to_hyperdb_value(props[name].__class__)(value)
             node[name] = value
 
 
@@ -1009,7 +1043,7 @@
             # get the link ids
             sql = 'select linkid from %s_%s where nodeid=%s'%(classname, col,
                 self.arg)
-            self.cursor.execute(sql, (nodeid,))
+            self.sql(sql, (nodeid,))
             # extract the first column from the result
             # XXX numeric ids
             items = [int(x[0]) for x in self.cursor.fetchall()]
@@ -1021,7 +1055,7 @@
         self.cache[key] = node
         # update the LRU
         self.cache_lru.insert(0, key)
-        if len(self.cache_lru) > ROW_CACHE_SIZE:
+        if len(self.cache_lru) > self.cache_size:
             del self.cache[self.cache_lru.pop()]
 
         if __debug__:
@@ -1070,6 +1104,12 @@
     def hasnode(self, classname, nodeid):
         """ Determine if the database has a given node.
         """
+        # If this node is in the cache, then we do not need to go to
+        # the database.  (We don't consider this an LRU hit, though.)
+        if self.cache.has_key((classname, nodeid)):
+            # Return 1, not True, to match the type of the result of
+            # the SQL operation below.
+            return 1
         sql = 'select count(*) from _%s where id=%s'%(classname, self.arg)
         self.sql(sql, (nodeid,))
         return int(self.cursor.fetchone()[0])
@@ -1104,9 +1144,8 @@
         # create the journal entry
         cols = 'nodeid,date,tag,action,params'
 
-        if __debug__:
-            logging.getLogger('hyperdb').debug('addjournal %s%s %r %s %s %r'%(classname,
-                nodeid, journaldate, journaltag, action, params))
+        self.log_debug('addjournal %s%s %r %s %s %r'%(classname,
+            nodeid, journaldate, journaltag, action, params))
 
         # make the journalled data marshallable
         if isinstance(params, type({})):
@@ -1114,7 +1153,7 @@
 
         params = repr(params)
 
-        dc = self.hyperdb_to_sql_value[hyperdb.Date]
+        dc = self.to_sql_value(hyperdb.Date)
         journaldate = dc(journaldate)
 
         self.save_journal(classname, cols, nodeid, journaldate,
@@ -1129,12 +1168,11 @@
         # create the journal entry
         cols = 'nodeid,date,tag,action,params'
 
-        dc = self.hyperdb_to_sql_value[hyperdb.Date]
+        dc = self.to_sql_value(hyperdb.Date)
         for nodeid, journaldate, journaltag, action, params in journal:
-            if __debug__:
-                logging.getLogger('hyperdb').debug('addjournal %s%s %r %s %s %r'%(
-                    classname, nodeid, journaldate, journaltag, action,
-                    params))
+            self.log_debug('addjournal %s%s %r %s %s %r'%(
+                classname, nodeid, journaldate, journaltag, action,
+                params))
 
             # make the journalled data marshallable
             if isinstance(params, type({})):
@@ -1152,7 +1190,7 @@
             if not value:
                 continue
             property = properties[param]
-            cvt = self.hyperdb_to_sql_value[property.__class__]
+            cvt = self.to_sql_value(property.__class__)
             if isinstance(property, Password):
                 params[param] = cvt(value)
             elif isinstance(property, Date):
@@ -1173,7 +1211,7 @@
         journal = self.load_journal(classname, cols, nodeid)
 
         # now unmarshal the data
-        dc = self.sql_to_hyperdb_value[hyperdb.Date]
+        dc = self.to_hyperdb_value(hyperdb.Date)
         res = []
         properties = self.getclass(classname).getprops()
         for nodeid, date_stamp, user, action, params in journal:
@@ -1186,7 +1224,7 @@
                     if property is None:
                         # deleted property
                         continue
-                    cvt = self.sql_to_hyperdb_value[property.__class__]
+                    cvt = self.to_hyperdb_value(property.__class__)
                     if isinstance(property, Password):
                         params[param] = cvt(value)
                     elif isinstance(property, Date):
@@ -1223,7 +1261,7 @@
     def pack(self, pack_before):
         """ Delete all journal entries except "create" before 'pack_before'.
         """
-        date_stamp = self.hyperdb_to_sql_value[Date](pack_before)
+        date_stamp = self.to_sql_value(Date)(pack_before)
 
         # do the delete
         for classname in self.classes.keys():
@@ -1702,10 +1740,17 @@
 
                 # handle additions
                 for id in value:
-                    if not self.db.getclass(link_class).hasnode(id):
-                        raise IndexError, '%s has no node %s'%(link_class, id)
                     if id in l:
                         continue
+                    # We can safely check this condition after
+                    # checking that this is an addition to the
+                    # multilink since the condition was checked for
+                    # existing entries at the point they were added to
+                    # the multilink.  Since the hasnode call will
+                    # result in a SQL query, it is more efficient to
+                    # avoid the check if possible.
+                    if not self.db.getclass(link_class).hasnode(id):
+                        raise IndexError, '%s has no node %s'%(link_class, id)
                     # register the link with the newly linked node
                     if self.do_journal and self.properties[propname].do_journal:
                         self.db.addjournal(link_class, id, 'link',
@@ -1920,7 +1965,7 @@
         # sqlite)
         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, 0))
+        self.db.sql(sql, (str(keyvalue), 0))
 
         # see if there was a result that's not retired
         row = self.db.sql_fetchone()
@@ -2099,7 +2144,7 @@
         backward-compatibility reasons a single (dir, prop) tuple is
         also allowed.
 
-        "search_matches" is {nodeid: marker} or None
+        "search_matches" is a container type or None
 
         The filter must match all properties specificed. If the property
         value to match is a list:
@@ -2108,7 +2153,7 @@
         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 == {}:
+        if not search_matches and search_matches is not None:
             return []
 
         if __debug__:
@@ -2160,7 +2205,7 @@
                 if p.sort_type < 2:
                     mlfilt = 1
                     tn = '%s_%s'%(pcn, k)
-                    if v in ('-1', ['-1']):
+                    if v in ('-1', ['-1'], []):
                         # only match rows that have count(linkid)=0 in the
                         # corresponding multilink table)
                         where.append(self._subselect(pcn, tn))
@@ -2223,7 +2268,7 @@
                                 d[entry] = entry
                             l = []
                             if d.has_key(None) or not d:
-                                if d.has_key(None):del d[None]
+                                if d.has_key(None): del d[None]
                                 l.append('_%s._%s is NULL'%(pln, k))
                             if d:
                                 v = d.keys()
@@ -2249,7 +2294,7 @@
                                 cn, ln, pln, k, ln))
                         oc = '_%s._%s'%(ln, lp)
             elif isinstance(propclass, Date) and p.sort_type < 2:
-                dc = self.db.hyperdb_to_sql_value[hyperdb.Date]
+                dc = self.db.to_sql_value(hyperdb.Date)
                 if isinstance(v, type([])):
                     s = ','.join([a for x in v])
                     where.append('_%s._%s in (%s)'%(pln, k, s))
@@ -2319,8 +2364,7 @@
 
         # add results of full text search
         if search_matches is not None:
-            v = search_matches.keys()
-            s = ','.join([a for x in v])
+            s = ','.join([a for x in search_matches])
             where.append('_%s.id in (%s)'%(icn, s))
             args = args + v
 
@@ -2656,11 +2700,12 @@
             self.db.indexer.add_text((self.classname, newid, 'content'),
                 content, mime_type)
 
+        # store off the content as a file
+        self.db.storefile(self.classname, newid, None, content)
+
         # fire reactors
         self.fireReactors('create', newid, None)
 
-        # store off the content as a file
-        self.db.storefile(self.classname, newid, None, content)
         return newid
 
     def get(self, nodeid, propname, default=_marker, cache=1):

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 15 22:43:30 2009
@@ -1,4 +1,4 @@
-#$Id: sessions_dbm.py,v 1.9 2007/09/27 06:18:53 jpend Exp $
+#$Id: sessions_dbm.py,v 1.10 2008-08-18 05:04:01 richard Exp $
 """This module defines a very basic store that's used by the CGI interface
 to store session and one-time-key information.
 
@@ -139,15 +139,15 @@
         if sess is None or now > sess + 60:
             self.set(sessid, __timestamp=now)
 
-    def clean(self, now):
-        """Age sessions, remove when they haven't been used for a week.
-        """
+    def clean(self):
+        ''' Remove session records that haven't been used for a week. '''
+        now = time.time()
         week = 60*60*24*7
         for sessid in self.list():
             sess = self.get(sessid, '__timestamp', None)
             if sess is None:
-                sess=time.time()
                 self.updateTimestamp(sessid)
+                continue
             interval = now - sess
             if interval > week:
                 self.destroy(sessid)

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 15 22:43:30 2009
@@ -1,4 +1,4 @@
-#$Id: sessions_rdbms.py,v 1.7 2007/09/25 19:49:19 jpend Exp $
+#$Id: sessions_rdbms.py,v 1.8 2008-08-18 05:04:01 richard Exp $
 """This module defines a very basic store that's used by the CGI interface
 to store session and one-time-key information.
 
@@ -84,10 +84,11 @@
             self.name, self.db.arg, self.name, self.db.arg),
             (now, infoid, now-60))
 
-    def clean(self, now):
-        """Age sessions, remove when they haven't been used for a week.
-        """
-        old = now - 60*60*24*7
+    def clean(self):
+        ''' Remove session records that haven't been used for a week. '''
+        now = time.time()
+        week = 60*60*24*7
+        old = now - week
         self.cursor.execute('delete from %ss where %s_time < %s'%(self.name,
             self.name, self.db.arg), (old, ))
 

Modified: tracker/roundup-src/roundup/backends/tsearch2_setup.py
==============================================================================
--- tracker/roundup-src/roundup/backends/tsearch2_setup.py	(original)
+++ tracker/roundup-src/roundup/backends/tsearch2_setup.py	Sun Mar 15 22:43:30 2009
@@ -1,4 +1,4 @@
-#$Id: tsearch2_setup.py,v 1.2 2005/01/08 11:25:23 jlgijsbers Exp $
+#$Id: tsearch2_setup.py,v 1.2 2005-01-08 11:25:23 jlgijsbers Exp $
 
 # All the SQL in this module is taken from the tsearch2 module in the contrib
 # tree of PostgreSQL 7.4.6. PostgreSQL, and this code, has the following

Modified: tracker/roundup-src/roundup/cgi/PageTemplates/GlobalTranslationService.py
==============================================================================
--- tracker/roundup-src/roundup/cgi/PageTemplates/GlobalTranslationService.py	(original)
+++ tracker/roundup-src/roundup/cgi/PageTemplates/GlobalTranslationService.py	Sun Mar 15 22:43:30 2009
@@ -16,7 +16,7 @@
 # 2. make imports use roundup.cgi
 """Global Translation Service for providing I18n to Page Templates.
 
-$Id: GlobalTranslationService.py,v 1.4 2004/05/29 00:08:07 a1s Exp $
+$Id: GlobalTranslationService.py,v 1.4 2004-05-29 00:08:07 a1s Exp $
 """
 
 import re

Modified: tracker/roundup-src/roundup/cgi/PageTemplates/__init__.py
==============================================================================
--- tracker/roundup-src/roundup/cgi/PageTemplates/__init__.py	(original)
+++ tracker/roundup-src/roundup/cgi/PageTemplates/__init__.py	Sun Mar 15 22:43:30 2009
@@ -15,7 +15,7 @@
 This wrapper allows the Page Template modules to be segregated in a
 separate package.
 
-$Id: __init__.py,v 1.3 2004/05/21 05:56:46 richard Exp $'''
+$Id: __init__.py,v 1.3 2004-05-21 05:56:46 richard Exp $'''
 __version__='$$'[11:-2]
 
 

Modified: tracker/roundup-src/roundup/cgi/TAL/TranslationContext.py
==============================================================================
--- tracker/roundup-src/roundup/cgi/TAL/TranslationContext.py	(original)
+++ tracker/roundup-src/roundup/cgi/TAL/TranslationContext.py	Sun Mar 15 22:43:30 2009
@@ -16,7 +16,7 @@
 The translation context provides a container for the information
 needed to perform translation of a marked string from a page template.
 
-$Id: TranslationContext.py,v 1.1 2004/05/21 05:36:30 richard Exp $
+$Id: TranslationContext.py,v 1.1 2004-05-21 05:36:30 richard Exp $
 """
 
 DEFAULT_DOMAIN = "default"

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 15 22:43:30 2009
@@ -13,8 +13,8 @@
 #   translate(domain, msgid, mapping, context, target_language, default)
 #
 
-__version__ = "$Revision: 1.4 $"[11:-2]
-__date__ = "$Date: 2007/01/14 22:54:15 $"[7:-2]
+__version__ = "$Revision: 1.6 $"[11:-2]
+__date__ = "$Date: 2008-08-18 05:04:01 $"[7:-2]
 
 from roundup import i18n
 from roundup.cgi.PageTemplates import Expressions, PathIterator, TALES
@@ -37,11 +37,16 @@
     def gettext(self, msgid):
         if not isinstance(msgid, unicode):
             msgid = unicode(msgid, 'utf8')
-        return self.ugettext(msgid).encode(self.OUTPUT_ENCODING)
+        msgtrans=self.ugettext(msgid)
+        return msgtrans.encode(self.OUTPUT_ENCODING)
 
     def ngettext(self, singular, plural, number):
-        return self.ungettext(singular, plural, number).encode(
-            self.OUTPUT_ENCODING)
+        if not isinstance(singular, unicode):
+            singular = unicode(singular, 'utf8')
+        if not isinstance(plural, unicode):
+            plural = unicode(plural, 'utf8')
+        msgtrans=self.ungettext(singular, plural, number)
+        return msgtrans.encode(self.OUTPUT_ENCODING)
 
 class TranslationService(TranslationServiceMixin, i18n.RoundupTranslations):
     pass

Modified: tracker/roundup-src/roundup/cgi/ZTUtils/Batch.py
==============================================================================
--- tracker/roundup-src/roundup/cgi/ZTUtils/Batch.py	(original)
+++ tracker/roundup-src/roundup/cgi/ZTUtils/Batch.py	Sun Mar 15 22:43:30 2009
@@ -12,7 +12,7 @@
 ##############################################################################
 __doc__='''Batch class, for iterating over a sequence in batches
 
-$Id: Batch.py,v 1.3 2004/02/11 23:55:09 richard Exp $'''
+$Id: Batch.py,v 1.3 2004-02-11 23:55:09 richard Exp $'''
 __docformat__ = 'restructuredtext'
 __version__='$Revision: 1.3 $'[11:-2]
 

Modified: tracker/roundup-src/roundup/cgi/ZTUtils/Iterator.py
==============================================================================
--- tracker/roundup-src/roundup/cgi/ZTUtils/Iterator.py	(original)
+++ tracker/roundup-src/roundup/cgi/ZTUtils/Iterator.py	Sun Mar 15 22:43:30 2009
@@ -18,7 +18,7 @@
 iterator.  The next() method fetches the next item, and returns
 true if it succeeds.
 
-$Id: Iterator.py,v 1.4 2005/02/16 22:07:33 richard Exp $'''
+$Id: Iterator.py,v 1.4 2005-02-16 22:07:33 richard Exp $'''
 __docformat__ = 'restructuredtext'
 __version__='$Revision: 1.4 $'[11:-2]
 

Modified: tracker/roundup-src/roundup/cgi/ZTUtils/__init__.py
==============================================================================
--- tracker/roundup-src/roundup/cgi/ZTUtils/__init__.py	(original)
+++ tracker/roundup-src/roundup/cgi/ZTUtils/__init__.py	Sun Mar 15 22:43:30 2009
@@ -16,7 +16,7 @@
 
 - removed Zope imports
 
-$Id: __init__.py,v 1.3 2004/02/11 23:55:09 richard Exp $'''
+$Id: __init__.py,v 1.3 2004-02-11 23:55:09 richard Exp $'''
 __docformat__ = 'restructuredtext'
 __version__='$Revision: 1.3 $'[11:-2]
 

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 15 22:43:30 2009
@@ -1,8 +1,7 @@
-#$Id: actions.py,v 1.71 2007/09/20 23:44:58 jpend Exp $
-
-import re, cgi, StringIO, urllib, Cookie, time, random, csv, codecs
+import re, cgi, StringIO, urllib, time, random, csv, codecs
 
 from roundup import hyperdb, token, date, password
+from roundup.actions import Action as BaseAction
 from roundup.i18n import _
 import roundup.exceptions
 from roundup.cgi import exceptions, templating
@@ -104,30 +103,37 @@
 
     def handle(self):
         """Retire the context item."""
-        # if we want to view the index template now, then unset the nodeid
+        # ensure modification comes via POST
+        if self.client.env['REQUEST_METHOD'] != 'POST':
+            self.client.error_message.append(self._('Invalid request'))
+
+        # if we want to view the index template now, then unset the itemid
         # context info (a special-case for retire actions on the index page)
-        nodeid = self.nodeid
+        itemid = self.nodeid
         if self.template == 'index':
             self.client.nodeid = None
 
         # make sure we don't try to retire admin or anonymous
         if self.classname == 'user' and \
-                self.db.user.get(nodeid, 'username') in ('admin', 'anonymous'):
+                self.db.user.get(itemid, 'username') in ('admin', 'anonymous'):
             raise ValueError, self._(
                 'You may not retire the admin or anonymous user')
 
+        # check permission
+        if not self.hasPermission('Retire', classname=self.classname,
+                itemid=itemid):
+            raise exceptions.Unauthorised, self._(
+                'You do not have permission to retire %(class)s'
+            ) % {'class': self.classname}
+
         # do the retire
-        self.db.getclass(self.classname).retire(nodeid)
+        self.db.getclass(self.classname).retire(itemid)
         self.db.commit()
 
         self.client.ok_message.append(
             self._('%(classname)s %(itemid)s has been retired')%{
-                'classname': self.classname.capitalize(), 'itemid': nodeid})
+                'classname': self.classname.capitalize(), 'itemid': itemid})
 
-    def hasPermission(self, permission, classname=Action._marker, itemid=None):
-        if itemid is None:
-            itemid = self.nodeid
-        return Action.hasPermission(self, permission, classname, itemid, '__retired__')
 
 class SearchAction(Action):
     name = 'search'
@@ -234,7 +240,7 @@
                 if isinstance(prop, hyperdb.String):
                     v = self.form[key].value
                     l = token.token_split(v)
-                    if len(l) > 1 or l[0] != v:
+                    if len(l) != 1 or l[0] != v:
                         self.form.value.remove(self.form[key])
                         # replace the single value with the split list
                         for v in l:
@@ -275,12 +281,19 @@
         The "rows" CGI var defines the CSV-formatted entries for the class. New
         nodes are identified by the ID 'X' (or any other non-existent ID) and
         removed lines are retired.
-
         """
+        # ensure modification comes via POST
+        if self.client.env['REQUEST_METHOD'] != 'POST':
+            self.client.error_message.append(self._('Invalid request'))
+
+        # figure the properties list for the class
         cl = self.db.classes[self.classname]
-        idlessprops = cl.getprops(protected=0).keys()
-        idlessprops.sort()
-        props = ['id'] + idlessprops
+        props_without_id = cl.getprops(protected=0).keys()
+
+        # the incoming CSV data will always have the properties in colums
+        # sorted and starting with the "id" column
+        props_without_id.sort()
+        props = ['id'] + props_without_id
 
         # do the edit
         rows = StringIO.StringIO(self.form['rows'].value)
@@ -294,25 +307,38 @@
             if values == props:
                 continue
 
-            # extract the nodeid
-            nodeid, values = values[0], values[1:]
-            found[nodeid] = 1
+            # extract the itemid
+            itemid, values = values[0], values[1:]
+            found[itemid] = 1
 
             # see if the node exists
-            if nodeid in ('x', 'X') or not cl.hasnode(nodeid):
+            if itemid in ('x', 'X') or not cl.hasnode(itemid):
                 exists = 0
+
+                # check permission to create this item
+                if not self.hasPermission('Create', classname=self.classname):
+                    raise exceptions.Unauthorised, self._(
+                        'You do not have permission to create %(class)s'
+                    ) % {'class': self.classname}
             else:
                 exists = 1
 
             # confirm correct weight
-            if len(idlessprops) != len(values):
+            if len(props_without_id) != len(values):
                 self.client.error_message.append(
                     self._('Not enough values on line %(line)s')%{'line':line})
                 return
 
             # extract the new values
             d = {}
-            for name, value in zip(idlessprops, values):
+            for name, value in zip(props_without_id, values):
+                # check permission to edit this property on this item
+                if exists and not self.hasPermission('Edit', itemid=itemid,
+                        classname=self.classname, property=name):
+                    raise exceptions.Unauthorised, self._(
+                        'You do not have permission to edit %(class)s'
+                    ) % {'class': self.classname}
+
                 prop = cl.properties[name]
                 value = value.strip()
                 # only add the property if it has a value
@@ -341,15 +367,21 @@
             # perform the edit
             if exists:
                 # edit existing
-                cl.set(nodeid, **d)
+                cl.set(itemid, **d)
             else:
                 # new node
                 found[cl.create(**d)] = 1
 
         # retire the removed entries
-        for nodeid in cl.list():
-            if not found.has_key(nodeid):
-                cl.retire(nodeid)
+        for itemid in cl.list():
+            if not found.has_key(itemid):
+                # check permission to retire this item
+                if not self.hasPermission('Retire', itemid=itemid,
+                        classname=self.classname):
+                    raise exceptions.Unauthorised, self._(
+                        'You do not have permission to retire %(class)s'
+                    ) % {'class': self.classname}
+                cl.retire(itemid)
 
         # all OK
         self.db.commit()
@@ -486,26 +518,20 @@
 
     _cn_marker = []
     def editItemPermission(self, props, classname=_cn_marker, itemid=None):
-        """Determine whether the user has permission to edit this item.
-
-        Base behaviour is to check the user can edit this class. If we're
-        editing the "user" class, users are allowed to edit their own details.
-        Unless it's the "roles" property, which requires the special Permission
-        "Web Roles".
-        """
-        if self.classname == 'user':
-            if props.has_key('roles') and not self.hasPermission('Web Roles'):
-                raise exceptions.Unauthorised, self._(
-                    "You do not have permission to edit user roles")
-            if self.isEditingSelf():
-                return 1
+        """Determine whether the user has permission to edit this item."""
         if itemid is None:
             itemid = self.nodeid
         if classname is self._cn_marker:
             classname = self.classname
-        if self.hasPermission('Edit', itemid=itemid, classname=classname):
-            return 1
-        return 0
+        # The user must have permission to edit each of the properties
+        # being changed.
+        for p in props:
+            if not self.hasPermission('Edit', itemid=itemid,
+                    classname=classname, property=p):
+                return 0
+        # Since the user has permission to edit all of the properties,
+        # the edit is OK.
+        return 1
 
     def newItemPermission(self, props, classname=None):
         """Determine whether the user has permission to create this item.
@@ -559,6 +585,10 @@
         See parsePropsFromForm and _editnodes for special variables.
 
         """
+        # ensure modification comes via POST
+        if self.client.env['REQUEST_METHOD'] != 'POST':
+            self.client.error_message.append(self._('Invalid request'))
+
         user_activity = self.lastUserActivity()
         if user_activity:
             props = self.detectCollision(user_activity, self.lastNodeActivity())
@@ -601,6 +631,10 @@
             This follows the same form as the EditItemAction, with the same
             special form values.
         '''
+        # ensure modification comes via POST
+        if self.client.env['REQUEST_METHOD'] != 'POST':
+            self.client.error_message.append(self._('Invalid request'))
+
         # parse the props from the form
         try:
             props, links = self.client.parsePropsFromForm(create=1)
@@ -609,6 +643,11 @@
                 % str(message))
             return
 
+        # guard against new user creation that would bypass security checks
+        for key in props:
+            if 'user' in key:
+                return
+
         # handle the props - edit or create
         try:
             # when it hits the None element, it'll set self.nodeid
@@ -741,13 +780,8 @@
         # re-open the database for real, using the user
         self.client.opendb(user)
 
-        # if we have a session, update it
-        if hasattr(self.client, 'session'):
-            self.client.db.getSessionManager().set(self.client.session,
-                user=user, last_use=time.time())
-        else:
-            # new session cookie
-            self.client.set_cookie(user, expire=None)
+        # update session data
+        self.client.session_api.set(user=user)
 
         # nice message
         message = self._('You are now registered, welcome!')
@@ -779,10 +813,14 @@
 
     def handle(self):
         """Attempt to create a new user based on the contents of the form
-        and then set the cookie.
+        and then remember it in session.
 
         Return 1 on successful login.
         """
+        # ensure modification comes via POST
+        if self.client.env['REQUEST_METHOD'] != 'POST':
+            self.client.error_message.append(self._('Invalid request'))
+
         # parse the props from the form
         try:
             props, links = self.client.parsePropsFromForm(create=1)
@@ -876,15 +914,10 @@
 
 class LogoutAction(Action):
     def handle(self):
-        """Make us really anonymous - nuke the cookie too."""
+        """Make us really anonymous - nuke the session too."""
         # log us out
         self.client.make_user_anonymous()
-
-        # construct the logout cookie
-        now = Cookie._getdate()
-        self.client.additional_headers['Set-Cookie'] = \
-           '%s=deleted; Max-Age=0; expires=%s; Path=%s;' % (
-               self.client.cookie_name, now, self.client.cookie_path)
+        self.client.session_api.destroy()
 
         # Let the user know what's going on
         self.client.ok_message.append(self._('You are logged out'))
@@ -902,6 +935,10 @@
         Sets up a session for the user which contains the login credentials.
 
         """
+        # ensure modification comes via POST
+        if self.client.env['REQUEST_METHOD'] != 'POST':
+            self.client.error_message.append(self._('Invalid request'))
+
         # we need the username at a minimum
         if not self.form.has_key('__login_name'):
             self.client.error_message.append(self._('Username required'))
@@ -924,11 +961,10 @@
         # now we're OK, re-open the database for real, using the user
         self.client.opendb(self.client.user)
 
-        # set the session cookie
+        # save user in session
+        self.client.session_api.set(user=self.client.user)
         if self.form.has_key('remember'):
-            self.client.set_cookie(self.client.user, expire=86400*365)
-        else:
-            self.client.set_cookie(self.client.user, expire=None)
+            self.client.session_api.update(set_cookie=True, expire=24*3600*365)
 
         # If we came from someplace, go back there
         if self.form.has_key('__came_from'):
@@ -974,14 +1010,6 @@
         columns = request.columns
         klass = self.db.getclass(request.classname)
 
-        # validate the request
-        allprops = klass.getprops()
-        for c in filterspec.keys() + columns + [x[1] for x in group + sort]:
-            if not allprops.has_key(c):
-                # Can't use FormError, since that would try to use
-                # the same bogus field specs
-                raise exceptions.SeriousError, "Property %s does not exist" % c
-
         # full-text search
         if request.search_text:
             matches = self.db.indexer.search(
@@ -1006,14 +1034,63 @@
                 self.client.STORAGE_CHARSET, self.client.charset, 'replace')
 
         writer = csv.writer(wfile)
-        # mvl: protect against connection loss
         self.client._socket_op(writer.writerow, columns)
 
         # and search
         for itemid in klass.filter(matches, filterspec, sort, group):
-            # mvl: likewise
-            self.client._socket_op(writer.writerow, [str(klass.get(itemid, col)) for col in columns])
+            row = []
+            for name in columns:
+                # check permission to view this property on this item
+                if exists and not self.hasPermission('View', itemid=itemid,
+                        classname=request.classname, property=name):
+                    raise exceptions.Unauthorised, self._(
+                        'You do not have permission to view %(class)s'
+                    ) % {'class': request.classname}
+                row.append(str(klass.get(itemid, name)))
+            self.client._socket_op(writer.writerow, row)
 
         return '\n'
 
+
+class Bridge(BaseAction):
+    """Make roundup.actions.Action executable via CGI request.
+
+    Using this allows users to write actions executable from multiple frontends.
+    CGI Form content is translated into a dictionary, which then is passed as
+    argument to 'handle()'. XMLRPC requests have to pass this dictionary
+    directly.
+    """
+
+    def __init__(self, *args):
+
+        # As this constructor is callable from multiple frontends, each with
+        # different Action interfaces, we have to look at the arguments to
+        # figure out how to complete construction.
+        if (len(args) == 1 and
+            hasattr(args[0], '__class__') and
+            args[0].__class__.__name__ == 'Client'):
+            self.cgi = True
+            self.execute = self.execute_cgi
+            self.client = args[0]
+            self.form = self.client.form
+        else:
+            self.cgi = False
+
+    def execute_cgi(self):
+        args = {}
+        for key in self.form.keys():
+            args[key] = self.form.getvalue(key)
+        self.permission(args)
+        return self.handle(args)
+
+    def permission(self, args):
+        """Raise Unauthorised if the current user is not allowed to execute
+        this action. Users may override this method."""
+        
+        pass
+
+    def handle(self, args):
+        
+        raise NotImplementedError
+
 # vim: set filetype=python sts=4 sw=4 et si :

Modified: tracker/roundup-src/roundup/cgi/apache.py
==============================================================================
--- tracker/roundup-src/roundup/cgi/apache.py	(original)
+++ tracker/roundup-src/roundup/cgi/apache.py	Sun Mar 15 22:43:30 2009
@@ -21,7 +21,7 @@
 # 29-apr-2004 [als] created
 
 __version__ = "$Revision: 1.6 $"[11:-2]
-__date__ = "$Date: 2006/11/09 00:36:21 $"[7:-2]
+__date__ = "$Date: 2006-11-09 00:36:21 $"[7:-2]
 
 import cgi
 import os
@@ -77,6 +77,13 @@
         """NOOP. There aint no such thing as 'end_headers' in mod_python"""
         pass
 
+ 
+    def sendfile(self, filename, offset = 0, len = -1):
+        """Send 'filename' to the user."""
+
+        return self._req.sendfile(filename, offset, len)
+
+
 def handler(req):
     """HTTP request handler"""
     _options = req.get_options()

Modified: tracker/roundup-src/roundup/cgi/cgitb.py
==============================================================================
--- tracker/roundup-src/roundup/cgi/cgitb.py	(original)
+++ tracker/roundup-src/roundup/cgi/cgitb.py	Sun Mar 15 22:43:30 2009
@@ -1,7 +1,7 @@
 #
 # This module was written by Ka-Ping Yee, <ping at lfw.org>.
 #
-# $Id: cgitb.py,v 1.12 2004/07/13 10:18:00 a1s Exp $
+# $Id: cgitb.py,v 1.12 2004-07-13 10:18:00 a1s Exp $
 
 """Extended CGI traceback handler by Ka-Ping Yee, <ping at lfw.org>.
 """

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 15 22:43:30 2009
@@ -1,13 +1,12 @@
-# $Id: client.py,v 1.238 2007/09/22 21:20:57 jpend Exp $
-
 """WWW request handler (also used in the stand-alone server).
 """
 __docformat__ = 'restructuredtext'
 
-import base64, binascii, cgi, codecs, mimetypes, os
-import random, re, rfc822, stat, time, urllib, urlparse
+import base64, binascii, cgi, codecs, httplib, mimetypes, os
+import quopri, random, re, rfc822, stat, sys, time, urllib, urlparse
 import Cookie, socket, errno
 from Cookie import CookieError, BaseCookie, SimpleCookie
+from cStringIO import StringIO
 
 from roundup import roundupdb, date, hyperdb, password
 from roundup.cgi import templating, cgitb, TranslationService
@@ -17,6 +16,7 @@
 from roundup.cgi.form_parser import FormParser
 from roundup.mailer import Mailer, MessageSendError
 from roundup.cgi import accept_language
+from roundup import xmlrpc
 
 def initialiseSecurity(security):
     '''Create some Permissions and Roles on the security object
@@ -41,22 +41,22 @@
 def clean_message(message, mc=re.compile(CLEAN_MESSAGE_RE, re.I)):
     return mc.sub(clean_message_callback, message)
 def clean_message_callback(match, ok={'a':1,'i':1,'b':1,'br':1}):
-    ''' Strip all non <a>,<i>,<b> and <br> tags from a string
-    '''
+    """ Strip all non <a>,<i>,<b> and <br> tags from a string
+    """
     if ok.has_key(match.group(3).lower()):
         return match.group(1)
     return '&lt;%s&gt;'%match.group(2)
 
 
-error_message = ""'''<html><head><title>An error has occurred</title></head>
+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>'''
+</body></html>"""
 
 
 class LiberalCookie(SimpleCookie):
-    ''' Python's SimpleCookie throws an exception if the cookie uses invalid
+    """ 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:
@@ -67,7 +67,7 @@
         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 = []
@@ -88,8 +88,114 @@
             dict.__setitem__(self, key, None)
 
 
+class Session:
+    """
+    Needs DB to be already opened by client
+
+    Session attributes at instantiation:
+
+    - "client" - reference to client for add_cookie function
+    - "session_db" - session DB manager
+    - "cookie_name" - name of the cookie with session id
+    - "_sid" - session id for current user
+    - "_data" - session data cache
+
+    session = Session(client)
+    session.set(name=value)
+    value = session.get(name)
+
+    session.destroy()  # delete current session
+    session.clean_up() # clean up session table
+
+    session.update(set_cookie=True, expire=3600*24*365)
+                       # refresh session expiration time, setting persistent
+                       # cookie if needed to last for 'expire' seconds
+
+    """
+
+    def __init__(self, client):
+        self._data = {}
+        self._sid  = None
+
+        self.client = client
+        self.session_db = client.db.getSessionManager()
+
+        # parse cookies for session id
+        self.cookie_name = 'roundup_session_%s' % \
+            re.sub('[^a-zA-Z]', '', client.instance.config.TRACKER_NAME)
+        cookies = LiberalCookie(client.env.get('HTTP_COOKIE', ''))
+        if self.cookie_name in cookies:
+            if not self.session_db.exists(cookies[self.cookie_name].value):
+                self._sid = None
+                # remove old cookie
+                self.client.add_cookie(self.cookie_name, None)
+            else:
+                self._sid = cookies[self.cookie_name].value
+                self._data = self.session_db.getall(self._sid)
+
+    def _gen_sid(self):
+        """ generate a unique session key """
+        while 1:
+            s = '%s%s'%(time.time(), random.random())
+            s = binascii.b2a_base64(s).strip()
+            if not self.session_db.exists(s):
+                break
+
+        # clean up the base64
+        if s[-1] == '=':
+            if s[-2] == '=':
+                s = s[:-2]
+            else:
+                s = s[:-1]
+        return s
+
+    def clean_up(self):
+        """Remove expired sessions"""
+        self.session_db.clean()
+
+    def destroy(self):
+        self.client.add_cookie(self.cookie_name, None)
+        self._data = {}
+        self.session_db.destroy(self._sid)
+        self.client.db.commit()
+
+    def get(self, name, default=None):
+        return self._data.get(name, default)
+
+    def set(self, **kwargs):
+        self._data.update(kwargs)
+        if not self._sid:
+            self._sid = self._gen_sid()
+            self.session_db.set(self._sid, **self._data)
+            # add session cookie
+            self.update(set_cookie=True)
+
+            # XXX added when patching 1.4.4 for backward compatibility
+            # XXX remove
+            self.client.session = self._sid
+        else:
+            self.session_db.set(self._sid, **self._data)
+            self.client.db.commit()
+
+    def update(self, set_cookie=False, expire=None):
+        """ update timestamp in db to avoid expiration
+
+            if 'set_cookie' is True, set cookie with 'expire' seconds lifetime
+            if 'expire' is None - session will be closed with the browser
+             
+            XXX the session can be purged within a week even if a cookie
+                lifetime is longer
+        """
+        self.session_db.updateTimestamp(self._sid)
+        self.client.db.commit()
+
+        if set_cookie:
+            self.client.add_cookie(self.cookie_name, self._sid, expire=expire)
+
+
+
 class Client:
-    '''Instantiate to handle one CGI request.
+    """Instantiate to handle one CGI request.
 
     See inner_main for request processing.
 
@@ -106,9 +212,11 @@
 
     During the processing of a request, the following attributes are used:
 
+    - "db" 
     - "error_message" holds a list of error messages
     - "ok_message" holds a list of OK messages
-    - "session" is the current user session id
+    - "session" is deprecated in favor of session_api (XXX remove)
+    - "session_api" is the interface to store data in session
     - "user" is the current user's name
     - "userid" is the current user's id
     - "template" is the current :template context
@@ -116,18 +224,18 @@
     - "nodeid" is the current context item id
 
     User Identification:
-     If the user has no login cookie, then they are anonymous and are logged
+     Users that are absent in session data are anonymous and are logged
      in as that user. This typically gives them all Permissions assigned to the
      Anonymous Role.
 
-     Once a user logs in, they are assigned a session. The Client instance
-     keeps the nodeid of the session as the "session" attribute.
+     Every user is assigned a session. "session_api" is the interface to work
+     with session data.
 
     Special form variables:
      Note that in various places throughout this code, special form
      variables of the form :<name> are used. The colon (":") part may
      actually be one of either ":" or "@".
-    '''
+    """
 
     # charset used for data storage and form templates
     # Note: must be in lower case for comparisons!
@@ -186,11 +294,9 @@
         # this is the "cookie path" for this tracker (ie. the path part of
         # the "base" url)
         self.cookie_path = urlparse.urlparse(self.base)[2]
-        self.cookie_name = 'roundup_session_' + re.sub('[^a-zA-Z]', '',
-            self.instance.config.TRACKER_NAME)
         # cookies to set in http responce
         # {(path, name): (value, expire)}
-        self.add_cookies = {}
+        self._cookies = {}
 
         # see if we need to re-parse the environment for the form (eg Zope)
         if form is None:
@@ -216,7 +322,7 @@
         # default character set
         self.charset = self.STORAGE_CHARSET
 
-        # parse cookies (used in charset and session lookups)
+        # parse cookies (used for charset lookups)
         # 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', ''))
@@ -246,16 +352,49 @@
         self.ngettext = translator.ngettext
 
     def main(self):
-        ''' Wrap the real main in a try/finally so we always close off the db.
-        '''
+        """ Wrap the real main in a try/finally so we always close off the db.
+        """
         try:
-            self.inner_main()
+            if self.env.get('CONTENT_TYPE') == 'text/xml':
+                self.handle_xmlrpc()
+            else:
+                self.inner_main()
         finally:
             if hasattr(self, 'db'):
                 self.db.close()
 
+
+    def handle_xmlrpc(self):
+
+        # Pull the raw XML out of the form.  The "value" attribute
+        # will be the raw content of the POST request.
+        assert self.form.file
+        input = self.form.value
+        # So that the rest of Roundup can query the form in the
+        # usual way, we create an empty list of fields.
+        self.form.list = []
+
+        # Set the charset and language, since other parts of
+        # Roundup may depend upon that.
+        self.determine_charset()
+        self.determine_language()
+        # Open the database as the correct user.
+        self.determine_user()
+
+        # Call the appropriate XML-RPC method.
+        handler = xmlrpc.RoundupDispatcher(self.db,
+                                           self.instance.actions,
+                                           self.translator,
+                                           allow_none=True)
+        output = handler.dispatch(input)
+        self.db.commit()
+
+        self.setHeader("Content-Type", "text/xml")
+        self.setHeader("Content-Length", str(len(output)))
+        self.write(output)
+        
     def inner_main(self):
-        '''Process a request.
+        """Process a request.
 
         The most common requests are handled like so:
 
@@ -285,48 +424,57 @@
           doesn't have permission
         - NotFound       (raised wherever it needs to be)
           percolates up to the CGI interface that called the client
-        '''
+        """
         self.ok_message = []
         self.error_message = []
         try:
             self.determine_charset()
             self.determine_language()
 
-            # make sure we're identified (even anonymously)
-            self.determine_user()
-
-            # figure out the context and desired content template
-            self.determine_context()
-
-            # possibly handle a form submit action (may change self.classname
-            # and self.template, and may also append error/ok_messages)
-            html = self.handle_action()
+            try:
+                # make sure we're identified (even anonymously)
+                self.determine_user()
 
-            if html:
-                self.write_html(html)
-                return
+                # figure out the context and desired content template
+                self.determine_context()
 
-            # now render the page
-            # we don't want clients caching our dynamic pages
-            self.additional_headers['Cache-Control'] = 'no-cache'
-# Pragma: no-cache makes Mozilla and its ilk double-load all pages!!
-#            self.additional_headers['Pragma'] = 'no-cache'
-
-            # pages with messages added expire right now
-            # simple views may be cached for a small amount of time
-            # TODO? make page expire time configurable
-            # <rj> always expire pages, as IE just doesn't seem to do the
-            # right thing here :(
-            date = time.time() - 1
-            #if self.error_message or self.ok_message:
-            #    date = time.time() - 1
-            #else:
-            #    date = time.time() + 5
-            self.additional_headers['Expires'] = rfc822.formatdate(date)
+                # possibly handle a form submit action (may change self.classname
+                # and self.template, and may also append error/ok_messages)
+                html = self.handle_action()
+
+                if html:
+                    self.write_html(html)
+                    return
+
+                # now render the page
+                # we don't want clients caching our dynamic pages
+                self.additional_headers['Cache-Control'] = 'no-cache'
+                # Pragma: no-cache makes Mozilla and its ilk
+                # double-load all pages!!
+                #            self.additional_headers['Pragma'] = 'no-cache'
+
+                # pages with messages added expire right now
+                # simple views may be cached for a small amount of time
+                # TODO? make page expire time configurable
+                # <rj> always expire pages, as IE just doesn't seem to do the
+                # right thing here :(
+                date = time.time() - 1
+                #if self.error_message or self.ok_message:
+                #    date = time.time() - 1
+                #else:
+                #    date = time.time() + 5
+                self.additional_headers['Expires'] = rfc822.formatdate(date)
 
-            # render the content
-            try:
+                # render the content
                 self.write_html(self.renderContext())
+            except SendFile, designator:
+                # The call to serve_file may result in an Unauthorised
+                # exception or a NotModified exception.  Those
+                # exceptions will be handled by the outermost set of
+                # exception handlers.
+                self.serve_file(designator)
+            except SendStaticFile, file:
+                self.serve_static_file(str(file))
             except IOError:
                 # IOErrors here are due to the client disconnecting before
                 # recieving the reply.
@@ -342,26 +490,17 @@
                 self.additional_headers['Location'] = str(url)
                 self.response_code = 302
             self.write_html('Redirecting to <a href="%s">%s</a>'%(url, url))
-        except SendFile, designator:
-            try:
-                self.serve_file(designator)
-            except NotModified:
-                # send the 304 response
-                self.response_code = 304
-                self.header()
-        except SendStaticFile, file:
-            try:
-                self.serve_static_file(str(file))
-            except NotModified:
-                # send the 304 response
-                self.response_code = 304
-                self.header()
         except Unauthorised, message:
             # users may always see the front page
+            self.response_code = 403
             self.classname = self.nodeid = None
             self.template = ''
             self.error_message.append(message)
             self.write_html(self.renderContext())
+        except NotModified:
+            # send the 304 response
+            self.response_code = 304
+            self.header()
         except NotFound, e:
             self.response_code = 404
             self.template = '404'
@@ -380,63 +519,32 @@
             if self.instance.config.WEB_DEBUG:
                 self.write_html(cgitb.html(i18n=self.translator))
             else:
-                self.mailer.exception_message(self.exception_data())
+                self.mailer.exception_message()
                 return self.write_html(self._(error_message))
 
-    def exception_data(self):
-        result = ''
-        try:
-            for k,v in self.env.items():
-                result += "%s=%s\n" % (k,v)
-            for k,v in self.request.headers.items():
-                result += "%s=%s\n" % (k,v)
-            result += "user:" + repr(self.user) + "\n"
-        except:
-            pass
-        return result
-
     def clean_sessions(self):
-        """Age sessions, remove when they haven't been used for a week.
+        """Deprecated
+           XXX remove
+        """
+        self.clean_up()
 
-        Do it only once an hour.
+    def clean_up(self):
+        """Remove expired sessions and One Time Keys.
 
-        Note: also cleans One Time Keys, and other "session" based stuff.
+           Do it only once an hour.
         """
-        sessions = self.db.getSessionManager()
-        last_clean = sessions.get('last_clean', 'last_use', 0)
-
-        # time to clean?
-        #week = 60*60*24*7
         hour = 60*60
         now = time.time()
+
+        # XXX: hack - use OTK table to store last_clean time information
+        #      'last_clean' string is used instead of otk key
+        last_clean = self.db.getOTKManager().get('last_clean', 'last_use', 0)
         if now - last_clean < hour:
-            # Release the database lock obtained when looking at last_clean
-            self.db.rollback()
             return
 
-        # This is a bit ugly, but right now, I'm too lazy to fix a new API
-        # in all rdbms-based backends to cope with this problem that only
-        # appears on Postgres.
-        try:
-            from psycopg import ProgrammingError
-        except ImportError:
-            from psycopg2.psycopg1 import ProgrammingError
-        except ImportError:
-            ProgrammingError = None
-
-        try:
-            sessions.clean(now)
-            self.db.getOTKManager().clean(now)
-            sessions.set('last_clean', last_use=time.time())
-        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()
-                return 
+        self.session_api.clean_up()
+        self.db.getOTKManager().clean()
+        self.db.getOTKManager().set('last_clean', last_use=now)
         self.db.commit(fail_ok=True)
 
     def determine_charset(self):
@@ -529,9 +637,12 @@
         """Determine who the user is"""
         self.opendb('admin')
 
-        # make sure we have the session Class
-        self.clean_sessions()
-        sessions = self.db.getSessionManager()
+        # get session data from db
+        # XXX: rename
+        self.session_api = Session(self)
+
+        # take the opportunity to cleanup expired sessions and otks
+        self.clean_up()
 
         user = None
         # first up, try http authorization if enabled
@@ -555,27 +666,17 @@
                         login.verifyLogin(username, password)
                     except LoginError, err:
                         self.make_user_anonymous()
-                        self.response_code = 403
                         raise Unauthorised, err
-
                     user = username
 
-        # if user was not set by http authorization, try session cookie
-        if (not user and self.cookie.has_key(self.cookie_name)
-                and (self.cookie[self.cookie_name].value != 'deleted')):
-            # get the session key from the cookie
-            self.session = self.cookie[self.cookie_name].value
-            # get the user from the session
-            try:
-                # update the lifetime datestamp
-                sessions.updateTimestamp(self.session)
-                self.db.commit()
-                user = sessions.get(self.session, 'user')
-            except KeyError:
-                # not valid, ignore id
-                pass
+        # if user was not set by http authorization, try session lookup
+        if not user:
+            user = self.session_api.get('user')
+            if user:
+                # update session lifetime datestamp
+                self.session_api.update()
 
-        # if no user name set by http authorization or session cookie
+        # if no user name set by http authorization or session lookup
         # the user is anonymous
         if not user:
             user = 'anonymous'
@@ -708,10 +809,6 @@
                 klass = self.db.getclass(self.classname)
             except KeyError:
                 raise NotFound, '%s/%s'%(self.classname, self.nodeid)
-            if long(self.nodeid) > 2**31:
-                # Postgres will complain with a ProgrammingError
-                # if we try to pass in numbers that are too large
-                raise NotFound, '%s/%s'%(self.classname, self.nodeid)
             if not klass.hasnode(self.nodeid):
                 raise NotFound, '%s/%s'%(self.classname, self.nodeid)
             # with a designator, we default to item view
@@ -731,8 +828,8 @@
             self.template = template_override
 
     def serve_file(self, designator, dre=re.compile(r'([^\d]+)(\d+)')):
-        ''' Serve the file from the content property of the designated item.
-        '''
+        """ Serve the file from the content property of the designated item.
+        """
         m = dre.match(str(designator))
         if not m:
             raise NotFound, str(designator)
@@ -754,14 +851,41 @@
                 "this file.")
 
         mime_type = klass.get(nodeid, 'type')
-        content = klass.get(nodeid, 'content')
+
+        # if the mime_type is HTML-ish then make sure we're allowed to serve up
+        # HTML-ish content
+        if mime_type in ('text/html', 'text/x-html'):
+            if not self.instance.config['WEB_ALLOW_HTML_FILE']:
+                # do NOT serve the content up as HTML
+                mime_type = 'application/octet-stream'
+
+        # If this object is a file (i.e., an instance of FileClass),
+        # see if we can find it in the filesystem.  If so, we may be
+        # able to use the more-efficient request.sendfile method of
+        # sending the file.  If not, just get the "content" property
+        # in the usual way, and use that.
+        content = None
+        filename = None
+        if isinstance(klass, hyperdb.FileClass):
+            try:
+                filename = self.db.filename(classname, nodeid)
+            except AttributeError:
+                # The database doesn't store files in the filesystem
+                # and therefore doesn't provide the "filename" method.
+                pass
+            except IOError:
+                # The file does not exist.
+                pass
+        if not filename:
+            content = klass.get(nodeid, 'content')
+        
         lmt = klass.get(nodeid, 'activity').timestamp()
 
-        self._serve_file(lmt, mime_type, content)
+        self._serve_file(lmt, mime_type, content, filename)
 
     def serve_static_file(self, file):
-        ''' Serve up the file named from the templates dir
-        '''
+        """ Serve up the file named from the templates dir
+        """
         # figure the filename - try STATIC_FILES, then TEMPLATES dir
         for dir_option in ('STATIC_FILES', 'TEMPLATES'):
             prefix = self.instance.config[dir_option]
@@ -788,21 +912,14 @@
             else:
                 mime_type = 'text/plain'
 
-        # snarf the content
-        f = open(filename, 'rb')
-        try:
-            content = f.read()
-        finally:
-            f.close()
+        self._serve_file(lmt, mime_type, '', filename)
 
-        self._serve_file(lmt, mime_type, content)
+    def _serve_file(self, lmt, mime_type, content=None, filename=None):
+        """ guts of serve_file() and serve_static_file()
+        """
 
-    def _serve_file(self, lmt, mime_type, content):
-        ''' guts of serve_file() and serve_static_file()
-        '''
         # spit out headers
         self.additional_headers['Content-Type'] = mime_type
-        self.additional_headers['Content-Length'] = str(len(content))
         self.additional_headers['Last-Modified'] = rfc822.formatdate(lmt)
 
         ims = None
@@ -819,14 +936,17 @@
             if lmtt <= ims:
                 raise NotModified
 
-        self.write(content)
+        if filename:
+            self.write_file(filename)
+        else:
+            self.additional_headers['Content-Length'] = str(len(content))
+            self.write(content)
 
     def renderContext(self):
-        ''' Return a PageTemplate for the named page
-        '''
+        """ Return a PageTemplate for the named page
+        """
         name = self.classname
         extension = self.template
-        pt = self.instance.templates.get(name, extension)
 
         # catch errors so we can handle PT rendering errors more nicely
         args = {
@@ -834,6 +954,7 @@
             'error_message': self.error_message
         }
         try:
+            pt = self.instance.templates.get(name, extension)
             # let the template render figure stuff out
             result = pt.render(self, None, None, **args)
             self.additional_headers['Content-Type'] = pt.content_type
@@ -861,12 +982,35 @@
             raise Unauthorised, str(message)
         except:
             # everything else
-            return cgitb.pt_html(i18n=self.translator)
+            if self.instance.config.WEB_DEBUG:
+                return cgitb.pt_html(i18n=self.translator)
+            exc_info = sys.exc_info()
+            try:
+                # If possible, send the HTML page template traceback
+                # to the administrator.
+                to = [self.mailer.config.ADMIN_EMAIL]
+                subject = "Templating Error: %s" % exc_info[1]
+                content = cgitb.pt_html()
+                message, writer = self.mailer.get_standard_message(
+                    to, subject)
+                writer.addheader('Content-Transfer-Encoding', 'quoted-printable')
+                body = writer.startbody('text/html; charset=utf-8')
+                content = StringIO(content)
+                quopri.encode(content, body, 0)
+                self.mailer.smtp_send(to, message)
+                # Now report the error to the user.
+                return self._(error_message)
+            except:
+                # Reraise the original exception.  The user will
+                # receive an error message, and the adminstrator will
+                # receive a traceback, albeit with less information
+                # than the one we tried to generate above.
+                raise exc_info[0], exc_info[1], exc_info[2]
 
     # these are the actions that are available
     actions = (
         ('edit',        EditItemAction),
-        # ('editcsv',     EditCSVAction),
+        ('editcsv',     EditCSVAction),
         ('new',         NewItemAction),
         ('register',    RegisterAction),
         ('confrego',    ConfRegoAction),
@@ -879,7 +1023,7 @@
         ('export_csv',  ExportCSVAction),
     )
     def handle_action(self):
-        ''' Determine whether there should be an Action called.
+        """ Determine whether there should be an Action called.
 
             The action is defined by the form variable :action which
             identifies the method on this object to call. The actions
@@ -890,7 +1034,7 @@
 
             We explicitly catch Reject and ValueError exceptions and
             present their messages to the user.
-        '''
+        """
         if self.form.has_key(':action'):
             action = self.form[':action'].value.lower()
         elif self.form.has_key('@action'):
@@ -944,6 +1088,14 @@
                     pass
             if err_errno not in self.IGNORE_NET_ERRORS:
                 raise
+        except IOError:
+            # Apache's mod_python will raise IOError -- without an
+            # accompanying errno -- when a write to the client fails.
+            # A common case is that the client has closed the
+            # connection.  There's no way to be certain that this is
+            # the situation that has occurred here, but that is the
+            # most likely case.
+            pass
 
     def write(self, content):
         if not self.headers_done:
@@ -971,14 +1123,236 @@
         # and write
         self._socket_op(self.request.wfile.write, content)
 
+    def http_strip(self, content):
+        """Remove HTTP Linear White Space from 'content'.
+
+        'content' -- A string.
+
+        returns -- 'content', with all leading and trailing LWS
+        removed."""
+
+        # RFC 2616 2.2: Basic Rules
+        #
+        # LWS = [CRLF] 1*( SP | HT )
+        return content.strip(" \r\n\t")
+
+    def http_split(self, content):
+        """Split an HTTP list.
+
+        'content' -- A string, giving a list of items.
+
+        returns -- A sequence of strings, containing the elements of
+        the list."""
+
+        # RFC 2616 2.1: Augmented BNF
+        #
+        # Grammar productions of the form "#rule" indicate a
+        # comma-separated list of elements matching "rule".  LWS
+        # is then removed from each element, and empty elements
+        # removed.
+
+        # Split at commas.
+        elements = content.split(",")
+        # Remove linear whitespace at either end of the string.
+        elements = [self.http_strip(e) for e in elements]
+        # Remove any now-empty elements.
+        return [e for e in elements if e]
+        
+    def handle_range_header(self, length, etag):
+        """Handle the 'Range' and 'If-Range' headers.
+
+        'length' -- the length of the content available for the
+        resource.
+
+        'etag' -- the entity tag for this resources.
+
+        returns -- If the request headers (including 'Range' and
+        'If-Range') indicate that only a portion of the entity should
+        be returned, then the return value is a pair '(offfset,
+        length)' indicating the first byte and number of bytes of the
+        content that should be returned to the client.  In addition,
+        this method will set 'self.response_code' to indicate Partial
+        Content.  In all other cases, the return value is 'None'.  If
+        appropriate, 'self.response_code' will be
+        set to indicate 'REQUESTED_RANGE_NOT_SATISFIABLE'.  In that
+        case, the caller should not send any data to the client."""
+
+        # RFC 2616 14.35: Range
+        #
+        # See if the Range header is present.
+        ranges_specifier = self.env.get("HTTP_RANGE")
+        if ranges_specifier is None:
+            return None
+        # RFC 2616 14.27: If-Range
+        #
+        # Check to see if there is an If-Range header.
+        # Because the specification says:
+        #
+        #  The If-Range header ... MUST be ignored if the request
+        #  does not include a Range header, we check for If-Range
+        #  after checking for Range.
+        if_range = self.env.get("HTTP_IF_RANGE")
+        if if_range:
+            # The grammar for the If-Range header is:
+            # 
+            #   If-Range = "If-Range" ":" ( entity-tag | HTTP-date )
+            #   entity-tag = [ weak ] opaque-tag
+            #   weak = "W/"
+            #   opaque-tag = quoted-string
+            #
+            # We only support strong entity tags.
+            if_range = self.http_strip(if_range)
+            if (not if_range.startswith('"')
+                or not if_range.endswith('"')):
+                return None
+            # If the condition doesn't match the entity tag, then we
+            # must send the client the entire file.
+            if if_range != etag:
+                return
+        # The grammar for the Range header value is:
+        #
+        #   ranges-specifier = byte-ranges-specifier
+        #   byte-ranges-specifier = bytes-unit "=" byte-range-set
+        #   byte-range-set = 1#( byte-range-spec | suffix-byte-range-spec )
+        #   byte-range-spec = first-byte-pos "-" [last-byte-pos]
+        #   first-byte-pos = 1*DIGIT
+        #   last-byte-pos = 1*DIGIT
+        #   suffix-byte-range-spec = "-" suffix-length
+        #   suffix-length = 1*DIGIT
+        #
+        # Look for the "=" separating the units from the range set.
+        specs = ranges_specifier.split("=", 1)
+        if len(specs) != 2:
+            return None
+        # Check that the bytes-unit is in fact "bytes".  If it is not,
+        # we do not know how to process this range.
+        bytes_unit = self.http_strip(specs[0])
+        if bytes_unit != "bytes":
+            return None
+        # Seperate the range-set into range-specs.
+        byte_range_set = self.http_strip(specs[1])
+        byte_range_specs = self.http_split(byte_range_set)
+        # We only handle exactly one range at this time.
+        if len(byte_range_specs) != 1:
+            return None
+        # Parse the spec.
+        byte_range_spec = byte_range_specs[0]
+        pos = byte_range_spec.split("-", 1)
+        if len(pos) != 2:
+            return None
+        # Get the first and last bytes.
+        first = self.http_strip(pos[0])
+        last = self.http_strip(pos[1])
+        # We do not handle suffix ranges.
+        if not first:
+            return None
+       # Convert the first and last positions to integers.
+        try:
+            first = int(first)
+            if last:
+                last = int(last)
+            else:
+                last = length - 1
+        except:
+            # The positions could not be parsed as integers.
+            return None
+        # Check that the range makes sense.
+        if (first < 0 or last < 0 or last < first):
+            return None
+        if last >= length:
+            # RFC 2616 10.4.17: 416 Requested Range Not Satisfiable
+            #
+            # If there is an If-Range header, RFC 2616 says that we
+            # should just ignore the invalid Range header.
+            if if_range:
+                return None
+            # Return code 416 with a Content-Range header giving the
+            # allowable range.
+            self.response_code = httplib.REQUESTED_RANGE_NOT_SATISFIABLE
+            self.setHeader("Content-Range", "bytes */%d" % length)
+            return None
+        # RFC 2616 10.2.7: 206 Partial Content
+        #
+        # Tell the client that we are honoring the Range request by
+        # indicating that we are providing partial content.
+        self.response_code = httplib.PARTIAL_CONTENT
+        # RFC 2616 14.16: Content-Range
+        #
+        # Tell the client what data we are providing.
+        #
+        #   content-range-spec = byte-content-range-spec
+        #   byte-content-range-spec = bytes-unit SP
+        #                             byte-range-resp-spec "/"
+        #                             ( instance-length | "*" )
+        #   byte-range-resp-spec = (first-byte-pos "-" last-byte-pos)
+        #                          | "*"
+        #   instance-length      = 1 * DIGIT
+        self.setHeader("Content-Range",
+                       "bytes %d-%d/%d" % (first, last, length))
+        return (first, last - first + 1)
+
+    def write_file(self, filename):
+        """Send the contents of 'filename' to the user."""
+
+        # Determine the length of the file.
+        stat_info = os.stat(filename)
+        length = stat_info[stat.ST_SIZE]
+        # Assume we will return the entire file.
+        offset = 0
+        # If the headers have not already been finalized, 
+        if not self.headers_done:
+            # RFC 2616 14.19: ETag
+            #
+            # Compute the entity tag, in a format similar to that
+            # used by Apache.
+            etag = '"%x-%x-%x"' % (stat_info[stat.ST_INO],
+                                   length,
+                                   stat_info[stat.ST_MTIME])
+            self.setHeader("ETag", etag)
+            # RFC 2616 14.5: Accept-Ranges
+            #
+            # Let the client know that we will accept range requests.
+            self.setHeader("Accept-Ranges", "bytes")
+            # RFC 2616 14.35: Range
+            #
+            # If there is a Range header, we may be able to avoid
+            # sending the entire file.
+            content_range = self.handle_range_header(length, etag)
+            if content_range:
+                offset, length = content_range
+            # RFC 2616 14.13: Content-Length
+            #
+            # Tell the client how much data we are providing.
+            self.setHeader("Content-Length", length)
+            # Send the HTTP header.
+            self.header()
+        # If the client doesn't actually want the body, or if we are
+        # indicating an invalid range.
+        if (self.env['REQUEST_METHOD'] == 'HEAD'
+            or self.response_code == httplib.REQUESTED_RANGE_NOT_SATISFIABLE):
+            return
+        # Use the optimized "sendfile" operation, if possible.
+        if hasattr(self.request, "sendfile"):
+            self._socket_op(self.request.sendfile, filename, offset, length)
+            return
+        # Fallback to the "write" operation.
+        f = open(filename, 'rb')
+        try:
+            if offset:
+                f.seek(offset)
+            content = f.read(length)
+        finally:
+            f.close()
+        self.write(content)
+
     def setHeader(self, header, value):
-        '''Override a header to be returned to the user's browser.
-        '''
+        """Override a header to be returned to the user's browser.
+        """
         self.additional_headers[header] = value
 
     def header(self, headers=None, response=None):
-        '''Put up the appropriate header.
-        '''
+        """Put up the appropriate header.
+        """
         if headers is None:
             headers = {'Content-Type':'text/html; charset=utf-8'}
         if response is None:
@@ -992,7 +1366,7 @@
 
         headers = headers.items()
 
-        for ((path, name), (value, expire)) in self.add_cookies.items():
+        for ((path, name), (value, expire)) in self._cookies.items():
             cookie = "%s=%s; Path=%s;"%(name, value, path)
             if expire is not None:
                 cookie += " expires=%s;"%Cookie._getdate(expire)
@@ -1027,48 +1401,30 @@
             path = self.cookie_path
         if not value:
             expire = -1
-        self.add_cookies[(path, name)] = (value, expire)
+        self._cookies[(path, name)] = (value, expire)
 
     def set_cookie(self, user, expire=None):
-        """Set up a session cookie for the user.
+        """Deprecated. Use session_api calls directly
 
-        Also store away the user's login info against the session.
+        XXX remove
         """
-        sessions = self.db.getSessionManager()
-
-        # generate a unique session key
-        while 1:
-            s = '%s%s'%(time.time(), random.random())
-            s = binascii.b2a_base64(s).strip()
-            if not sessions.exists(s):
-                break
-        self.session = s
 
-        # clean up the base64
-        if self.session[-1] == '=':
-            if self.session[-2] == '=':
-                self.session = self.session[:-2]
-            else:
-                self.session = self.session[:-1]
-
-        # insert the session in the sessiondb
-        sessions.set(self.session, user=user)
-        self.db.commit()
-
-        # add session cookie
-        self.add_cookie(self.cookie_name, self.session, expire=expire)
+        # insert the session in the session db
+        self.session_api.set(user=user)
+        # refresh session cookie
+        self.session_api.update(set_cookie=True, expire=expire)
 
     def make_user_anonymous(self):
-        ''' Make us anonymous
+        """ Make us anonymous
 
             This method used to handle non-existence of the 'anonymous'
             user, but that user is mandatory now.
-        '''
+        """
         self.userid = self.db.user.lookup('anonymous')
         self.user = 'anonymous'
 
     def standard_message(self, to, subject, body, author=None):
-        '''Send a standard email message from Roundup.
+        """Send a standard email message from Roundup.
 
         "to"      - recipients list
         "subject" - Subject
@@ -1076,7 +1432,7 @@
         "author"  - (name, address) tuple or None for admin email
 
         Arguments are passed to the Mailer.standard_message code.
-        '''
+        """
         try:
             self.mailer.standard_message(to, subject, body, author)
         except MessageSendError, e:

Modified: tracker/roundup-src/roundup/cgi/exceptions.py
==============================================================================
--- tracker/roundup-src/roundup/cgi/exceptions.py	(original)
+++ tracker/roundup-src/roundup/cgi/exceptions.py	Sun Mar 15 22:43:30 2009
@@ -1,20 +1,14 @@
-#$Id: exceptions.py,v 1.6 2004/11/18 14:10:27 a1s Exp $
-'''Exceptions for use in Roundup's web interface.
-'''
+"""Exceptions for use in Roundup's web interface.
+"""
 
 __docformat__ = 'restructuredtext'
 
+from roundup.exceptions import LoginError, Unauthorised
 import cgi
 
 class HTTPException(Exception):
     pass
 
-class LoginError(HTTPException):
-    pass
-
-class Unauthorised(HTTPException):
-    pass
-
 class Redirect(HTTPException):
     pass
 
@@ -50,13 +44,13 @@
     escaped.
     """
     def __str__(self):
-        return '''
+        return """
 <html><head><title>Roundup issue tracker: An error has occurred</title>
  <link rel="stylesheet" type="text/css" href="@@file/style.css">
 </head>
 <body class="body" marginwidth="0" marginheight="0">
  <p class="error-message">%s</p>
 </body></html>
-'''%cgi.escape(self.args[0])
+"""%cgi.escape(self.args[0])
 
 # vim: set filetype=python sts=4 sw=4 et si :

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 15 22:43:30 2009
@@ -425,7 +425,9 @@
                             if entry not in existing:
                                 existing.append(entry)
                     value = existing
-                    value.sort()
+                    # Sort the value in the same order used by
+                    # Multilink.from_raw.
+                    value.sort(key = lambda x: int(x))
 
             elif value == '':
                 # other types should be None'd if there's no value
@@ -483,9 +485,13 @@
                 except IndexError, message:
                     raise FormError(str(message))
 
-                # make sure the existing multilink is sorted
+                # make sure the existing multilink is sorted.  We must
+                # be sure to use the same sort order in all places,
+                # since we want to compare values with "=" or "!=".
+                # The canonical order (given in Multilink.from_raw) is
+                # by the numeric value of the IDs.
                 if isinstance(proptype, hyperdb.Multilink):
-                    existing.sort()
+                    existing.sort(key = lambda x: int(x))
 
                 # "missing" existing values may not be None
                 if not existing:
@@ -563,11 +569,11 @@
         # either have a non-empty content property or no property at all. In
         # the latter case, nothing will change.
         for (cn, id), props in all_props.items():
-            if (id == '-1') and not props:
+            if id is not None and id.startswith('-') and not props:
                 # new item (any class) with no content - ignore
                 del all_props[(cn, id)]
             elif isinstance(self.db.classes[cn], hyperdb.FileClass):
-                if id == '-1':
+                if id is not None and id.startswith('-'):
                     if not props.get('content', ''):
                         del all_props[(cn, id)]
                 elif props.has_key('content') and not props['content']:

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 15 22:43:30 2009
@@ -341,7 +341,7 @@
         # we want config to be exposed
         self.config = client.db.config
 
-    def __getitem__(self, item, desre=re.compile(r'(?P<cl>\w+)(?P<id>[-\d]+)')):
+    def __getitem__(self, item, desre=re.compile(r'(?P<cl>[a-zA-Z_]+)(?P<id>[-\d]+)')):
         # check to see if we're actually accessing an item
         m = desre.match(item)
         if m:
@@ -473,6 +473,14 @@
             raise Unauthorised("edit", self._classname,
                 translator=self._client.translator)
 
+    def retire_check(self):
+        """ Raise the Unauthorised exception if the user's not permitted to
+            retire items of this class.
+        """
+        if not self.is_retire_ok():
+            raise Unauthorised("retire", self._classname,
+                translator=self._client.translator)
+
 
 class HTMLClass(HTMLInputMixin, HTMLPermissions):
     """ Accesses through a class (either through *class* or *db.<classname>*)
@@ -497,6 +505,12 @@
         return self._db.security.hasPermission('Create', self._client.userid,
             self._classname)
 
+    def is_retire_ok(self):
+        """ Is the user allowed to retire items of the current class?
+        """
+        return self._db.security.hasPermission('Retire', self._client.userid,
+            self._classname)
+
     def is_view_ok(self):
         """ Is the user allowed to View the current class?
         """
@@ -530,24 +544,10 @@
         for klass, htmlklass in propclasses:
             if not isinstance(prop, klass):
                 continue
-            if form.has_key(item):
-                if isinstance(prop, hyperdb.Multilink):
-                    value = lookupIds(self._db, prop,
-                        handleListCGIValue(form[item]), fail_ok=1)
-                elif isinstance(prop, hyperdb.Link):
-                    value = form[item].value.strip()
-                    if value:
-                        value = lookupIds(self._db, prop, [value],
-                            fail_ok=1)[0]
-                    else:
-                        value = None
-                else:
-                    value = form[item].value.strip() or None
+            if isinstance(prop, hyperdb.Multilink):
+                value = []
             else:
-                if isinstance(prop, hyperdb.Multilink):
-                    value = []
-                else:
-                    value = None
+                value = None
             return htmlklass(self._client, self._classname, None, prop, item,
                 value, self._anonymous)
 
@@ -615,9 +615,16 @@
         s = StringIO.StringIO()
         writer = csv.writer(s)
         writer.writerow(props)
+        check = self._client.db.security.hasPermission
         for nodeid in self._klass.list():
             l = []
             for name in props:
+                # check permission to view this property on this item
+                if not check('View', self._client.userid, itemid=nodeid,
+                        classname=self._klass.classname, property=name):
+                    raise Unauthorised('view', self._klass.classname,
+                        translator=self._client.translator)
+                row.append(str(klass.get(itemid, name)))
                 value = self._klass.get(nodeid, name)
                 if value is None:
                     l.append('')
@@ -695,7 +702,7 @@
             if 'username' in properties.split( ',' ):
                 sort = 'username'
             else:
-                sort = find_sort_key(self._klass)
+                sort = self._klass.orderprop()
         sort = '&amp;@sort=' + sort
         if property:
             property = '&amp;property=%s'%property
@@ -775,13 +782,19 @@
         HTMLInputMixin.__init__(self)
 
     def is_edit_ok(self):
-        """ Is the user allowed to Edit the current class?
+        """ Is the user allowed to Edit this item?
         """
         return self._db.security.hasPermission('Edit', self._client.userid,
             self._classname, itemid=self._nodeid)
 
+    def is_retire_ok(self):
+        """ Is the user allowed to Reture this item?
+        """
+        return self._db.security.hasPermission('Retire', 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 this item?
         """
         if self._db.security.hasPermission('View', self._client.userid,
                 self._classname, itemid=self._nodeid):
@@ -789,7 +802,7 @@
         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) this item?
         """
         return self.is_view_ok() and not self.is_edit_ok()
 
@@ -878,7 +891,7 @@
             prop = self[prop_n]
             if not isinstance(prop, HTMLProperty):
                 continue
-            current[prop_n] = prop.plain()
+            current[prop_n] = prop.plain(escape=1)
             # make link if hrefable
             if (self._props.has_key(prop_n) and
                     isinstance(self._props[prop_n], hyperdb.Link)):
@@ -979,6 +992,7 @@
                                     if labelprop is not None and \
                                             labelprop != 'id':
                                         label = linkcl.get(linkid, labelprop)
+                                        label = cgi.escape(label)
                                 except IndexError:
                                     comments['no_link'] = self._(
                                         "<strike>The linked node"
@@ -1002,7 +1016,8 @@
                         # there's no labelprop!
                         if labelprop is not None and labelprop != 'id':
                             try:
-                                label = linkcl.get(args[k], labelprop)
+                                label = cgi.escape(linkcl.get(args[k],
+                                    labelprop))
                             except IndexError:
                                 comments['no_link'] = self._(
                                     "<strike>The linked node"
@@ -1012,7 +1027,8 @@
                                 label = None
                         if label is not None:
                             if hrefable:
-                                old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
+                                old = '<a href="%s%s">%s</a>'%(classname,
+                                    args[k], label)
                             else:
                                 old = label;
                             cell.append('%s: %s' % (self._(k), old))
@@ -1113,7 +1129,10 @@
 
         # new template, using the specified classname and request
         pt = self._client.instance.templates.get(req.classname, 'search')
-
+        # The context for a search page should be the class, not any
+        # node.
+        self._client.nodeid = None
+        
         # use our fabricated request
         return pt.render(self._client, req.classname, req)
 
@@ -1203,6 +1222,25 @@
         else:
             self._formname = name
 
+        # If no value is already present for this property, see if one
+        # is specified in the current form.
+        form = self._client.form
+        if not self._value and form.has_key(self._formname):
+            if isinstance(prop, hyperdb.Multilink):
+                value = lookupIds(self._db, prop,
+                                  handleListCGIValue(form[self._formname]),
+                                  fail_ok=1)
+            elif isinstance(prop, hyperdb.Link):
+                value = form.getfirst(self._formname).strip()
+                if value:
+                    value = lookupIds(self._db, prop, [value],
+                                      fail_ok=1)[0]
+                else:
+                    value = None
+            else:
+                value = form.getfirst(self._formname).strip() or None
+            self._value = value
+
         HTMLInputMixin.__init__(self)
 
     def __repr__(self):
@@ -1242,9 +1280,27 @@
         return self.is_edit_ok()
 
 class StringHTMLProperty(HTMLProperty):
-    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+)))')
+    hyper_re = re.compile(r'''(
+        (?P<url>
+         (
+          (ht|f)tp(s?)://                   # protocol
+          ([\w]+(:\w+)?@)?                  # username/password
+          ([\w\-]+)                         # hostname
+          ((\.[\w-]+)+)?                    # .domain.etc
+         |                                  # ... or ...
+          ([\w]+(:\w+)?@)?                  # username/password
+          www\.                             # "www."
+          ([\w\-]+\.)+                      # hostname
+          [\w]{2,5}                         # TLD
+         )
+         (:[\d]{1,5})?                     # port
+         (/[\w\-$.+!*(),;:@&=?/~\\#%]*)?   # path etc.
+        )|
+        (?P<email>[-+=%/\w\.]+@[\w\.\-]+)|
+        (?P<item>(?P<class>[A-Za-z_]+)(\s*)(?P<id>\d+))
+    )''', re.X | re.I)
+    protocol_re = re.compile('^(ht|f)tp(s?)://', re.I)
+
     def _hyper_repl_item(self,match,replacement):
         item = match.group('item')
         cls = match.group('class').lower()
@@ -1260,8 +1316,16 @@
 
     def _hyper_repl(self, match):
         if match.group('url'):
-            s = match.group('url')
-            return '<a href="%s">%s</a>'%(s, s)
+            u = s = match.group('url')
+            if not self.protocol_re.search(s):
+                u = 'http://' + s
+            # catch an escaped ">" at the end of the URL
+            if s.endswith('&gt;'):
+                u = s = s[:-4]
+                e = '&gt;'
+            else:
+                e = ''
+            return '<a href="%s">%s</a>%s'%(u, s, e)
         elif match.group('email'):
             s = match.group('email')
             return '<a href="mailto:%s">%s</a>'%(s, s)
@@ -1283,14 +1347,14 @@
         """ Render a "hyperlinked" version of the text """
         return self.plain(hyperlink=1)
 
-    def plain(self, escape=0, hyperlink=0, unchecked=0):
+    def plain(self, escape=0, hyperlink=0):
         """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() and not unchecked:
+        if not self.is_view_ok():
             return self._('[hidden]')
 
         if self._value is None:
@@ -1360,7 +1424,7 @@
         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",
+        return ReStructuredText(s, writer_name="html")["html_body"].encode("utf-8",
             "replace")
 
     def field(self, **kwargs):
@@ -1369,7 +1433,7 @@
             If not editable, just display the value via plain().
         """
         if not self.is_edit_ok():
-            return self.plain()
+            return self.plain(escape=1)
 
         value = self._value
         if value is None:
@@ -1423,7 +1487,7 @@
         return value
 
 class PasswordHTMLProperty(HTMLProperty):
-    def plain(self):
+    def plain(self, escape=0):
         """ Render a "plain" representation of the property
         """
         if not self.is_view_ok():
@@ -1439,7 +1503,7 @@
             If not editable, just display the value via plain().
         """
         if not self.is_edit_ok():
-            return self.plain()
+            return self.plain(escape=1)
 
         return self.input(type="password", name=self._formname, size=size)
 
@@ -1459,7 +1523,7 @@
             size=size)
 
 class NumberHTMLProperty(HTMLProperty):
-    def plain(self):
+    def plain(self, escape=0):
         """ Render a "plain" representation of the property
         """
         if not self.is_view_ok():
@@ -1476,7 +1540,7 @@
             If not editable, just display the value via plain().
         """
         if not self.is_edit_ok():
-            return self.plain()
+            return self.plain(escape=1)
 
         value = self._value
         if value is None:
@@ -1496,7 +1560,7 @@
 
 
 class BooleanHTMLProperty(HTMLProperty):
-    def plain(self):
+    def plain(self, escape=0):
         """ Render a "plain" representation of the property
         """
         if not self.is_view_ok():
@@ -1512,7 +1576,7 @@
             If not editable, just display the value via plain().
         """
         if not self.is_edit_ok():
-            return self.plain()
+            return self.plain(escape=1)
 
         value = self._value
         if isinstance(value, str) or isinstance(value, unicode):
@@ -1549,7 +1613,7 @@
         if self._offset is None :
             self._offset = self._prop.offset (self._db)
 
-    def plain(self):
+    def plain(self, escape=0):
         """ Render a "plain" representation of the property
         """
         if not self.is_view_ok():
@@ -1600,7 +1664,7 @@
         """
         if not self.is_edit_ok():
             if format is self._marker:
-                return self.plain()
+                return self.plain(escape=1)
             else:
                 return self.pretty(format)
 
@@ -1708,7 +1772,7 @@
         else :
             date = ""
         return ('<a class="classhelp" href="javascript:help_window('
-            "'%s?@template=calendar&property=%s&form=%s%s', %d, %d)"
+            "'%s?@template=calendar&amp;property=%s&amp;form=%s%s', %d, %d)"
             '">%s</a>'%(self._classname, self._name, form, date, width,
             height, label))
 
@@ -1720,7 +1784,7 @@
         if self._value and not isinstance(self._value, (str, unicode)):
             self._value.setTranslator(self._client.translator)
 
-    def plain(self):
+    def plain(self, escape=0):
         """ Render a "plain" representation of the property
         """
         if not self.is_view_ok():
@@ -1744,7 +1808,7 @@
             If not editable, just display the value via plain().
         """
         if not self.is_edit_ok():
-            return self.plain()
+            return self.plain(escape=1)
 
         value = self._value
         if value is None:
@@ -1793,7 +1857,10 @@
         linkcl = self._db.classes[self._prop.classname]
         k = linkcl.labelprop(1)
         if num_re.match(self._value):
-            value = str(linkcl.get(self._value, k))
+            try:
+                value = str(linkcl.get(self._value, k))
+            except IndexError:
+                value = self._value
         else :
             value = self._value
         if escape:
@@ -1806,7 +1873,7 @@
             If not editable, just display the value via plain().
         """
         if not self.is_edit_ok():
-            return self.plain()
+            return self.plain(escape=1)
 
         # edit field
         linkcl = self._db.getclass(self._prop.classname)
@@ -1842,10 +1909,16 @@
             If not editable, just display the value via plain().
         """
         if not self.is_edit_ok():
-            return self.plain()
+            return self.plain(escape=1)
 
+        # Since None indicates the default, we need another way to
+        # indicate "no selection".  We use -1 for this purpose, as
+        # that is the value we use when submitting a form without the
+        # value set.
         if value is None:
             value = self._value
+        elif value == '-1':
+            value = None
 
         linkcl = self._db.getclass(self._prop.classname)
         l = ['<select name="%s">'%self._formname]
@@ -1862,7 +1935,7 @@
                 else:
                     sort_on = ('+', sort_on)
         else:
-            sort_on = ('+', find_sort_key(linkcl))
+            sort_on = ('+', linkcl.orderprop())
 
         options = [opt
             for opt in linkcl.filter(None, conditions, sort_on, (None, None))
@@ -1873,6 +1946,21 @@
         if value and value not in options:
             options.insert(0, value)
 
+        if additional:
+            additional_fns = []
+            props = linkcl.getprops()
+            for propname in additional:
+                prop = props[propname]
+                if isinstance(prop, hyperdb.Link):
+                    cl = self._db.getclass(prop.classname)
+                    labelprop = cl.labelprop()
+                    fn = lambda optionid: cl.get(linkcl.get(optionid,
+                                                            propname),
+                                                 labelprop)
+                else:
+                    fn = lambda optionid: linkcl.get(optionid, propname)
+            additional_fns.append(fn)
+            
         for optionid in options:
             # get the option value, and if it's None use an empty string
             option = linkcl.get(optionid, k) or ''
@@ -1895,9 +1983,9 @@
                 lab = lab[:size-3] + '...'
             if additional:
                 m = []
-                for propname in additional:
-                    m.append(linkcl.get(optionid, propname))
-                lab = lab + ' (%s)'%', '.join(map(str, m))
+                for fn in additional_fns:
+                    m.append(str(fn(optionid)))
+                lab = lab + ' (%s)'%', '.join(m)
 
             # and generate
             lab = cgi.escape(self._(lab))
@@ -1984,9 +2072,15 @@
         k = linkcl.labelprop(1)
         labels = []
         for v in self._value:
-            label = linkcl.get(v, k)
-            # fall back to designator if label is None
-            if label is None: label = '%s%s'%(self._prop.classname, k)
+            if num_re.match(v):
+                try:
+                    label = linkcl.get(v, k)
+                except IndexError:
+                    label = None
+                # fall back to designator if label is None
+                if label is None: label = '%s%s'%(self._prop.classname, k)
+            else:
+                label = v
             labels.append(label)
         value = ', '.join(labels)
         if escape:
@@ -1999,7 +2093,7 @@
             If not editable, just display the value via plain().
         """
         if not self.is_edit_ok():
-            return self.plain()
+            return self.plain(escape=1)
 
         linkcl = self._db.getclass(self._prop.classname)
         value = self._value[:]
@@ -2034,7 +2128,7 @@
             If not editable, just display the value via plain().
         """
         if not self.is_edit_ok():
-            return self.plain()
+            return self.plain(escape=1)
 
         if value is None:
             value = self._value
@@ -2048,21 +2142,46 @@
                 else:
                     sort_on = ('+', sort_on)
         else:
-            sort_on = ('+', find_sort_key(linkcl))
+            sort_on = ('+', linkcl.orderprop())
 
         options = [opt
             for opt in linkcl.filter(None, conditions, sort_on)
             if self._db.security.hasPermission("View", self._client.userid,
                 linkcl.classname, itemid=opt)]
-        height = height or min(len(options), 7)
-        l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
-        k = linkcl.labelprop(1)
-
+        
         # make sure we list the current values if they're retired
         for val in value:
             if val not in options:
                 options.insert(0, val)
 
+        if not height:
+            height = len(options)
+            if value:
+                # The "no selection" option.
+                height += 1
+            height = min(height, 7)
+        l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
+        k = linkcl.labelprop(1)
+
+        if value:
+            l.append('<option value="%s">- no selection -</option>'
+                     % ','.join(['-' + v for v in value]))
+
+        if additional:
+            additional_fns = []
+            props = linkcl.getprops()
+            for propname in additional:
+                prop = props[propname]
+                if isinstance(prop, hyperdb.Link):
+                    cl = self._db.getclass(prop.classname)
+                    labelprop = cl.labelprop()
+                    fn = lambda optionid: cl.get(linkcl.get(optionid,
+                                                            propname),
+                                                 labelprop)
+                else:
+                    fn = lambda optionid: linkcl.get(optionid, propname)
+            additional_fns.append(fn)
+            
         for optionid in options:
             # get the option value, and if it's None use an empty string
             option = linkcl.get(optionid, k) or ''
@@ -2082,8 +2201,8 @@
                 lab = lab[:size-3] + '...'
             if additional:
                 m = []
-                for propname in additional:
-                    m.append(linkcl.get(optionid, propname))
+                for fn in additional_fns:
+                    m.append(str(fn(optionid)))
                 lab = lab + ' (%s)'%', '.join(m)
 
             # and generate
@@ -2110,17 +2229,11 @@
     """
     linkcl = db.getclass(classname)
     if sort_on is None:
-        sort_on = find_sort_key(linkcl)
+        sort_on = linkcl.orderprop()
     def sortfunc(a, b):
         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
     return sortfunc
 
-def find_sort_key(linkcl):
-    if linkcl.getprops().has_key('order'):
-        return 'order'
-    else:
-        return linkcl.labelprop()
-
 def handleListCGIValue(value):
     """ Value is either a single item or a list of items. Each item has a
         .value that we're actually interested in.
@@ -2140,6 +2253,7 @@
     - "env" the CGI environment variables
     - "base" the base URL for this instance
     - "user" a HTMLItem instance for this user
+    - "language" as determined by the browser or config
     - "classname" the current classname (possibly None)
     - "template" the current template (suffix, also possibly None)
 
@@ -2166,6 +2280,7 @@
         self.env = client.env
         self.base = client.base
         self.user = HTMLItem(client, 'user', client.userid)
+        self.language = client.language
 
         # store the current class name and action
         self.classname = client.classname
@@ -2200,10 +2315,10 @@
             key = '%s%s%d'%(special, name, idx)
             while key in self.form:
                 self.special_char = special
-                fields.append (self.form[key].value)
+                fields.append(self.form.getfirst(key))
                 dirkey = '%s%sdir%d'%(special, name, idx)
                 if dirkey in self.form:
-                    dirs.append(self.form[dirkey].value)
+                    dirs.append(self.form.getfirst(dirkey))
                 else:
                     dirs.append(None)
                 idx += 1
@@ -2214,7 +2329,7 @@
             if key in self.form and not fields:
                 fields = handleListCGIValue(self.form[key])
                 if dirkey in self.form:
-                    dirs.append(self.form[dirkey].value)
+                    dirs.append(self.form.getfirst(dirkey))
             if fields: # only try other special char if nothing found
                 break
         for f, d in map(None, fields, dirs):
@@ -2261,9 +2376,8 @@
                 fv = self.form[name]
                 if (isinstance(prop, hyperdb.Link) or
                         isinstance(prop, hyperdb.Multilink)):
-                    ids = lookupIds(db, prop, handleListCGIValue(fv))
-                    if ids :
-                        self.filterspec[name] = ids
+                    self.filterspec[name] = lookupIds(db, prop,
+                        handleListCGIValue(fv))
                 else:
                     if isinstance(fv, type([])):
                         self.filterspec[name] = [v.value for v in fv]
@@ -2278,13 +2392,7 @@
         for name in ':search_text @search_text'.split():
             if self.form.has_key(name):
                 self.special_char = name[0]
-                try:
-                    self.search_text = self.form[name].value
-                except AttributeError:
-                    # http://psf.upfronthosting.co.za/roundup/meta/issue111
-                    # Multiple search_text, probably some kind of spambot.
-                    # Use first value.
-                    self.search_text = self.form[name][0].value
+                self.search_text = self.form.getfirst(name)
 
         # pagination - size and start index
         # figure batch args
@@ -2292,17 +2400,17 @@
         for name in ':pagesize @pagesize'.split():
             if self.form.has_key(name):
                 self.special_char = name[0]
-                self.pagesize = int(self.form[name].value)
+                self.pagesize = int(self.form.getfirst(name))
 
         self.startwith = 0
         for name in ':startwith @startwith'.split():
             if self.form.has_key(name):
                 self.special_char = name[0]
-                self.startwith = int(self.form[name].value)
+                self.startwith = int(self.form.getfirst(name))
 
         # dispname
         if self.form.has_key('@dispname'):
-            self.dispname = self.form['@dispname'].value
+            self.dispname = self.form.getfirst('@dispname')
         else:
             self.dispname = None
 
@@ -2394,10 +2502,10 @@
         if filter and self.filter:
             add(sc+'filter', ','.join(self.filter))
         if self.classname and filterspec:
-            props = self.client.db.getclass(self.classname).getprops()
+            cls = self.client.db.getclass(self.classname)
             for k,v in self.filterspec.items():
                 if type(v) == type([]):
-                    if isinstance(props[k], hyperdb.String):
+                    if isinstance(cls.get_transitive_prop(k), hyperdb.String):
                         add(k, ' '.join(v))
                     else:
                         add(k, ','.join(v))

Modified: tracker/roundup-src/roundup/cgi/wsgi_handler.py
==============================================================================
--- tracker/roundup-src/roundup/cgi/wsgi_handler.py	(original)
+++ tracker/roundup-src/roundup/cgi/wsgi_handler.py	Sun Mar 15 22:43:30 2009
@@ -37,27 +37,28 @@
     def __call__(self, environ, start_response):
         """Initialize with `apache.Request` object"""
         self.environ = environ
-        self.__start_response = start_response
+        request = RequestDispatcher(self.home, self.debug, self.timing)
+        request.__start_response = start_response
 
-        self.wfile = Writer(self)
-        self.__wfile = None
+        request.wfile = Writer(request)
+        request.__wfile = None
 
         tracker = roundup.instance.open(self.home, not self.debug)
 
         # need to strip the leading '/'
         environ["PATH_INFO"] = environ["PATH_INFO"][1:]
-        if self.timing:
-            environ["CGI_SHOW_TIMING"] = self.timing
+        if request.timing:
+            environ["CGI_SHOW_TIMING"] = request.timing
 
         form = cgi.FieldStorage(fp=environ['wsgi.input'], environ=environ)
 
-        client = tracker.Client(tracker, self, environ, form,
-            self.translator)
+        client = tracker.Client(tracker, request, environ, form,
+            request.translator)
         try:
             client.main()
         except roundup.cgi.client.NotFound:
-            self.start_response([('Content-Type', 'text/html')], 404)
-            self.wfile.write('Not found: %s'%client.path)
+            request.start_response([('Content-Type', 'text/html')], 404)
+            request.wfile.write('Not found: %s'%client.path)
 
         # all body data has been written using wfile
         return []

Modified: tracker/roundup-src/roundup/configuration.py
==============================================================================
--- tracker/roundup-src/roundup/configuration.py	(original)
+++ tracker/roundup-src/roundup/configuration.py	Sun Mar 15 22:43:30 2009
@@ -1,6 +1,6 @@
 # Roundup Issue Tracker configuration support
 #
-# $Id: configuration.py,v 1.50 2007/11/14 14:57:47 schlatterbeck Exp $
+# $Id: configuration.py,v 1.51 2008-09-01 02:30:06 richard Exp $
 #
 __docformat__ = "restructuredtext"
 
@@ -370,7 +370,7 @@
 
 class NullableOption(Option):
 
-    """Option that is set to None if it's string value is one of NULL strings
+    """Option that is set to None if its string value is one of NULL strings
 
     Default nullable strings list contains empty string only.
     There is constructor parameter allowing to specify different nullables.
@@ -478,12 +478,16 @@
             "specified above.  If this option is not set, all static\n"
             "files are taken from the TEMPLATES directory"),
         (MailAddressOption, "admin_email", "roundup-admin",
-            "Email address that roundup will complain to"
-            " if it runs into trouble."),
+            "Email address that roundup will complain to if it runs\n"
+            "into trouble.\n"
+            "If no domain is specified then the config item\n"
+            "mail -> domain is added."),
         (MailAddressOption, "dispatcher_email", "roundup-admin",
             "The 'dispatcher' is a role that can get notified\n"
             "of new items to the database.\n"
-            "It is used by the ERROR_MESSAGES_TO config setting."),
+            "It is used by the ERROR_MESSAGES_TO config setting.\n"
+            "If no domain is specified then the config item\n"
+            "mail -> domain is added."),
         (Option, "email_from_tag", "",
             "Additional text to include in the \"name\" part\n"
             "of the From: address used in nosy messages.\n"
@@ -538,7 +542,8 @@
             "that is required to get to the home page of the tracker.\n"
             "You MUST include a trailing '/' in the URL."),
         (MailAddressOption, "email", "issue_tracker",
-            "Email address that mail to roundup should go to."),
+            "Email address that mail to roundup should go to.\n"
+            "If no domain is specified then mail_domain is added."),
         (NullableOption, "language", "",
             "Default locale name for this tracker.\n"
             "If this option is not set, the language is determined\n"
@@ -546,6 +551,11 @@
             "or LANG, in that order of preference."),
     )),
     ("web", (
+        (BooleanOption, "allow_html_file", "no",
+            "Setting this option enables Roundup to serve uploaded HTML\n"
+            "file content *as HTML*. This is a potential security risk\n"
+            "and is therefore disabled by default. Set to 'yes' if you\n"
+            "trust *all* users uploading content to your tracker."),
         (BooleanOption, 'http_auth', "yes",
             "Whether to use HTTP Basic Authentication, if present.\n"
             "Roundup will use either the REMOTE_USER or HTTP_AUTHORIZATION\n"
@@ -587,8 +597,10 @@
         (NullableOption, 'read_default_group', 'roundup',
             "Name of the group to use in the MySQL defaults file (.my.cnf).\n"
             "Only used in MySQL connections."),
+        (IntegerNumberOption, 'cache_size', '100',
+            "Size of the node cache (in elements)"),
     ), "Settings in this section are used"
-        " by Postgresql and MySQL backends only"
+        " by RDBMS backends only"
     ),
     ("logging", (
         (FilePathOption, "config", "",
@@ -606,7 +618,12 @@
             "Allowed values: DEBUG, INFO, WARNING, ERROR"),
     )),
     ("mail", (
-        (Option, "domain", NODEFAULT, "Domain name used for email addresses."),
+        (Option, "domain", NODEFAULT,
+            "The email domain that admin_email, issue_tracker and\n"
+            "dispatcher_email belong to.\n"
+            "This domain is added to those config items if they don't\n"
+            "explicitly include a domain.\n"
+            "Do not include the '@' symbol."),
         (Option, "host", NODEFAULT,
             "SMTP mail host that roundup will use to send mail",
             ["MAILHOST"],),
@@ -864,7 +881,7 @@
             _options.append(_name)
         # (section, name) key is used for writing .ini file
         self.options[(_section, _name)] = option
-        # make the option known under all of it's A.K.A.s
+        # make the option known under all of its A.K.A.s
         for _name in option.aliases:
             self.options[_name] = option
 

Modified: tracker/roundup-src/roundup/date.py
==============================================================================
--- tracker/roundup-src/roundup/date.py	(original)
+++ tracker/roundup-src/roundup/date.py	Sun Mar 15 22:43:30 2009
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-# $Id: date.py,v 1.94 2007/12/23 00:23:23 richard Exp $
+# $Id: date.py,v 1.94 2007-12-23 00:23:23 richard Exp $
 
 """Date, time and time interval handling.
 """
@@ -295,7 +295,10 @@
 
         info = m.groupdict()
 
-        # determine whether we need to add anything at the end
+        # If add_granularity is true, construct the maximum time given
+        # the precision of the input.  For example, given the input
+        # "12:15", construct "12:15:59".  Or, for "2008", construct
+        # "2008-12-31.23:59:59".
         if add_granularity:
             for gran in 'SMHdmy':
                 if info[gran] is not None:
@@ -308,6 +311,8 @@
                     else:
                         add_granularity = Interval('+1%s'%gran)
                     break
+            else:
+                raise ValueError(self._('Could not determine granularity'))
 
         # get the current date as our default
         dt = datetime.datetime.utcnow()

Added: tracker/roundup-src/roundup/dist/__init__.py
==============================================================================

Added: tracker/roundup-src/roundup/dist/command/__init__.py
==============================================================================

Added: tracker/roundup-src/roundup/dist/command/bdist_rpm.py
==============================================================================
--- (empty file)
+++ tracker/roundup-src/roundup/dist/command/bdist_rpm.py	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,33 @@
+#
+# Copyright (C) 2009 Stefan Seefeld
+# All rights reserved.
+# For license terms see the file COPYING.txt.
+#
+from distutils.command.bdist_rpm import bdist_rpm as base
+from distutils.file_util import write_file
+import os
+
+class bdist_rpm(base):
+
+    def finalize_options(self):
+        base.finalize_options(self)
+        if self.install_script:
+            # install script is overridden.  skip default
+            return
+        # install script option must be file name.
+        # create the file in rpm build directory.
+        install_script = os.path.join(self.rpm_base, "install.sh")
+        self.mkpath(self.rpm_base)
+        self.execute(write_file, (install_script, [
+                ("%s setup.py install --root=$RPM_BUILD_ROOT "
+                    "--record=ROUNDUP_FILES") % self.python,
+                # allow any additional extension for man pages
+                # (rpm may compress them to .gz or .bz2)
+                # man page here is any file
+                # with single-character extension
+                # in man directory
+                "sed -e 's,\(/man/.*\..\)$,\\1*,' "
+                    "<ROUNDUP_FILES >INSTALLED_FILES",
+            ]), "writing '%s'" % install_script)
+        self.install_script = install_script
+

Added: tracker/roundup-src/roundup/dist/command/build.py
==============================================================================
--- (empty file)
+++ tracker/roundup-src/roundup/dist/command/build.py	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,62 @@
+#
+# Copyright (C) 2009 Stefan Seefeld
+# All rights reserved.
+# For license terms see the file COPYING.txt.
+#
+from roundup import msgfmt
+from distutils.command.build import build as base
+import os
+from glob import glob
+
+def list_message_files(suffix=".po"):
+    """Return list of all found message files and their intallation paths"""
+    _files = glob("locale/*" + suffix)
+    _list = []
+    for _file in _files:
+        # basename (without extension) is a locale name
+        _locale = os.path.splitext(os.path.basename(_file))[0]
+        _list.append((_file, os.path.join(
+            "share", "locale", _locale, "LC_MESSAGES", "roundup.mo")))
+    return _list
+
+def check_manifest():
+    """Check that the files listed in the MANIFEST are present when the
+    source is unpacked.
+    """
+    try:
+        f = open('MANIFEST')
+    except:
+        print '\n*** SOURCE WARNING: The MANIFEST file is missing!'
+        return
+    try:
+        manifest = [l.strip() for l in f.readlines()]
+    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)!'%(
+            n-len(err), n)
+        print 'Missing:', '\nMissing: '.join(err)
+
+
+class build(base):
+
+    def build_message_files(self):
+        """For each locale/*.po, build .mo file in target locale directory"""
+        for (_src, _dst) in list_message_files():
+            _build_dst = os.path.join("build", _dst)
+            self.mkpath(os.path.dirname(_build_dst))
+            self.announce("Compiling %s -> %s" % (_src, _build_dst))
+            msgfmt.make(_src, _build_dst)
+
+    def run(self):
+        check_manifest()
+        self.build_message_files()
+        base.run(self)
+

Added: tracker/roundup-src/roundup/dist/command/build_doc.py
==============================================================================
--- (empty file)
+++ tracker/roundup-src/roundup/dist/command/build_doc.py	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,37 @@
+#
+# Copyright (C) 2009 Stefan Seefeld
+# All rights reserved.
+# For license terms see the file COPYING.txt.
+#
+
+import os, sys
+from stat import *
+import os.path
+from shutil import *
+import glob
+
+from distutils.command import build
+from distutils.spawn import spawn, find_executable
+from distutils.dep_util import newer, newer_group
+from distutils.dir_util import copy_tree, remove_tree, mkpath
+from distutils.file_util import copy_file
+from distutils import sysconfig
+
+class build_doc(build.build):
+    """Defines the specific procedure to build roundup's documentation."""
+
+    description = "build documentation"
+
+    def run(self):
+        """Run this command, i.e. do the actual document generation."""
+
+        sphinx = find_executable('sphinx-build')
+        if not sphinx:
+            self.warn("could not find sphinx-build in PATH")
+            self.warn("cannot build documentation")
+            return
+
+        doc_dir = os.path.join('share', 'doc', 'roundup', 'html')
+        temp_dir = os.path.join(self.build_temp, 'doc')
+        cmd = [sphinx, '-d', temp_dir, 'doc', doc_dir]
+        spawn(cmd)

Added: tracker/roundup-src/roundup/dist/command/build_py.py
==============================================================================
--- (empty file)
+++ tracker/roundup-src/roundup/dist/command/build_py.py	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,23 @@
+#
+# Copyright (C) 2009 Stefan Seefeld
+# All rights reserved.
+# For license terms see the file COPYING.txt.
+#
+from distutils.command.build_py import build_py
+
+class build_py(build_py):
+
+    def find_modules(self):
+        # Files listed in py_modules are in the toplevel directory
+        # of the source distribution.
+        modules = []
+        for module in self.py_modules:
+            path = module.split('.')
+            package = '.'.join(path[0:-1])
+            module_base = path[-1]
+            module_file = module_base + '.py'
+            if self.check_module(module, module_file):
+                modules.append((package, module_base, module_file))
+        return modules
+
+

Added: tracker/roundup-src/roundup/dist/command/build_scripts.py
==============================================================================
--- (empty file)
+++ tracker/roundup-src/roundup/dist/command/build_scripts.py	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,142 @@
+#
+# Copyright (C) 2009 Stefan Seefeld
+# All rights reserved.
+# For license terms see the file COPYING.txt.
+#
+from distutils.command.build_scripts import build_scripts as base
+import sys, os, string
+
+class build_scripts(base):
+    """ Overload the build_scripts command and create the scripts
+        from scratch, depending on the target platform.
+
+        You have to define the name of your package in an inherited
+        class (due to the delayed instantiation of command classes
+        in distutils, this cannot be passed to __init__).
+
+        The scripts are created in an uniform scheme: they start the
+        run() function in the module
+
+            <packagename>.scripts.<mangled_scriptname>
+
+        The mangling of script names replaces '-' and '/' characters
+        with '-' and '.', so that they are valid module paths.
+
+        If the target platform is win32, create .bat files instead of
+        *nix shell scripts.  Target platform is set to "win32" if main
+        command is 'bdist_wininst' or if the command is 'bdist' and
+        it has the list of formats (from command line or config file)
+        and the first item on that list is wininst.  Otherwise
+        target platform is set to current (build) platform.
+    """
+    package_name = 'roundup'
+
+    def initialize_options(self):
+        base.initialize_options(self)
+        self.script_preamble = None
+        self.target_platform = None
+        self.python_executable = None
+
+    def finalize_options(self):
+        base.finalize_options(self)
+        cmdopt=self.distribution.command_options
+
+        # find the target platform
+        if self.target_platform:
+            # TODO? allow explicit setting from command line
+            target = self.target_platform
+        if cmdopt.has_key("bdist_wininst"):
+            target = "win32"
+        elif cmdopt.get("bdist", {}).has_key("formats"):
+            formats = cmdopt["bdist"]["formats"][1].split(",")
+            if formats[0] == "wininst":
+                target = "win32"
+            else:
+                target = sys.platform
+            if len(formats) > 1:
+                self.warn(
+                    "Scripts are built for %s only (requested formats: %s)"
+                    % (target, ",".join(formats)))
+        else:
+            # default to current platform
+            target = sys.platform
+        self.target_platfom = target
+
+        # for native builds, use current python executable path;
+        # for cross-platform builds, use default executable name
+        if self.python_executable:
+            # TODO? allow command-line option
+            pass
+        if target == sys.platform:
+            self.python_executable = os.path.normpath(sys.executable)
+        else:
+            self.python_executable = "python"
+
+        # for windows builds, add ".bat" extension
+        if target == "win32":
+            # *nix-like scripts may be useful also on win32 (cygwin)
+            # to build both script versions, use:
+            #self.scripts = list(self.scripts) + [script + ".bat"
+            #    for script in self.scripts]
+            self.scripts = [script + ".bat" for script in self.scripts]
+
+        # tweak python path for installations outside main python library
+        if cmdopt.get("install", {}).has_key("prefix"):
+            prefix = os.path.expanduser(cmdopt['install']['prefix'][1])
+            version = '%d.%d'%sys.version_info[:2]
+            self.script_preamble = """
+import sys
+sys.path.insert(1, "%s/lib/python%s/site-packages")
+"""%(prefix, version)
+        else:
+            self.script_preamble = ''
+
+    def copy_scripts(self):
+        """ Create each script listed in 'self.scripts'
+        """
+
+        to_module = string.maketrans('-/', '_.')
+
+        self.mkpath(self.build_dir)
+        for script in self.scripts:
+            outfile = os.path.join(self.build_dir, os.path.basename(script))
+
+            #if not self.force and not newer(script, outfile):
+            #    self.announce("not copying %s (up-to-date)" % script)
+            #    continue
+
+            if self.dry_run:
+                self.announce("would create %s" % outfile)
+                continue
+
+            module = os.path.splitext(os.path.basename(script))[0]
+            module = string.translate(module, to_module)
+            script_vars = {
+                'python': self.python_executable,
+                'package': self.package_name,
+                'module': module,
+                'prefix': self.script_preamble,
+            }
+
+            self.announce("creating %s" % outfile)
+            file = open(outfile, 'w')
+
+            try:
+                # could just check self.target_platform,
+                # but looking at the script extension
+                # makes it possible to build both *nix-like
+                # and windows-like scripts on win32.
+                # may be useful for cygwin.
+                if os.path.splitext(outfile)[1] == ".bat":
+                    file.write('@echo off\n'
+                        'if NOT "%%_4ver%%" == "" "%(python)s" -c "from %(package)s.scripts.%(module)s import run; run()" %%$\n'
+                        'if     "%%_4ver%%" == "" "%(python)s" -c "from %(package)s.scripts.%(module)s import run; run()" %%*\n'
+                        % script_vars)
+                else:
+                    file.write('#! %(python)s\n%(prefix)s'
+                        'from %(package)s.scripts.%(module)s import run\n'
+                        'run()\n'
+                        % script_vars)
+            finally:
+                file.close()
+                os.chmod(outfile, 0755)

Modified: tracker/roundup-src/roundup/exceptions.py
==============================================================================
--- tracker/roundup-src/roundup/exceptions.py	(original)
+++ tracker/roundup-src/roundup/exceptions.py	Sun Mar 15 22:43:30 2009
@@ -1,11 +1,16 @@
-#$Id: exceptions.py,v 1.1 2004/03/26 00:44:11 richard Exp $
-'''Exceptions for use across all Roundup components.
-'''
+"""Exceptions for use across all Roundup components.
+"""
 
 __docformat__ = 'restructuredtext'
 
+class LoginError(Exception):
+    pass
+
+class Unauthorised(Exception):
+    pass
+
 class Reject(Exception):
-    '''An auditor may raise this exception when the current create or set
+    """An auditor may raise this exception when the current create or set
     operation should be stopped.
 
     It is up to the specific interface invoking the create or set to
@@ -13,7 +18,10 @@
 
     - mailgw will trap and ignore Reject for file attachments and messages
     - cgi will trap and present the exception in a nice format
-    '''
+    """
+    pass
+
+class UsageError(ValueError):
     pass
 
 # vim: set filetype=python ts=4 sw=4 et si

Modified: tracker/roundup-src/roundup/hyperdb.py
==============================================================================
--- tracker/roundup-src/roundup/hyperdb.py	(original)
+++ tracker/roundup-src/roundup/hyperdb.py	Sun Mar 15 22:43:30 2009
@@ -15,7 +15,6 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-# $Id: hyperdb.py,v 1.131 2007/09/27 06:18:53 jpend Exp $
 
 """Hyperdatabase implementation, especially field types.
 """
@@ -23,7 +22,8 @@
 
 # standard python modules
 import os, re, shutil, weakref
-from sets import Set
+# Python 2.3 ... 2.6 compatibility:
+from roundup.anypy.sets_ import set
 
 # roundup modules
 import date, password
@@ -53,8 +53,12 @@
     def __init__(self, indexme='no', required=False):
         super(String, self).__init__(required)
         self.indexme = indexme == 'yes'
-    def from_raw(self, value, **kw):
+    def from_raw(self, value, propname='', **kw):
         """fix the CRLF/CR -> LF stuff"""
+        if propname == 'content':
+            # Why oh why wasn't the FileClass content property a File
+            # type from the beginning?
+            return value
         return fixNewlines(value)
     def sort_repr (self, cls, val, name):
         if not val:
@@ -133,8 +137,8 @@
     """An object designating a Pointer property that links or multilinks
     to a node in a specified class."""
     def __init__(self, classname, do_journal='yes', required=False):
-        ''' Default is to journal link and unlink events
-        '''
+        """ Default is to journal link and unlink events
+        """
         super(_Pointer, self).__init__(required)
         self.classname = classname
         self.do_journal = do_journal == 'yes'
@@ -190,7 +194,7 @@
         # definitely in the new list (in case of e.g.
         # <propname>=A,+B, which should replace the old
         # list with A,B)
-        set = 1
+        do_set = 1
         newvalue = []
         for item in value:
             item = item.strip()
@@ -203,10 +207,10 @@
             if item.startswith('-'):
                 remove = 1
                 item = item[1:]
-                set = 0
+                do_set = 0
             elif item.startswith('+'):
                 item = item[1:]
-                set = 0
+                do_set = 0
 
             # look up the value
             itemid = convertLinkValue(db, propname, self, item)
@@ -225,7 +229,7 @@
 
         # that's it, set the new Multilink property value,
         # or overwrite it completely
-        if set:
+        if do_set:
             value = newvalue
         else:
             value = curvalue
@@ -269,15 +273,15 @@
 class DesignatorError(ValueError):
     pass
 def splitDesignator(designator, dre=re.compile(r'([^\d]+)(\d+)')):
-    ''' Take a foo123 and return ('foo', 123)
-    '''
+    """ Take a foo123 and return ('foo', 123)
+    """
     m = dre.match(designator)
     if m is None:
         raise DesignatorError, _('"%s" not a node designator')%designator
     return m.group(1), m.group(2)
 
 class Proptree(object):
-    ''' Simple tree data structure for optimizing searching of
+    """ Simple tree data structure for optimizing searching of
     properties. Each node in the tree represents a roundup Class
     Property that has to be navigated for finding the given search
     or sort properties. The sort_type attribute is used for
@@ -289,7 +293,7 @@
     The Proptree is also used for transitively searching attributes for
     backends that do not support transitive search (e.g. anydbm). The
     _val attribute with set_val is used for this.
-    '''
+    """
 
     def __init__(self, db, cls, name, props, parent = None):
         self.db = db
@@ -484,7 +488,7 @@
             v = self._val
             if not isinstance(self._val, type([])):
                 v = [self._val]
-            vals = Set(v)
+            vals = set(v)
             vals.intersection_update(val)
             self._val = [v for v in vals]
         else:
@@ -561,11 +565,11 @@
 # the base Database class
 #
 class DatabaseError(ValueError):
-    '''Error to be raised when there is some problem in the database code
-    '''
+    """Error to be raised when there is some problem in the database code
+    """
     pass
 class Database:
-    '''A database for storing records containing flexible data types.
+    """A database for storing records containing flexible data types.
 
 This class defines a hyperdatabase storage layer, which the Classes use to
 store their data.
@@ -588,7 +592,7 @@
 
 All methods except __repr__ must be implemented by a concrete backend Database.
 
-'''
+"""
 
     # flag to set on retired entries
     RETIRED_FLAG = '__hyperdb_retired'
@@ -630,8 +634,8 @@
         raise NotImplementedError
 
     def addclass(self, cl):
-        '''Add a Class to the hyperdatabase.
-        '''
+        """Add a Class to the hyperdatabase.
+        """
         raise NotImplementedError
 
     def getclasses(self):
@@ -646,14 +650,14 @@
         raise NotImplementedError
 
     def clear(self):
-        '''Delete all database contents.
-        '''
+        """Delete all database contents.
+        """
         raise NotImplementedError
 
     def getclassdb(self, classname, mode='r'):
-        '''Obtain a connection to the class db that will be used for
+        """Obtain a connection to the class db that will be used for
            multiple actions.
-        '''
+        """
         raise NotImplementedError
 
     def addnode(self, classname, nodeid, node):
@@ -662,73 +666,73 @@
         raise NotImplementedError
 
     def serialise(self, classname, node):
-        '''Copy the node contents, converting non-marshallable data into
+        """Copy the node contents, converting non-marshallable data into
            marshallable data.
-        '''
+        """
         return node
 
     def setnode(self, classname, nodeid, node):
-        '''Change the specified node.
-        '''
+        """Change the specified node.
+        """
         raise NotImplementedError
 
     def unserialise(self, classname, node):
-        '''Decode the marshalled node data
-        '''
+        """Decode the marshalled node data
+        """
         return node
 
     def getnode(self, classname, nodeid):
-        '''Get a node from the database.
+        """Get a node from the database.
 
         'cache' exists for backwards compatibility, and is not used.
-        '''
+        """
         raise NotImplementedError
 
     def hasnode(self, classname, nodeid):
-        '''Determine if the database has a given node.
-        '''
+        """Determine if the database has a given node.
+        """
         raise NotImplementedError
 
     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.
+        """
         raise NotImplementedError
 
     def storefile(self, classname, nodeid, property, content):
-        '''Store the content of the file in the database.
+        """Store the content of the file in the database.
 
            The property may be None, in which case the filename does not
            indicate which property is being saved.
-        '''
+        """
         raise NotImplementedError
 
     def getfile(self, classname, nodeid, property):
-        '''Store the content of the file in the database.
-        '''
+        """Get the content of the file in the database.
+        """
         raise NotImplementedError
 
     def addjournal(self, classname, nodeid, action, params):
-        ''' 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
-        '''
+        """
         raise NotImplementedError
 
     def getjournal(self, classname, nodeid):
-        ''' get the journal for id
-        '''
+        """ get the journal for id
+        """
         raise NotImplementedError
 
     def pack(self, pack_before):
-        ''' pack the database
-        '''
+        """ pack the database
+        """
         raise NotImplementedError
 
     def commit(self):
-        ''' Commit the current transactions.
+        """ Commit the current transactions.
 
         Save all data changed since the database was opened or since the
         last commit() or rollback().
@@ -738,15 +742,15 @@
         database. We don't care if there's a concurrency issue there.
 
         The only backend this seems to affect is postgres.
-        '''
+        """
         raise NotImplementedError
 
     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.
-        '''
+        """
         raise NotImplementedError
 
     def close(self):
@@ -794,8 +798,8 @@
         self.reactors = dict([(a, PrioList()) for a in actions])
 
     def __repr__(self):
-        '''Slightly more useful representation
-        '''
+        """Slightly more useful representation
+        """
         return '<hyperdb.Class "%s">'%self.classname
 
     # Editing nodes:
@@ -833,18 +837,18 @@
 
     # not in spec
     def getnode(self, nodeid):
-        ''' Return a convenience wrapper for the node.
+        """ Return a convenience wrapper for the node.
 
         'nodeid' must be the id of an existing node of this class or an
         IndexError is raised.
 
         'cache' exists for backwards compatibility, and is not used.
-        '''
+        """
         return Node(self, nodeid)
 
     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.
+        """
         raise NotImplementedError
 
     def set(self, nodeid, **propvalues):
@@ -879,15 +883,15 @@
         raise NotImplementedError
 
     def restore(self, nodeid):
-        '''Restpre a retired node.
+        """Restpre a retired node.
 
         Make node available for all operations like it was before retirement.
-        '''
+        """
         raise NotImplementedError
 
     def is_retired(self, nodeid):
-        '''Return true if the node is rerired
-        '''
+        """Return true if the node is rerired
+        """
         raise NotImplementedError
 
     def destroy(self, nodeid):
@@ -928,8 +932,8 @@
 
     # Locating nodes:
     def hasnode(self, nodeid):
-        '''Determine if the given nodeid actually exists
-        '''
+        """Determine if the given nodeid actually exists
+        """
         raise NotImplementedError
 
     def setkey(self, propname):
@@ -1125,7 +1129,7 @@
         backward-compatibility reasons a single (dir, prop) tuple is
         also allowed.
 
-        "search_matches" is {nodeid: marker}
+        "search_matches" is a container type
 
         The filter must match all properties specificed. If the property
         value to match is a list:
@@ -1226,11 +1230,11 @@
 
 
 class HyperdbValueError(ValueError):
-    ''' Error converting a raw value into a Hyperdb value '''
+    """ Error converting a raw value into a Hyperdb value """
     pass
 
 def convertLinkValue(db, propname, prop, value, idre=re.compile('^\d+$')):
-    ''' Convert the link value (may be id or key value) to an id value. '''
+    """ Convert the link value (may be id or key value) to an id value. """
     linkcl = db.classes[prop.classname]
     if not idre.match(value):
         if linkcl.getkey():
@@ -1255,14 +1259,14 @@
     return text.replace('\r', '\n')
 
 def rawToHyperdb(db, klass, itemid, propname, value, **kw):
-    ''' Convert the raw (user-input) value to a hyperdb-storable value. The
+    """ Convert the raw (user-input) value to a hyperdb-storable value. The
         value is for the "propname" property on itemid (may be None for a
         new item) of "klass" in "db".
 
         The value is usually a string, but in the case of multilink inputs
         it may be either a list of strings or a string with comma-separated
         values.
-    '''
+    """
     properties = klass.getprops()
 
     # ensure it's a valid property name
@@ -1284,21 +1288,21 @@
     return value
 
 class FileClass:
-    ''' A class that requires the "content" property and stores it on
+    """ A class that requires the "content" property and stores it on
         disk.
-    '''
+    """
     default_mime_type = 'text/plain'
 
     def __init__(self, db, classname, **properties):
-        '''The newly-created class automatically includes the "content"
+        """The newly-created class automatically includes the "content"
         property.
-        '''
+        """
         if not properties.has_key('content'):
             properties['content'] = String(indexme='yes')
 
     def export_propnames(self):
-        ''' Don't export the "content" property
-        '''
+        """ Don't export the "content" property
+        """
         propnames = self.getprops().keys()
         propnames.remove('content')
         propnames.sort()
@@ -1309,8 +1313,8 @@
         return os.path.join(dirname, self.classname+'-files', subdir_filename)
 
     def export_files(self, dirname, nodeid):
-        ''' Export the "content" property as a file, not csv column
-        '''
+        """ Export the "content" property as a file, not csv column
+        """
         source = self.db.filename(self.classname, nodeid)
 
         dest = self.exportFilename(dirname, nodeid)
@@ -1318,8 +1322,8 @@
         shutil.copyfile(source, dest)
 
     def import_files(self, dirname, nodeid):
-        ''' Import the "content" property as a file
-        '''
+        """ Import the "content" property as a file
+        """
         source = self.exportFilename(dirname, nodeid)
 
         dest = self.db.filename(self.classname, nodeid, create=1)
@@ -1337,8 +1341,8 @@
                 self.get(nodeid, 'content'), mime_type)
 
 class Node:
-    ''' A convenience wrapper for the given node
-    '''
+    """ A convenience wrapper for the given node
+    """
     def __init__(self, cl, nodeid, cache=1):
         self.__dict__['cl'] = cl
         self.__dict__['nodeid'] = nodeid
@@ -1388,8 +1392,8 @@
 
 
 def Choice(name, db, *options):
-    '''Quick helper to create a simple class with choices
-    '''
+    """Quick helper to create a simple class with choices
+    """
     cl = Class(db, name, name=String(), order=String())
     for i in range(len(options)):
         cl.create(name=options[i], order=i)

Modified: tracker/roundup-src/roundup/i18n.py
==============================================================================
--- tracker/roundup-src/roundup/i18n.py	(original)
+++ tracker/roundup-src/roundup/i18n.py	Sun Mar 15 22:43:30 2009
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-# $Id: i18n.py,v 1.15 2005/06/14 05:33:32 a1s Exp $
+# $Id: i18n.py,v 1.15 2005-06-14 05:33:32 a1s Exp $
 
 """
 RoundUp Internationalization (I18N)

Modified: tracker/roundup-src/roundup/init.py
==============================================================================
--- tracker/roundup-src/roundup/init.py	(original)
+++ tracker/roundup-src/roundup/init.py	Sun Mar 15 22:43:30 2009
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-# $Id: init.py,v 1.36 2005/12/03 11:22:50 a1s Exp $
+# $Id: init.py,v 1.36 2005-12-03 11:22:50 a1s Exp $
 
 """Init (create) a roundup instance.
 """
@@ -30,7 +30,7 @@
 def copytree(src, dst, symlinks=0):
     """Recursively copy a directory tree using copyDigestedFile().
 
-    The destination directory os allowed to exist.
+    The destination directory is allowed to exist.
 
     If the optional symlinks flag is true, symbolic links in the
     source tree result in symbolic links in the destination tree; if
@@ -39,7 +39,9 @@
 
     This was copied from shutil.py in std lib.
     """
-    names = os.listdir(src)
+
+    # Prevent 'hidden' files (those starting with '.') from being considered.
+    names = [f for f in os.listdir(src) if not f.startswith('.')]
     try:
         os.mkdir(dst)
     except OSError, error:

Modified: tracker/roundup-src/roundup/install_util.py
==============================================================================
--- tracker/roundup-src/roundup/install_util.py	(original)
+++ tracker/roundup-src/roundup/install_util.py	Sun Mar 15 22:43:30 2009
@@ -15,13 +15,14 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: install_util.py,v 1.11 2006/01/25 03:11:43 richard Exp $
+# $Id: install_util.py,v 1.11 2006-01-25 03:11:43 richard Exp $
 
 """Support module to generate and check fingerprints of installed files.
 """
 __docformat__ = 'restructuredtext'
 
-import os, sha, shutil
+import os, shutil
+from roundup.anypy.hashlib_ import sha1
 
 sgml_file_types = [".xml", ".ent", ".html"]
 hash_file_types = [".py", ".sh", ".conf", ".cgi"]
@@ -59,7 +60,7 @@
     del lines[-1]
 
     # calculate current digest
-    digest = sha.new()
+    digest = sha1()
     for line in lines:
         digest.update(line)
 
@@ -74,7 +75,7 @@
 
     def __init__(self, filename):
         self.filename = filename
-        self.digest = sha.new()
+        self.digest = sha1()
         self.file = open(self.filename, "w")
 
     def write(self, data):

Modified: tracker/roundup-src/roundup/instance.py
==============================================================================
--- tracker/roundup-src/roundup/instance.py	(original)
+++ tracker/roundup-src/roundup/instance.py	Sun Mar 15 22:43:30 2009
@@ -15,19 +15,19 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-# $Id: instance.py,v 1.37 2006/12/11 23:36:15 richard Exp $
 
-'''Tracker handling (open tracker).
+"""Tracker handling (open tracker).
 
 Backwards compatibility for the old-style "imported" trackers.
-'''
+"""
 __docformat__ = 'restructuredtext'
 
 import os
 import sys
 from roundup import configuration, mailgw
-from roundup import hyperdb, backends
+from roundup import hyperdb, backends, actions
 from roundup.cgi import client, templating
+from roundup.cgi import actions as cgi_actions
 
 class Vars:
     def __init__(self, vars):
@@ -47,6 +47,7 @@
         self.tracker_home = tracker_home
         self.optimize = optimize
         self.config = configuration.CoreConfig(tracker_home)
+        self.actions = {}
         self.cgi_actions = {}
         self.templating_utils = {}
         self.load_interfaces()
@@ -182,7 +183,20 @@
         return vars
 
     def registerAction(self, name, action):
-        self.cgi_actions[name] = action
+
+        # The logic here is this:
+        # * if `action` derives from actions.Action,
+        #   it is executable as a generic action.
+        # * if, moreover, it also derives from cgi.actions.Bridge,
+        #   it may in addition be called via CGI
+        # * in all other cases we register it as a CGI action, without
+        #   any check (for backward compatibility).
+        if issubclass(action, actions.Action):
+            self.actions[name] = action
+            if issubclass(action, cgi_actions.Bridge):
+                self.cgi_actions[name] = action
+        else:
+            self.cgi_actions[name] = action
 
     def registerUtil(self, name, function):
         self.templating_utils[name] = function

Modified: tracker/roundup-src/roundup/mailer.py
==============================================================================
--- tracker/roundup-src/roundup/mailer.py	(original)
+++ tracker/roundup-src/roundup/mailer.py	Sun Mar 15 22:43:30 2009
@@ -1,26 +1,30 @@
 """Sending Roundup-specific mail over SMTP.
 """
 __docformat__ = 'restructuredtext'
-# $Id: mailer.py,v 1.21 2007/11/14 05:53:20 jpend Exp $
+# $Id: mailer.py,v 1.22 2008-07-21 01:44:58 richard Exp $
 
-import time, quopri, os, socket, smtplib, re, sys, traceback
+import time, quopri, os, socket, smtplib, re, sys, traceback, email
 
 from cStringIO import StringIO
-from MimeWriter import MimeWriter
 
-from roundup.rfc2822 import encode_header
 from roundup import __version__
 from roundup.date import get_timezone
 
-try:
-    from email.Utils import formatdate
-except ImportError:
-    def formatdate():
-        return time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime())
+from email.Utils import formatdate, formataddr
+from email.Message import Message
+from email.Header import Header
+from email.MIMEText import MIMEText
+from email.MIMEMultipart import MIMEMultipart
 
 class MessageSendError(RuntimeError):
     pass
 
+def encode_quopri(msg):
+    orig = msg.get_payload()
+    encdata = quopri.encodestring(orig)
+    msg.set_payload(encdata)
+    msg['Content-Transfer-Encoding'] = 'quoted-printable'
+
 class Mailer:
     """Roundup-specific mail sending."""
     def __init__(self, config):
@@ -41,7 +45,7 @@
             os.environ['TZ'] = get_timezone(self.config.TIMEZONE).tzname(None)
             time.tzset()
 
-    def get_standard_message(self, to, subject, author=None):
+    def get_standard_message(self, to, subject, author=None, multipart=False):
         '''Form a standard email message from Roundup.
 
         "to"      - recipients list
@@ -55,35 +59,48 @@
         '''
         # encode header values if they need to be
         charset = getattr(self.config, 'EMAIL_CHARSET', 'utf-8')
-        tracker_name = self.config.TRACKER_NAME
-        if charset != 'utf-8':
-            tracker = unicode(tracker_name, 'utf-8').encode(charset)
+        tracker_name = unicode(self.config.TRACKER_NAME, 'utf-8')
         if not author:
-            author = straddr((tracker_name, self.config.ADMIN_EMAIL))
+            author = formataddr((tracker_name, self.config.ADMIN_EMAIL))
+        else:
+            name = unicode(author[0], 'utf-8')
+            author = formataddr((name, author[1]))
+
+        if multipart:
+            message = MIMEMultipart()
         else:
-            name = author[0]
-            if charset != 'utf-8':
-                name = unicode(name, 'utf-8').encode(charset)
-            author = straddr((encode_header(name, charset), author[1]))
-
-        message = StringIO()
-        writer = MimeWriter(message)
-        writer.addheader('Subject', encode_header(subject, charset))
-        writer.addheader('To', ', '.join(to))
-        writer.addheader('From', author)
-        writer.addheader('Date', formatdate(localtime=True))
+            message = Message()
+            message.set_charset(charset)
+            message['Content-Type'] = 'text/plain; charset="%s"'%charset
+
+        try:
+            message['Subject'] = subject.encode('ascii')
+        except UnicodeError:
+            message['Subject'] = Header(subject, charset)
+        message['To'] = ', '.join(to)
+        try:
+            message['From'] = author.encode('ascii')
+        except UnicodeError:
+            message['From'] = Header(author, charset)
+        message['Date'] = formatdate(localtime=True)
+
+        # add a Precedence header so autoresponders ignore us
+        message['Precedence'] = 'bulk'
 
         # Add a unique Roundup header to help filtering
-        writer.addheader('X-Roundup-Name', encode_header(tracker_name,
-            charset))
+        try:
+            message['X-Roundup-Name'] = tracker_name.encode('ascii')
+        except UnicodeError:
+            message['X-Roundup-Name'] = Header(tracker_name, charset)
+
         # and another one to avoid loops
-        writer.addheader('X-Roundup-Loop', 'hello')
+        message['X-Roundup-Loop'] = 'hello'
         # finally, an aid to debugging problems
-        writer.addheader('X-Roundup-Version', __version__)
+        message['X-Roundup-Version'] = __version__
 
-        writer.addheader('MIME-Version', '1.0')
+        message['MIME-Version'] = '1.0'
 
-        return message, writer
+        return message
 
     def standard_message(self, to, subject, content, author=None):
         """Send a standard message.
@@ -93,15 +110,12 @@
         - subject: the subject as a string.
         - content: the body of the message as a string.
         - author: the sender as a (name, address) tuple
-        """
-        message, writer = self.get_standard_message(to, subject, author)
-
-        writer.addheader('Content-Transfer-Encoding', 'quoted-printable')
-        body = writer.startbody('text/plain; charset=utf-8')
-        content = StringIO(content)
-        quopri.encode(content, body, 0)
 
-        self.smtp_send(to, message)
+        All strings are assumed to be UTF-8 encoded.
+        """
+        message = self.get_standard_message(to, subject, author)
+        message.set_payload(content)
+        self.smtp_send(to, str(message))
 
     def bounce_message(self, bounced_message, to, error,
                        subject='Failed issue tracker submission'):
@@ -125,23 +139,13 @@
         elif error_messages_to == "both":
             to.append(dispatcher_email)
 
-        message, writer = self.get_standard_message(to, subject)
+        message = self.get_standard_message(to, subject)
 
-        part = writer.startmultipartbody('mixed')
-        part = writer.nextpart()
-        part.addheader('Content-Transfer-Encoding', 'quoted-printable')
-        body = part.startbody('text/plain; charset=utf-8')
-        body.write(quopri.encodestring ('\n'.join(error)))
+        # add the error text
+        part = MIMEText(error)
+        message.attach(part)
 
         # attach the original message to the returned message
-        part = writer.nextpart()
-        part.addheader('Content-Disposition', 'attachment')
-        part.addheader('Content-Description', 'Message you sent')
-        body = part.startbody('text/plain')
-
-        for header in bounced_message.headers:
-            body.write(header)
-        body.write('\n')
         try:
             bounced_message.rewindbody()
         except IOError, message:
@@ -149,11 +153,15 @@
                        % bounced_message)
         else:
             body.write(bounced_message.fp.read())
+        part = MIMEText(bounced_message.fp.read())
+        part['Content-Disposition'] = 'attachment'
+        for header in bounced_message.headers:
+            part.write(header)
+        message.attach(part)
 
-        writer.lastpart()
-
+        # send
         try:
-            self.smtp_send(to, message)
+            self.smtp_send(to, str(message))
         except MessageSendError:
             # squash mail sending errors when bouncing mail
             # TODO this *could* be better, as we could notify admin of the
@@ -161,14 +169,14 @@
             # because of spam)
             pass
 
-    def exception_message(self, data=''):
+    def exception_message(self):
         '''Send a message to the admins with information about the latest
         traceback.
         '''
         subject = '%s: %s'%(self.config.TRACKER_NAME, sys.exc_info()[1])
         to = [self.config.ADMIN_EMAIL]
         content = '\n'.join(traceback.format_exception(*sys.exc_info()))
-        self.standard_message(to, subject, data+content)
+        self.standard_message(to, subject, content)
 
     def smtp_send(self, to, message):
         """Send a message over SMTP, using roundup's config.
@@ -181,16 +189,14 @@
             # don't send - just write to a file
             open(self.debug, 'a').write('FROM: %s\nTO: %s\n%s\n' %
                                         (self.config.ADMIN_EMAIL,
-                                         ', '.join(to),
-                                         message.getvalue()))
+                                         ', '.join(to), message))
         else:
             # now try to send the message
             try:
                 # send the message as admin so bounces are sent there
                 # instead of to roundup
                 smtp = SMTPConnection(self.config)
-                smtp.sendmail(self.config.ADMIN_EMAIL, to,
-                              message.getvalue())
+                smtp.sendmail(self.config.ADMIN_EMAIL, to, message)
             except socket.error, value:
                 raise MessageSendError("Error: couldn't send email: "
                                        "mailhost %s"%value)
@@ -214,20 +220,4 @@
         if mailuser:
             self.login(mailuser, config["MAIL_PASSWORD"])
 
-# use the 'email' module, either imported, or our copied version
-try :
-    from email.Utils import formataddr as straddr
-except ImportError :
-    # code taken from the email package 2.4.3
-    def straddr(pair, specialsre = re.compile(r'[][\()<>@,:;".]'),
-            escapesre = re.compile(r'[][\()"]')):
-        name, address = pair
-        if name:
-            quotes = ''
-            if specialsre.search(name):
-                quotes = '"'
-            name = escapesre.sub(r'\\\g<0>', name)
-            return '%s%s%s <%s>' % (quotes, name, quotes, address)
-        return address
-
 # vim: set et sts=4 sw=4 :

Modified: tracker/roundup-src/roundup/mailgw.py
==============================================================================
--- tracker/roundup-src/roundup/mailgw.py	(original)
+++ tracker/roundup-src/roundup/mailgw.py	Sun Mar 15 22:43:30 2009
@@ -73,7 +73,7 @@
 an exception, the original message is bounced back to the sender with the
 explanatory message given in the exception.
 
-$Id: mailgw.py,v 1.183 2007/01/28 13:49:13 forsberg Exp $
+$Id: mailgw.py,v 1.196 2008-07-23 03:04:44 richard Exp $
 """
 __docformat__ = 'restructuredtext'
 
@@ -81,6 +81,8 @@
 import time, random, sys, logging
 import traceback, MimeWriter, rfc822
 
+from email.Header import decode_header
+
 from roundup import configuration, hyperdb, date, password, rfc2822, exceptions
 from roundup.mailer import Mailer, MessageSendError
 from roundup.i18n import _
@@ -261,9 +263,30 @@
 
     def getheader(self, name, default=None):
         hdr = mimetools.Message.getheader(self, name, default)
+        if not hdr:
+            return ''
         if hdr:
             hdr = hdr.replace('\n','') # Inserted by rfc822.readheaders
-        return rfc2822.decode_header(hdr)
+        # historically this method has returned utf-8 encoded string
+        l = []
+        for part, encoding in decode_header(hdr):
+            if encoding:
+                part = part.decode(encoding)
+            l.append(part)
+        return ''.join([s.encode('utf-8') for s in l])
+
+    def getaddrlist(self, name):
+        # overload to decode the name part of the address
+        l = []
+        for (name, addr) in mimetools.Message.getaddrlist(self, name):
+            p = []
+            for part, encoding in decode_header(name):
+                if encoding:
+                    part = part.decode(encoding)
+                p.append(part)
+            name = ''.join([s.encode('utf-8') for s in p])
+            l.append((name, addr))
+        return l
 
     def getname(self):
         """Find an appropriate name for this message."""
@@ -348,7 +371,7 @@
 
     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.
@@ -768,7 +791,7 @@
             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[0][1], m)
+            self.mailer.bounce_message(message, [sendto[0][1]], m)
 
             m.append('----------------')
             m.append(traceback.format_exc())

Modified: tracker/roundup-src/roundup/password.py
==============================================================================
--- tracker/roundup-src/roundup/password.py	(original)
+++ tracker/roundup-src/roundup/password.py	Sun Mar 15 22:43:30 2009
@@ -15,32 +15,32 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-# $Id: password.py,v 1.15 2005/12/25 15:38:40 a1s Exp $
+# $Id: password.py,v 1.15 2005-12-25 15:38:40 a1s Exp $
 
 """Password handling (encoding, decoding).
 """
 __docformat__ = 'restructuredtext'
 
-import sha, md5, re, string, random
+import re, string, random
+from roundup.anypy.hashlib_ import md5, sha1
 try:
     import crypt
-except:
+except ImportError:
     crypt = None
-    pass
 
 class PasswordValueError(ValueError):
-    ''' The password value is not valid '''
+    """ The password value is not valid """
     pass
 
 def encodePassword(plaintext, scheme, other=None):
-    '''Encrypt the plaintext password.
-    '''
+    """Encrypt the plaintext password.
+    """
     if plaintext is None:
         plaintext = ""
     if scheme == 'SHA':
-        s = sha.sha(plaintext).hexdigest()
+        s = sha1(plaintext).hexdigest()
     elif scheme == 'MD5':
-        s = md5.md5(plaintext).hexdigest()
+        s = md5(plaintext).hexdigest()
     elif scheme == 'crypt' and crypt is not None:
         if other is not None:
             salt = other
@@ -59,7 +59,7 @@
     return ''.join([random.choice(chars) for x in range(length)])
 
 class Password:
-    '''The class encapsulates a Password property type value in the database.
+    """The class encapsulates a Password property type value in the database.
 
     The encoding of the password is one if None, 'SHA', 'MD5' or 'plaintext'.
     The encodePassword function is used to actually encode the password from
@@ -79,13 +79,13 @@
     1
     >>> 'not sekrit' != p
     1
-    '''
+    """
 
     default_scheme = 'SHA'        # new encryptions use this scheme
     pwre = re.compile(r'{(\w+)}(.+)')
 
     def __init__(self, plaintext=None, scheme=None, encrypted=None):
-        '''Call setPassword if plaintext is not None.'''
+        """Call setPassword if plaintext is not None."""
         if scheme is None:
             scheme = self.default_scheme
         if plaintext is not None:
@@ -98,9 +98,9 @@
             self.plaintext = None
 
     def unpack(self, encrypted, scheme=None):
-        '''Set the password info from the scheme:<encryted info> string
+        """Set the password info from the scheme:<encryted info> string
            (the inverse of __str__)
-        '''
+        """
         m = self.pwre.match(encrypted)
         if m:
             self.scheme = m.group(1)
@@ -111,7 +111,7 @@
             self.setPassword(encrypted, scheme)
 
     def setPassword(self, plaintext, scheme=None):
-        '''Sets encrypts plaintext.'''
+        """Sets encrypts plaintext."""
         if scheme is None:
             scheme = self.default_scheme
         self.scheme = scheme
@@ -119,7 +119,7 @@
         self.plaintext = plaintext
 
     def __cmp__(self, other):
-        '''Compare this password against another password.'''
+        """Compare this password against another password."""
         # check to see if we're comparing instances
         if isinstance(other, Password):
             if self.scheme != other.scheme:
@@ -133,7 +133,7 @@
             self.password))
 
     def __str__(self):
-        '''Stringify the encrypted password for database storage.'''
+        """Stringify the encrypted password for database storage."""
         if self.password is None:
             raise ValueError, 'Password not set'
         return '{%s}%s'%(self.scheme, self.password)

Modified: tracker/roundup-src/roundup/roundupdb.py
==============================================================================
--- tracker/roundup-src/roundup/roundupdb.py	(original)
+++ tracker/roundup-src/roundup/roundupdb.py	Sun Mar 15 22:43:30 2009
@@ -16,23 +16,27 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-# $Id: roundupdb.py,v 1.138 2008/01/08 20:58:31 richard Exp $
+# $Id: roundupdb.py,v 1.139 2008-08-07 06:31:16 richard Exp $
 
 """Extending hyperdb with types specific to issue-tracking.
 """
 __docformat__ = 'restructuredtext'
 
 import re, os, smtplib, socket, time, random
-import cStringIO, base64, quopri, mimetypes
+import cStringIO, base64, mimetypes
 import os.path
-
-from rfc2822 import encode_header
+import logging
+from email import Encoders
+from email.Utils import formataddr
+from email.Header import Header
+from email.MIMEText import MIMEText
+from email.MIMEBase import MIMEBase
 
 from roundup import password, date, hyperdb
 from roundup.i18n import _
 
 # MessageSendError is imported for backwards compatibility
-from roundup.mailer import Mailer, straddr, MessageSendError
+from roundup.mailer import Mailer, MessageSendError, encode_quopri
 
 class Database:
 
@@ -115,6 +119,29 @@
         return userid
 
 
+    def log_debug(self, msg, *args, **kwargs):
+        """Log a message with level DEBUG."""
+
+        logger = self.get_logger()
+        logger.debug(msg, *args, **kwargs)
+
+    def log_info(self, msg, *args, **kwargs):
+        """Log a message with level INFO."""
+
+        logger = self.get_logger()
+        logger.info(msg, *args, **kwargs)
+
+    def get_logger(self):
+        """Return the logger for this database."""
+
+        # Because getting a logger requires acquiring a lock, we want
+        # to do it only once.
+        if not hasattr(self, '__logger'):
+            self.__logger = logging.getLogger('hyperdb')
+
+        return self.__logger
+
+
 class DetectorError(RuntimeError):
     """ Raised by detectors that want to indicate that something's amiss
     """
@@ -251,7 +278,7 @@
     sendmessage = nosymessage
 
     def send_message(self, nodeid, msgid, note, sendto, from_address=None,
-            bcc_sendto=[], authid=None):
+            bcc_sendto=[]):
         '''Actually send the nominated message from this node to the sendto
            recipients, with the note appended.
         '''
@@ -281,12 +308,7 @@
         title = self.get(nodeid, 'title') or '%s message copy'%cn
 
         # figure author information
-        if authid:
-            authname = users.get(authid, 'realname')
-            if not authname:
-                authname = users.get(authid, 'username', '')
-            authaddr = users.get(authid, 'address', '')            
-        elif msgid:
+        if msgid:
             authid = messages.get(msgid, 'author')
         else:
             authid = self.db.getuid()
@@ -296,7 +318,7 @@
         authaddr = users.get(authid, 'address', '')
 
         if authaddr and self.db.config.MAIL_ADD_AUTHOREMAIL:
-            authaddr = " <%s>" % straddr( ('',authaddr) )
+            authaddr = " <%s>" % formataddr( ('',authaddr) )
         elif authaddr:
             authaddr = ""
 
@@ -309,15 +331,14 @@
 
         # add author information
         if authid and self.db.config.MAIL_ADD_AUTHORINFO:
-            if msgid and len(self.get(nodeid,'messages')) == 1:
+            if msgid and len(self.get(nodeid, 'messages')) == 1:
                 m.append(_("New submission from %(authname)s%(authaddr)s:")
                     % locals())
             elif msgid:
                 m.append(_("%(authname)s%(authaddr)s added the comment:")
                     % locals())
             else:
-                m.append(_("Changes by %(authname)s%(authaddr)s:")
-                         % locals())
+                m.append(_("Change by %(authname)s%(authaddr)s:") % locals())
             m.append('')
 
         # add the content
@@ -348,15 +369,11 @@
         if self.db.config.EMAIL_SIGNATURE_POSITION == 'bottom':
             m.append(self.email_signature(nodeid, msgid))
 
-        # encode the content as quoted-printable
+        # figure the encoding
         charset = getattr(self.db.config, 'EMAIL_CHARSET', 'utf-8')
-        m = '\n'.join(m)
-        if charset != 'utf-8':
-            m = unicode(m, 'utf-8').encode(charset)
-        content = cStringIO.StringIO(m)
-        content_encoded = cStringIO.StringIO()
-        quopri.encode(content, content_encoded, 0)
-        content_encoded = content_encoded.getvalue()
+
+        # construct the content and convert to unicode object
+        content = unicode('\n'.join(m), 'utf-8').encode(charset)
 
         # make sure the To line is always the same (for testing mostly)
         sendto.sort()
@@ -379,6 +396,10 @@
         else:
             sendto = [sendto]
 
+        tracker_name = unicode(self.db.config.TRACKER_NAME, 'utf-8')
+        tracker_name = formataddr((tracker_name, from_address))
+        tracker_name = Header(tracker_name, charset)
+
         # now send one or more messages
         # TODO: I believe we have to create a new message each time as we
         # can't fiddle the recipients in the message ... worth testing
@@ -387,24 +408,18 @@
         for sendto in sendto:
             # create the message
             mailer = Mailer(self.db.config)
-            message, writer = mailer.get_standard_message(sendto, subject,
-                author)
+
+            message = mailer.get_standard_message(sendto, subject, author,
+                multipart=message_files)
 
             # set reply-to to the tracker
-            tracker_name = self.db.config.TRACKER_NAME
-            if charset != 'utf-8':
-                tracker = unicode(tracker_name, 'utf-8').encode(charset)
-            tracker_name = encode_header(tracker_name, charset)
-            writer.addheader('Reply-To', straddr((tracker_name, from_address)))
+            message['Reply-To'] = tracker_name
 
             # message ids
             if messageid:
-                writer.addheader('Message-Id', messageid)
+                message['Message-Id'] = messageid
             if inreplyto:
-                writer.addheader('In-Reply-To', inreplyto)
-
-            # Additional headers for bugs.python.org
-            # 20080106 mvl
+                message['In-Reply-To'] = inreplyto
 
             # Generate a header for each link or multilink to
             # a class that has a name attribute
@@ -425,8 +440,12 @@
                         continue
                 values = [cl.get(v, 'name') for v in values]
                 values = ', '.join(values)
-                writer.addheader("X-Roundup-%s-%s" % (self.classname, propname),
-                                 values)
+                header = "X-Roundup-%s-%s"%(self.classname, propname)
+                try:
+                    message[header] = values.encode('ascii')
+                except UnicodeError:
+                    message[header] = Header(values, charset)
+
             if not inreplyto:
                 # Default the reply to the first message
                 msgs = self.get(nodeid, 'messages')
@@ -436,67 +455,31 @@
                 if msgs and msgs[0] != nodeid:
                     inreplyto = messages.get(msgs[0], 'messageid')
                     if inreplyto:
-                        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 and msgs[0] != nodeid:
-                    inreplyto = messages.get(msgs[0], 'messageid')
-                    if inreplyto:
-                        writer.addheader('In-Reply-To', inreplyto)
+                        message['In-Reply-To'] = inreplyto
 
             # attach files
             if message_files:
-                part = writer.startmultipartbody('mixed')
-                part = writer.nextpart()
-                part.addheader('Content-Transfer-Encoding', 'quoted-printable')
-                body = part.startbody('text/plain; charset=%s'%charset)
-                body.write(content_encoded)
+                # first up the text as a part
+                part = MIMEText(content)
+                encode_quopri(part)
+                message.attach(part)
+
                 for fileid in message_files:
                     name = files.get(fileid, 'name')
                     mime_type = files.get(fileid, 'type')
                     content = files.get(fileid, 'content')
-                    part = writer.nextpart()
                     if mime_type == 'text/plain':
-                        part.addheader('Content-Disposition',
-                            'attachment;\n filename="%s"'%name)
                         try:
                             content.decode('ascii')
                         except UnicodeError:
                             # the content cannot be 7bit-encoded.
                             # use quoted printable
-                            part.addheader('Content-Transfer-Encoding',
-                                'quoted-printable')
-                            body = part.startbody('text/plain')
-                            body.write(quopri.encodestring(content))
+                            # XXX stuffed if we know the charset though :(
+                            part = MIMEText(content)
+                            encode_quopri(part)
                         else:
-                            part.addheader('Content-Transfer-Encoding', '7bit')
-                            body = part.startbody('text/plain')
-                            body.write(content)
+                            part = MIMEText(content)
+                            part['Content-Transfer-Encoding'] = '7bit'
                     else:
                         # some other type, so encode it
                         if not mime_type:
@@ -504,17 +487,16 @@
                             mime_type = mimetypes.guess_type(name)[0]
                         if mime_type is None:
                             mime_type = 'application/octet-stream'
-                        part.addheader('Content-Disposition',
-                            'attachment;\n filename="%s"'%name)
-                        part.addheader('Content-Transfer-Encoding', 'base64')
-                        body = part.startbody(mime_type)
-                        body.write(base64.encodestring(content))
-                writer.lastpart()
+                        main, sub = mime_type.split('/')
+                        part = MIMEBase(main, sub)
+                        part.set_payload(content)
+                        Encoders.encode_base64(part)
+                    part['Content-Disposition'] = 'attachment;\n filename="%s"'%name
+                    message.attach(part)
+
             else:
-                writer.addheader('Content-Transfer-Encoding',
-                    'quoted-printable')
-                body = writer.startbody('text/plain; charset=%s'%charset)
-                body.write(content_encoded)
+                message.set_payload(content)
+                encode_quopri(message)
 
             if first:
                 mailer.smtp_send(sendto + bcc_sendto, message)
@@ -538,7 +520,7 @@
             web = base + self.classname + nodeid
 
         # ensure the email address is properly quoted
-        email = straddr((self.db.config.TRACKER_NAME,
+        email = formataddr((self.db.config.TRACKER_NAME,
             self.db.config.TRACKER_EMAIL))
 
         line = '_' * max(len(web)+2, len(email))

Modified: tracker/roundup-src/roundup/scripts/__init__.py
==============================================================================
--- tracker/roundup-src/roundup/scripts/__init__.py	(original)
+++ tracker/roundup-src/roundup/scripts/__init__.py	Sun Mar 15 22:43:30 2009
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: __init__.py,v 1.3 2004/02/11 23:55:10 richard Exp $
+# $Id: __init__.py,v 1.3 2004-02-11 23:55:10 richard Exp $
 
 '''Subpackage containing the modules that implement the command
 line tools.

Modified: tracker/roundup-src/roundup/scripts/roundup_admin.py
==============================================================================
--- tracker/roundup-src/roundup/scripts/roundup_admin.py	(original)
+++ tracker/roundup-src/roundup/scripts/roundup_admin.py	Sun Mar 15 22:43:30 2009
@@ -14,7 +14,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: roundup_admin.py,v 1.6 2004/02/11 23:55:10 richard Exp $
+# $Id: roundup_admin.py,v 1.6 2004-02-11 23:55:10 richard Exp $
 
 """Command-line script stub that calls the roundup.admin functions.
 """

Modified: tracker/roundup-src/roundup/scripts/roundup_demo.py
==============================================================================
--- tracker/roundup-src/roundup/scripts/roundup_demo.py	(original)
+++ tracker/roundup-src/roundup/scripts/roundup_demo.py	Sun Mar 15 22:43:30 2009
@@ -2,7 +2,6 @@
 #
 # Copyright 2004 Richard Jones (richard at mechanicalcat.net)
 #
-# $Id: roundup_demo.py,v 1.1 2004/10/18 07:56:09 a1s Exp $
 
 import sys
 
@@ -10,9 +9,11 @@
 from roundup.i18n import _
 
 DEFAULT_HOME = './demo'
+DEFAULT_TEMPLATE = 'classic'
 
 def run():
     home = DEFAULT_HOME
+    template = DEFAULT_TEMPLATE
     nuke = sys.argv[-1] == 'nuke'
     # if there is no tracker in home, force nuke
     try:
@@ -32,9 +33,15 @@
             _('Enter directory path to create demo tracker [%s]: ') % home)
         if not home:
             home = DEFAULT_HOME
+        templates = admin.AdminTool().listTemplates().keys()
+        template = raw_input(
+            _('Enter tracker template to use (one of (%s)) [%s]: ') %
+            (','.join(templates),template))
+        if not template:
+            template = DEFAULT_TEMPLATE
         # install
         demo.install_demo(home, backend,
-            admin.AdminTool().listTemplates()['classic']['path'])
+            admin.AdminTool().listTemplates()[template]['path'])
     # run
     demo.run_demo(home)
 

Modified: tracker/roundup-src/roundup/scripts/roundup_gettext.py
==============================================================================
--- tracker/roundup-src/roundup/scripts/roundup_gettext.py	(original)
+++ tracker/roundup-src/roundup/scripts/roundup_gettext.py	Sun Mar 15 22:43:30 2009
@@ -2,7 +2,7 @@
 #
 # Copyright 2004 Richard Jones (richard at mechanicalcat.net)
 #
-# $Id: roundup_gettext.py,v 1.1 2004/10/20 10:25:23 a1s Exp $
+# $Id: roundup_gettext.py,v 1.1 2004-10-20 10:25:23 a1s Exp $
 
 """Extract translatable strings from tracker templates"""
 

Modified: tracker/roundup-src/roundup/scripts/roundup_mailgw.py
==============================================================================
--- tracker/roundup-src/roundup/scripts/roundup_mailgw.py	(original)
+++ tracker/roundup-src/roundup/scripts/roundup_mailgw.py	Sun Mar 15 22:43:30 2009
@@ -14,7 +14,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-# $Id: roundup_mailgw.py,v 1.23 2006/12/13 23:32:39 richard Exp $
+# $Id: roundup_mailgw.py,v 1.25 2008-08-19 01:06:01 richard Exp $
 
 """Command-line script stub that calls the roundup.mailgw.
 """
@@ -24,7 +24,7 @@
 from roundup import version_check
 from roundup import __version__ as roundup_version
 
-import sys, os, re, cStringIO, getopt, socket
+import sys, os, re, cStringIO, getopt, socket, netrc
 
 from roundup import mailgw
 from roundup.i18n import _
@@ -67,16 +67,22 @@
  specified as:
    mailbox /path/to/mailbox
 
+In all of the following the username and password can be stored in a
+~/.netrc file. In this case only the server name need be specified on
+the command-line.
+
+The username and/or password will be prompted for if not supplied on
+the command-line or in ~/.netrc.
+
 POP:
  In the third case, the gateway reads all messages from the POP server
  specified and submits each in turn to the roundup.mailgw module. The
  server is specified as:
     pop username:password at server
- The username and password may be omitted:
+ Alternatively, one can omit one or both of username and password:
     pop username at server
     pop server
- are both valid. The username and/or password will be prompted for if
- not supplied on the command-line.
+ are both valid.
 
 POPS:
  Connect to a POP server over ssl. This requires python 2.4 or later.
@@ -158,36 +164,44 @@
 
         if source == 'mailbox':
             return handler.do_mailbox(specification)
-        elif source == 'pop' or source == 'pops':
-            m = re.match(r'((?P<user>[^:]+)(:(?P<pass>.+))?@)?(?P<server>.+)',
-                specification)
-            if m:
-                ssl = source.endswith('s')
-                if ssl and sys.version_info<(2,4):
-                    return usage(argv, _('Error: a later version of python is required'))
-                return handler.do_pop(m.group('server'), m.group('user'),
-                    m.group('pass'),ssl)
-            return usage(argv, _('Error: pop specification not valid'))
+
+        # the source will be a network server, so obtain the credentials to
+        # use in connecting to the server
+        try:
+            # attempt to obtain credentials from a ~/.netrc file
+            authenticator = netrc.netrc().authenticators(specification)
+            username = authenticator[0]
+            password = authenticator[2]
+            server = specification
+            # IOError if no ~/.netrc file, TypeError if the hostname
+            # not found in the ~/.netrc file:
+        except (IOError, TypeError):
+            match = re.match(r'((?P<user>[^:]+)(:(?P<pass>.+))?@)?(?P<server>.+)',
+                             specification)
+            if match:
+                username = match.group('user')
+                password = match.group('pass')
+                server = match.group('server')
+            else:
+                return usage(argv, _('Error: %s specification not valid') % source)
+
+        # now invoke the mailgw handler depending on the server handler requested
+        if source.startswith('pop'):
+            ssl = source.endswith('s')
+            if ssl and sys.version_info<(2,4):
+                return usage(argv, _('Error: a later version of python is required'))
+            return handler.do_pop(server, username, password, ssl)
         elif source == 'apop':
-            m = re.match(r'((?P<user>[^:]+)(:(?P<pass>.+))?@)?(?P<server>.+)',
-                specification)
-            if m:
-                return handler.do_apop(m.group('server'), m.group('user'),
-                    m.group('pass'))
-            return usage(argv, _('Error: apop specification not valid'))
-        elif source == 'imap' or source == 'imaps':
-            m = re.match(r'((?P<user>[^:]+)(:(?P<pass>.+))?@)?(?P<server>.+)',
-                specification)
-            if m:
-                ssl = source.endswith('s')
-                mailbox = ''
-                if len(args) > 3:
-                    mailbox = args[3]
-                return handler.do_imap(m.group('server'), m.group('user'),
-                    m.group('pass'), mailbox, ssl)
+            return handler.do_apop(server, username, password)
+        elif source.startswith('imap'):
+            ssl = source.endswith('s')
+            mailbox = ''
+            if len(args) > 3:
+                mailbox = args[3]
+            return handler.do_imap(server, username, password, mailbox, ssl)
 
         return usage(argv, _('Error: The source must be either "mailbox",'
-            ' "pop", "apop", "imap" or "imaps"'))
+            ' "pop", "pops", "apop", "imap" or "imaps"'))
     finally:
         # handler might have closed the initial db and opened a new one
         handler.db.close()

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 15 22:43:30 2009
@@ -17,7 +17,7 @@
 
 """Command-line script that runs a server over roundup.cgi.client.
 
-$Id: roundup_server.py,v 1.94 2007/09/25 04:27:12 jpend Exp $
+$Id: roundup_server.py,v 1.94 2007-09-25 04:27:12 jpend Exp $
 """
 __docformat__ = 'restructuredtext'
 

Modified: tracker/roundup-src/roundup/security.py
==============================================================================
--- tracker/roundup-src/roundup/security.py	(original)
+++ tracker/roundup-src/roundup/security.py	Sun Mar 15 22:43:30 2009
@@ -103,15 +103,11 @@
         self.addRole(name="Admin", description="An admin user, full privs")
         self.addRole(name="Anonymous", description="An anonymous user")
 
-        ce = self.addPermission(name="Create",
-            description="User may create everthing")
-        self.addPermissionToRole('Admin', ce)
-        ee = self.addPermission(name="Edit",
-            description="User may edit everthing")
-        self.addPermissionToRole('Admin', ee)
-        ae = self.addPermission(name="View",
-            description="User may access everything")
-        self.addPermissionToRole('Admin', ae)
+        # default permissions - Admin may do anything
+        for p in 'create edit retire view'.split():
+            p = self.addPermission(name=p.title(),
+                description="User may %s everthing"%p)
+            self.addPermissionToRole('Admin', p)
 
         # initialise the permissions and roles needed for the UIs
         from roundup.cgi import client

Modified: tracker/roundup-src/roundup/token.py
==============================================================================
--- tracker/roundup-src/roundup/token.py	(original)
+++ tracker/roundup-src/roundup/token.py	Sun Mar 15 22:43:30 2009
@@ -8,7 +8,7 @@
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 # 
-# $Id: token.py,v 1.4 2004/02/11 23:55:08 richard Exp $
+# $Id: token.py,v 1.4 2004-02-11 23:55:08 richard Exp $
 #
 
 """This module provides the tokeniser used by roundup-admin.

Modified: tracker/roundup-src/roundup/version_check.py
==============================================================================
--- tracker/roundup-src/roundup/version_check.py	(original)
+++ tracker/roundup-src/roundup/version_check.py	Sun Mar 15 22:43:30 2009
@@ -16,7 +16,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: version_check.py,v 1.4 2004/02/11 23:55:08 richard Exp $
+# $Id: version_check.py,v 1.4 2004-02-11 23:55:08 richard Exp $
 
 """Enforces the minimum Python version that Roundup requires.
 """

Added: tracker/roundup-src/roundup/xmlrpc.py
==============================================================================
--- (empty file)
+++ tracker/roundup-src/roundup/xmlrpc.py	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,163 @@
+#
+# Copyright (C) 2007 Stefan Seefeld
+# All rights reserved.
+# For license terms see the file COPYING.txt.
+#
+
+from roundup import hyperdb
+from roundup.cgi.exceptions import *
+from roundup.exceptions import UsageError
+from roundup.date import Date, Range, Interval
+from roundup import actions
+from SimpleXMLRPCServer import *
+
+def translate(value):
+    """Translate value to becomes valid for XMLRPC transmission."""
+
+    if isinstance(value, (Date, Range, Interval)):
+        return repr(value)
+    elif type(value) is list:
+        return [translate(v) for v in value]
+    elif type(value) is tuple:
+        return tuple([translate(v) for v in value])
+    elif type(value) is dict:
+        return dict([[translate(k), translate(value[k])] for k in value])
+    else:
+        return value
+
+
+def props_from_args(db, cl, args, itemid=None):
+    """Construct a list of properties from the given arguments,
+    and return them after validation."""
+
+    props = {}
+    for arg in args:
+        if arg.find('=') == -1:
+            raise UsageError, 'argument "%s" not propname=value'%arg
+        l = arg.split('=')
+        if len(l) < 2:
+            raise UsageError, 'argument "%s" not propname=value'%arg
+        key, value = l[0], '='.join(l[1:])
+        if value:
+            try:
+                props[key] = hyperdb.rawToHyperdb(db, cl, itemid,
+                                                  key, value)
+            except hyperdb.HyperdbValueError, message:
+                raise UsageError, message
+        else:
+            props[key] = None
+
+    return props
+
+class RoundupInstance:
+    """The RoundupInstance provides the interface accessible through
+    the Python XMLRPC mapping."""
+
+    def __init__(self, db, actions, translator):
+
+        self.db = db
+        self.actions = actions
+        self.translator = translator
+
+    def list(self, classname, propname=None):
+        cl = self.db.getclass(classname)
+        if not propname:
+            propname = cl.labelprop()
+        result = [cl.get(itemid, propname)
+                  for itemid in cl.list()
+                  if self.db.security.hasPermission('View', self.db.getuid(),
+                                                    classname, propname, itemid)
+                  ]
+        return result
+
+    def filter(self, classname, search_matches, filterspec,
+               sort=[], group=[]):
+        cl = self.db.getclass(classname)
+        result = cl.filter(search_matches, filterspec, sort=sort, group=group)
+        return result
+
+    def display(self, designator, *properties):
+        classname, itemid = hyperdb.splitDesignator(designator)
+        cl = self.db.getclass(classname)
+        props = properties and list(properties) or cl.properties.keys()
+        props.sort()
+        for p in props:
+            if not self.db.security.hasPermission('View', self.db.getuid(),
+                                                  classname, p, itemid):
+                raise Unauthorised('Permission to view %s of %s denied'%
+                                   (p, designator))
+            result = [(prop, cl.get(itemid, prop)) for prop in props]
+        return dict(result)
+
+    def create(self, classname, *args):
+        if not self.db.security.hasPermission('Create', self.db.getuid(), classname):
+            raise Unauthorised('Permission to create %s denied'%classname)
+
+        cl = self.db.getclass(classname)
+
+        # convert types
+        props = props_from_args(self.db, cl, args)
+
+        # check for the key property
+        key = cl.getkey()
+        if key and not props.has_key(key):
+            raise UsageError, 'you must provide the "%s" property.'%key
+
+        # do the actual create
+        try:
+            result = cl.create(**props)
+        except (TypeError, IndexError, ValueError), message:
+            raise UsageError, message
+        return result
+
+    def set(self, designator, *args):
+
+        classname, itemid = hyperdb.splitDesignator(designator)
+        cl = self.db.getclass(classname)
+        props = props_from_args(self.db, cl, args, itemid) # convert types
+        for p in props.iterkeys():
+            if not self.db.security.hasPermission('Edit', self.db.getuid(),
+                                                  classname, p, itemid):
+                raise Unauthorised('Permission to edit %s of %s denied'%
+                                   (p, designator))
+        try:
+            return cl.set(itemid, **props)
+        except (TypeError, IndexError, ValueError), message:
+            raise UsageError, message
+
+
+    builtin_actions = {'retire': actions.Retire}
+
+    def action(self, name, *args):
+        """"""
+        
+        if name in self.actions:
+            action_type = self.actions[name]
+        elif name in self.builtin_actions:
+            action_type = self.builtin_actions[name]
+        else:
+            raise Exception('action "%s" is not supported %s' % (name, ','.join(self.actions.keys())))
+        action = action_type(self.db, self.translator)
+        return action.execute(*args)
+
+
+class RoundupDispatcher(SimpleXMLRPCDispatcher):
+    """RoundupDispatcher bridges from cgi.client to RoundupInstance.
+    It expects user authentication to be done."""
+
+    def __init__(self, db, actions, translator,
+                 allow_none=False, encoding=None):
+
+        SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding)
+        self.register_instance(RoundupInstance(db, actions, translator))
+                 
+
+    def dispatch(self, input):
+        return self._marshaled_dispatch(input)
+
+    def _dispatch(self, method, params):
+
+        retn = SimpleXMLRPCDispatcher._dispatch(self, method, params)
+        retn = translate(retn)
+        return retn
+    

Modified: tracker/roundup-src/run_tests.py
==============================================================================
--- tracker/roundup-src/run_tests.py	(original)
+++ tracker/roundup-src/run_tests.py	Sun Mar 15 22:43:30 2009
@@ -448,7 +448,8 @@
     cycles in your Zope sandbox, so don't do that.
     """
     try:
-        names = os.listdir(top)
+        # Prevent 'hidden' files (those starting with '.') from being considered.
+        names = [f for f in os.listdir(top) if not f.startswith('.')]
     except os.error:
         return
     func(arg, top, names)
@@ -624,28 +625,24 @@
     # Hmm...
     logini = os.path.abspath("log.ini")
 
-    from setup import check_manifest
-    check_manifest()
-
     # Initialize the path and cwd
     global pathinit
     pathinit = PathInit(build, build_inplace, libdir)
 
-# No logging module in py 2.1
-#    # Initialize the logging module.
+    # Initialize the logging module.
 
-#    import logging.config
-#    logging.basicConfig()
+    import logging.config
+    logging.basicConfig()
 
-#    level = os.getenv("LOGGING")
-#    if level:
-#        level = int(level)
-#    else:
-#        level = logging.CRITICAL
-#    logging.root.setLevel(level)
+    level = os.getenv("LOGGING")
+    if level:
+        level = int(level)
+    else:
+        level = logging.CRITICAL
+    logging.root.setLevel(level)
 
-#    if os.path.exists(logini):
-#        logging.config.fileConfig(logini)
+    if os.path.exists(logini):
+        logging.config.fileConfig(logini)
 
     files = find_tests(module_filter)
     files.sort()

Modified: tracker/roundup-src/scripts/add-issue
==============================================================================
--- tracker/roundup-src/scripts/add-issue	(original)
+++ tracker/roundup-src/scripts/add-issue	Sun Mar 15 22:43:30 2009
@@ -1,5 +1,5 @@
 #! /usr/bin/env python
-# $Id: add-issue,v 1.2 2003/04/30 01:28:37 richard Exp $
+# $Id: add-issue,v 1.2 2003-04-30 01:28:37 richard Exp $
 
 '''
 Usage: %s <tracker home> <priority> <issue title>

Modified: tracker/roundup-src/scripts/copy-user.py
==============================================================================
--- tracker/roundup-src/scripts/copy-user.py	(original)
+++ tracker/roundup-src/scripts/copy-user.py	Sun Mar 15 22:43:30 2009
@@ -16,8 +16,8 @@
 """
 
 __version__ = "$Revision: 1.1 $"
-# $Source: /cvsroot/roundup/roundup/scripts/copy-user.py,v $
-# $Id: copy-user.py,v 1.1 2003/12/04 23:13:43 richard Exp $
+# $Source: /home/stefan/projects/roundup-migrate/roundup/scripts/copy-user.py,v $
+# $Id: copy-user.py,v 1.1 2003-12-04 23:13:43 richard Exp $
 
 import sys
 import roundup.instance

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 15 22:43:30 2009
@@ -1,4 +1,4 @@
-''' Import tracker data from Sourceforge.NET
+""" Import tracker data from Sourceforge.NET
 
 This script needs four steps to work:
 
@@ -19,9 +19,11 @@
     roundup-admin -i <tracker home> import /tmp/imported
 
 And you're done!
-'''
+"""
 
-import sys, sets, os, csv, time, urllib2, httplib, mimetypes, urlparse
+import sys, os, csv, time, urllib2, httplib, mimetypes, urlparse
+# Python 2.3 ... 2.6 compatibility:
+from roundup.anypy.sets_ import set
 
 try:
     import cElementTree as ElementTree
@@ -53,8 +55,8 @@
 def fetch_files(xml_file, file_dir):
     """ Fetch files referenced in the xml_file into the dir file_dir. """
     root = ElementTree.parse(xml_file).getroot()
-    to_fetch = sets.Set()
-    deleted = sets.Set()
+    to_fetch = set()
+    deleted = set()
     for artifact in root.find('artifacts'):
         for field in artifact.findall('field'):
             if field.get('name') == 'artifact_id':
@@ -73,7 +75,7 @@
                     deleted.add((aid, fid))
     to_fetch = to_fetch - deleted
 
-    got = sets.Set(os.listdir(file_dir))
+    got = set(os.listdir(file_dir))
     to_fetch = to_fetch - got
 
     # load cached urls (sigh)
@@ -122,10 +124,10 @@
 
     # parse out the XML
     artifacts = []
-    categories = sets.Set()
-    users = sets.Set()
-    add_files = sets.Set()
-    remove_files = sets.Set()
+    categories = set()
+    users = set()
+    add_files = set()
+    remove_files = set()
     for artifact in root.find('artifacts'):
         d = {}
         op = {}
@@ -254,7 +256,7 @@
         else:
             d['status'] = unread
 
-        nosy = sets.Set()
+        nosy = set()
         for message in artifact.get('messages', []):
             authid = users[message['user_name']]
             if not message['body']: continue
@@ -338,7 +340,7 @@
     f.close()
 
 def convert_message(content, id):
-    ''' Strip off the useless sf message header crap '''
+    """ Strip off the useless sf message header crap """
     if content[:14] == 'Logged In: YES':
         return '\n'.join(content.splitlines()[3:]).strip()
     return content

Modified: tracker/roundup-src/scripts/roundup-reminder
==============================================================================
--- tracker/roundup-src/scripts/roundup-reminder	(original)
+++ tracker/roundup-src/scripts/roundup-reminder	Sun Mar 15 22:43:30 2009
@@ -1,4 +1,4 @@
-#! /usr/bin/env python2.2
+#! /usr/bin/env python
 # Copyright (c) 2002 ekit.com Inc (http://www.ekit-inc.com/)
 #
 # Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -19,8 +19,6 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
-# $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
 are currently assigned to them.

Modified: tracker/roundup-src/scripts/roundup.rc-debian
==============================================================================
--- tracker/roundup-src/scripts/roundup.rc-debian	(original)
+++ tracker/roundup-src/scripts/roundup.rc-debian	Sun Mar 15 22:43:30 2009
@@ -2,7 +2,7 @@
 #
 # roundup	Startup script for the roundup http server.
 #
-# Version:	$Id: roundup.rc-debian,v 1.1 2003/10/07 23:02:58 richard Exp $
+# Version:	$Id: roundup.rc-debian,v 1.1 2003-10-07 23:02:58 richard Exp $
 
 DESC='Roundup HTTP-Server'
 

Modified: tracker/roundup-src/scripts/weekly-report
==============================================================================
--- tracker/roundup-src/scripts/weekly-report	(original)
+++ tracker/roundup-src/scripts/weekly-report	Sun Mar 15 22:43:30 2009
@@ -1,4 +1,4 @@
-#! /usr/bin/env python2.3
+#! /usr/bin/env python
 
 # This script generates a simple report outlining the activity in one
 # tracker for the most recent week.

Modified: tracker/roundup-src/setup.py
==============================================================================
--- tracker/roundup-src/setup.py	(original)
+++ tracker/roundup-src/setup.py	Sun Mar 15 22:43:30 2009
@@ -16,17 +16,16 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-# $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
-from distutils.file_util import write_file
-from distutils.command.bdist_rpm import bdist_rpm
-from distutils.command.build import build
-from distutils.command.build_scripts import build_scripts
-from distutils.command.build_py import build_py
 
-import sys, os, string
+from roundup.dist.command.build_doc import build_doc
+from roundup.dist.command.build_scripts import build_scripts
+from roundup.dist.command.build_py import build_py
+from roundup.dist.command.build import build, list_message_files
+from roundup.dist.command.bdist_rpm import bdist_rpm
+from distutils.core import setup
+
+import sys, os
 from glob import glob
 
 # patch distutils if it can't cope with the "classifiers" keyword
@@ -35,268 +34,28 @@
     DistributionMetadata.classifiers = None
     DistributionMetadata.download_url = None
 
-from roundup import msgfmt
-
-#############################################################################
-### Build script files
-#############################################################################
-
-class build_scripts_create(build_scripts):
-    """ Overload the build_scripts command and create the scripts
-        from scratch, depending on the target platform.
-
-        You have to define the name of your package in an inherited
-        class (due to the delayed instantiation of command classes
-        in distutils, this cannot be passed to __init__).
-
-        The scripts are created in an uniform scheme: they start the
-        run() function in the module
-
-            <packagename>.scripts.<mangled_scriptname>
-
-        The mangling of script names replaces '-' and '/' characters
-        with '-' and '.', so that they are valid module paths.
-
-        If the target platform is win32, create .bat files instead of
-        *nix shell scripts.  Target platform is set to "win32" if main
-        command is 'bdist_wininst' or if the command is 'bdist' and
-        it has the list of formats (from command line or config file)
-        and the first item on that list is wininst.  Otherwise
-        target platform is set to current (build) platform.
-    """
-    package_name = None
-
-    def initialize_options(self):
-        build_scripts.initialize_options(self)
-        self.script_preamble = None
-        self.target_platform = None
-        self.python_executable = None
-
-    def finalize_options(self):
-        build_scripts.finalize_options(self)
-        cmdopt=self.distribution.command_options
-
-        # find the target platform
-        if self.target_platform:
-            # TODO? allow explicit setting from command line
-            target = self.target_platform
-        if cmdopt.has_key("bdist_wininst"):
-            target = "win32"
-        elif cmdopt.get("bdist", {}).has_key("formats"):
-            formats = cmdopt["bdist"]["formats"][1].split(",")
-            if formats[0] == "wininst":
-                target = "win32"
-            else:
-                target = sys.platform
-            if len(formats) > 1:
-                self.warn(
-                    "Scripts are built for %s only (requested formats: %s)"
-                    % (target, ",".join(formats)))
-        else:
-            # default to current platform
-            target = sys.platform
-        self.target_platfom = target
-
-        # for native builds, use current python executable path;
-        # for cross-platform builds, use default executable name
-        if self.python_executable:
-            # TODO? allow command-line option
-            pass
-        if target == sys.platform:
-            self.python_executable = os.path.normpath(sys.executable)
-        else:
-            self.python_executable = "python"
-
-        # for windows builds, add ".bat" extension
-        if target == "win32":
-            # *nix-like scripts may be useful also on win32 (cygwin)
-            # to build both script versions, use:
-            #self.scripts = list(self.scripts) + [script + ".bat"
-            #    for script in self.scripts]
-            self.scripts = [script + ".bat" for script in self.scripts]
-
-        # tweak python path for installations outside main python library
-        if cmdopt.get("install", {}).has_key("prefix"):
-            prefix = os.path.expanduser(cmdopt['install']['prefix'][1])
-            version = '%d.%d'%sys.version_info[:2]
-            self.script_preamble = '''
-import sys
-sys.path.insert(1, "%s/lib/python%s/site-packages")
-'''%(prefix, version)
-        else:
-            self.script_preamble = ''
-
-    def copy_scripts(self):
-        """ Create each script listed in 'self.scripts'
-        """
-        if not self.package_name:
-            raise Exception("You have to inherit build_scripts_create and"
-                " provide a package name")
-
-        to_module = string.maketrans('-/', '_.')
-
-        self.mkpath(self.build_dir)
-        for script in self.scripts:
-            outfile = os.path.join(self.build_dir, os.path.basename(script))
-
-            #if not self.force and not newer(script, outfile):
-            #    self.announce("not copying %s (up-to-date)" % script)
-            #    continue
-
-            if self.dry_run:
-                self.announce("would create %s" % outfile)
-                continue
-
-            module = os.path.splitext(os.path.basename(script))[0]
-            module = string.translate(module, to_module)
-            script_vars = {
-                'python': self.python_executable,
-                'package': self.package_name,
-                'module': module,
-                'prefix': self.script_preamble,
-            }
-
-            self.announce("creating %s" % outfile)
-            file = open(outfile, 'w')
-
-            try:
-                # could just check self.target_platform,
-                # but looking at the script extension
-                # makes it possible to build both *nix-like
-                # and windows-like scripts on win32.
-                # may be useful for cygwin.
-                if os.path.splitext(outfile)[1] == ".bat":
-                    file.write('@echo off\n'
-                        'if NOT "%%_4ver%%" == "" "%(python)s" -c "from %(package)s.scripts.%(module)s import run; run()" %%$\n'
-                        'if     "%%_4ver%%" == "" "%(python)s" -c "from %(package)s.scripts.%(module)s import run; run()" %%*\n'
-                        % script_vars)
-                else:
-                    file.write('#! %(python)s\n%(prefix)s'
-                        'from %(package)s.scripts.%(module)s import run\n'
-                        'run()\n'
-                        % script_vars)
-            finally:
-                file.close()
-                os.chmod(outfile, 0755)
+def include(d, e):
+    """Generate a pair of (directory, file-list) for installation.
 
+    'd' -- A directory
 
-class build_scripts_roundup(build_scripts_create):
-    package_name = 'roundup'
+    'e' -- A glob pattern"""
 
+    return (d, [f for f in glob('%s/%s'%(d, e)) if os.path.isfile(f)])
 
 def scriptname(path):
     """ Helper for building a list of script names from a list of
         module files.
     """
     script = os.path.splitext(os.path.basename(path))[0]
-    script = string.replace(script, '_', '-')
+    script = script.replace('_', '-')
     return script
 
-### Build Roundup
-
-def list_message_files(suffix=".po"):
-    """Return list of all found message files and their intallation paths"""
-    _files = glob("locale/*" + suffix)
-    _list = []
-    for _file in _files:
-        # basename (without extension) is a locale name
-        _locale = os.path.splitext(os.path.basename(_file))[0]
-        _list.append((_file, os.path.join(
-            "share", "locale", _locale, "LC_MESSAGES", "roundup.mo")))
-    return _list
-
-def check_manifest():
-    """Check that the files listed in the MANIFEST are present when the
-    source is unpacked.
-    """
-    try:
-        f = open('MANIFEST')
-    except:
-        print '\n*** SOURCE WARNING: The MANIFEST file is missing!'
-        return
-    try:
-        manifest = [l.strip() for l in f.readlines()]
-    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)!'%(
-            n-len(err), n)
-        print 'Missing:', '\nMissing: '.join(err)
-
-
-class build_py_roundup(build_py):
-
-    def find_modules(self):
-        # Files listed in py_modules are in the toplevel directory
-        # of the source distribution.
-        modules = []
-        for module in self.py_modules:
-            path = string.split(module, '.')
-            package = string.join(path[0:-1], '.')
-            module_base = path[-1]
-            module_file = module_base + '.py'
-            if self.check_module(module, module_file):
-                modules.append((package, module_base, module_file))
-        return modules
-
-
-class build_roundup(build):
-
-    def build_message_files(self):
-        """For each locale/*.po, build .mo file in target locale directory"""
-        for (_src, _dst) in list_message_files():
-            _build_dst = os.path.join("build", _dst)
-            self.mkpath(os.path.dirname(_build_dst))
-            self.announce("Compiling %s -> %s" % (_src, _build_dst))
-            msgfmt.make(_src, _build_dst)
-
-    def run(self):
-        check_manifest()
-        self.build_message_files()
-        build.run(self)
-
-class bdist_rpm_roundup(bdist_rpm):
-
-    def finalize_options(self):
-        bdist_rpm.finalize_options(self)
-        if self.install_script:
-            # install script is overridden.  skip default
-            return
-        # install script option must be file name.
-        # create the file in rpm build directory.
-        install_script = os.path.join(self.rpm_base, "install.sh")
-        self.mkpath(self.rpm_base)
-        self.execute(write_file, (install_script, [
-                ("%s setup.py install --root=$RPM_BUILD_ROOT "
-                    "--record=ROUNDUP_FILES") % self.python,
-                # allow any additional extension for man pages
-                # (rpm may compress them to .gz or .bz2)
-                # man page here is any file
-                # with single-character extension
-                # in man directory
-                "sed -e 's,\(/man/.*\..\)$,\\1*,' "
-                    "<ROUNDUP_FILES >INSTALLED_FILES",
-            ]), "writing '%s'" % install_script)
-        self.install_script = install_script
-
-#############################################################################
-### Main setup stuff
-#############################################################################
-
 def main():
-    # build list of scripts from their implementation modules
-    roundup_scripts = map(scriptname, glob('roundup/scripts/[!_]*.py'))
-
     # template munching
-    packagelist = [
+    packages = [
         'roundup',
+        'roundup.anypy',
         'roundup.cgi',
         'roundup.cgi.PageTemplates',
         'roundup.cgi.TAL',
@@ -304,83 +63,71 @@
         'roundup.backends',
         'roundup.scripts',
     ]
-    installdatafiles = [
-        ('share/roundup/cgi-bin', ['frontends/roundup.cgi']),
-    ]
     py_modules = ['roundup.demo',]
 
+    # build list of scripts from their implementation modules
+    scripts = [scriptname(f) for f in glob('roundup/scripts/[!_]*.py')]
+
+    data_files = [
+        ('share/roundup/cgi-bin', ['frontends/roundup.cgi']),
+    ]
     # install man pages on POSIX platforms
     if os.name == 'posix':
-        installdatafiles.append(('man/man1', ['doc/roundup-admin.1',
-            'doc/roundup-mailgw.1', 'doc/roundup-server.1',
-            'doc/roundup-demo.1']))
+        data_files.append(include('share/man/man1', '*'))
 
     # add the templates to the data files lists
     from roundup.init import listTemplates
-    templates = [t['path'] for t in listTemplates('templates').values()]
+    templates = [t['path']
+                 for t in listTemplates('share/roundup/templates').values()]
     for tdir in templates:
-        # scan for data files
         for idir in '. detectors extensions html'.split():
-            idir = os.path.join(tdir, idir)
-            if not os.path.isdir(idir):
-                continue
-            tfiles = []
-            for f in os.listdir(idir):
-                if f.startswith('.'):
-                    continue
-                ifile = os.path.join(idir, f)
-                if os.path.isfile(ifile):
-                    tfiles.append(ifile)
-            installdatafiles.append(
-                (os.path.join('share', 'roundup', idir), tfiles)
-            )
+            data_files.append(include(os.path.join(tdir, idir), '*'))
 
     # add message files
     for (_dist_file, _mo_file) in list_message_files():
-        installdatafiles.append((os.path.dirname(_mo_file),
-            [os.path.join("build", _mo_file)]))
+        data_files.append((os.path.dirname(_mo_file),
+                           [os.path.join("build", _mo_file)]))
+
+    # add docs
+    data_files.append(include('share/doc/roundup/html', '*'))
 
     # perform the setup action
     from roundup import __version__
-    setup_args = {
-        'name': "roundup",
-        'version': __version__,
-        'description': "A simple-to-use and -install issue-tracking system"
+
+    setup(name='roundup',
+          version=__version__,
+          author="Richard Jones",
+          author_email="richard at users.sourceforge.net",
+          description="A simple-to-use and -install issue-tracking system"
             " with command-line, web and e-mail interfaces. Highly"
             " customisable.",
-        'long_description':
+          long_description=
 '''In this release
 ===============
 
-The metakit backend has been removed due to lack of maintenance and
-presence of good alternatives (in particular sqlite built into Python 2.5)
-
-Release 1.4.1 removes an old trace of the metakit backend that was
-preventing new tracker installation.
-
-New Features in 1.4.0:
+1.4.7 is primarily a bugfix release which contains important security
+fixes:
 
-- 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.
+- a number of security issues were discovered by Daniel Diniz
+- EditCSV and ExportCSV altered to include permission checks
+- HTTP POST required on actions which alter data
+- HTML file uploads served as application/octet-stream
+- Handle Unauthorised in file serving correctly
+- New item action reject creation of new users
+- Item retirement was not being controlled
+- Roundup is now compatible with Python 2.6
+- Improved French and German translations
+- Improve consistency of item sorting in HTML interface
+- Various other small bug fixes, robustification and optimisation
+
+Though some new features made it in also:
+
+- Provide a "no selection" option in web interface selection widgets
+- Debug logging now uses the logging module rather than print
+- Allow CGI frontend to serve XMLRPC requests.
+- Added XMLRPC actions, as well as bridging CGI actions to XMLRPC actions.
+- Optimized large file serving via mod_python / sendfile().
+- Support resuming downloads for (large) files.
 
 If you're upgrading from an older version of Roundup you *must* follow
 the "Software Upgrade" guidelines given in the maintenance documentation.
@@ -424,42 +171,35 @@
 a minimal skeleton) and five database back-ends (anydbm, sqlite, metakit,
 mysql and postgresql).
 ''',
-        'author': "Richard Jones",
-        'author_email': "richard at users.sourceforge.net",
-        'url': 'http://roundup.sourceforge.net/',
-        'packages': packagelist,
-        'classifiers': [
-            'Development Status :: 5 - Production/Stable',
-            'Environment :: Console',
-            'Environment :: Web Environment',
-            'Intended Audience :: End Users/Desktop',
-            'Intended Audience :: Developers',
-            'Intended Audience :: System Administrators',
-            'License :: OSI Approved :: Python Software Foundation License',
-            'Operating System :: MacOS :: MacOS X',
-            'Operating System :: Microsoft :: Windows',
-            'Operating System :: POSIX',
-            'Programming Language :: Python',
-            'Topic :: Communications :: Email',
-            'Topic :: Office/Business',
-            'Topic :: Software Development :: Bug Tracking',
-        ],
-
-        # Override certain command classes with our own ones
-        'cmdclass': {
-            'build_scripts': build_scripts_roundup,
-            'build_py': build_py_roundup,
-            'build': build_roundup,
-            'bdist_rpm': bdist_rpm_roundup,
-        },
-        'scripts': roundup_scripts,
-
-        'data_files':  installdatafiles
-    }
-    if sys.version_info[:2] > (2, 2):
-       setup_args['py_modules'] = py_modules
-
-    setup(**setup_args)
+          url='http://www.roundup-tracker.org',
+          download_url='http://pypi.python.org/pypi/roundup',
+          classifiers=['Development Status :: 5 - Production/Stable',
+                       'Environment :: Console',
+                       'Environment :: Web Environment',
+                       'Intended Audience :: End Users/Desktop',
+                       'Intended Audience :: Developers',
+                       'Intended Audience :: System Administrators',
+                       'License :: OSI Approved :: Python Software Foundation License',
+                       'Operating System :: MacOS :: MacOS X',
+                       'Operating System :: Microsoft :: Windows',
+                       'Operating System :: POSIX',
+                       'Programming Language :: Python',
+                       'Topic :: Communications :: Email',
+                       'Topic :: Office/Business',
+                       'Topic :: Software Development :: Bug Tracking',
+                       ],
+
+          # Override certain command classes with our own ones
+          cmdclass= {'build_doc': build_doc,
+                     'build_scripts': build_scripts,
+                     'build_py': build_py,
+                     'build': build,
+                     'bdist_rpm': bdist_rpm,
+                     },
+          packages=packages,
+          py_modules=py_modules,
+          scripts=scripts,
+          data_files=data_files)
 
 if __name__ == '__main__':
     main()

Added: tracker/roundup-src/share/man/man1/roundup-admin.1
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/man/man1/roundup-admin.1	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,26 @@
+.TH ROUNDUP-ADMIN 1 "24 January 2003"
+.SH NAME
+roundup-admin \- administrate roundup trackers
+.SH SYNOPSIS
+\fBroundup-admin\fP [\fIoptions\fP] \fI<command>\fP \fI<arguments>\fP
+.SH OPTIONS
+.TP
+\fB-i\fP \fIinstance home\fP
+specify the issue tracker "home directory" to administer
+.TP
+\fB-u\fP
+the user[:password] to use for commands
+.TP
+\fB-c\fP
+when outputting lists of data, just comma-separate them
+.SH FURTHER HELP
+ roundup-admin -h
+ roundup-admin help                       -- this help
+ roundup-admin help <command>             -- command-specific help
+ roundup-admin help all                   -- all available help
+.SH AUTHOR
+This manpage was written by Bastian Kleineidam
+<calvin at debian.org> for the Debian distribution of roundup.
+
+The main author of roundup is Richard Jones
+<richard at users.sourceforge.net>.

Added: tracker/roundup-src/share/man/man1/roundup-demo.1
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/man/man1/roundup-demo.1	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,30 @@
+.TH ROUNDUP-SERVER 1 "27 July 2004"
+.SH NAME
+roundup-demo \- create a roundup "demo" tracker and launch its web interface
+.SH SYNOPSIS
+\fBroundup-demo\fP [\fIbackend\fP [\fBnuke\fP]]
+.SH OPTIONS
+.TP
+\fBnuke\fP
+Create a fresh demo tracker (deleting the existing one if any). If the
+additional \fIbackend\fP argument is specified, the new demo tracker will
+use the backend named (one of "anydbm", "sqlite", "metakit", "mysql" or
+"postgresql"; subject to availability on your system).
+.SH DESCRIPTION
+This command creates a fresh demo tracker for you to experiment with. The
+email features of Roundup will be turned off (so the nosy feature won't
+send email). It does this by removing the \fInosyreaction.py\fP module
+from the demo tracker's \fIdetectors\fP directory.
+
+If you wish, you may modify the demo tracker by editing its configuration
+files and HTML templates. See the \fIcustomisation\fP manual for
+information about how to do that.
+
+Once you've fiddled with the demo tracker, you may use it as a template for
+creating your real, live tracker. Simply run the \fIroundup-admin\fP
+command to install the tracker from inside the demo tracker home directory,
+and it will be listed as an available template for installation. No data
+will be copied over.
+.SH AUTHOR
+This manpage was written by Richard Jones
+<richard at users.sourceforge.net>.

Added: tracker/roundup-src/share/man/man1/roundup-mailgw.1
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/man/man1/roundup-mailgw.1	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,65 @@
+.TH ROUNDUP-MAILGW 1 "24 January 2003"
+.SH NAME
+roundup-mailgw \- mail gateway for roundup
+.SH SYNOPSIS
+\fBroundup-mailgw\fP \fI<instance home>\fP [\fImethod\fP]
+.SH OPTIONS
+.TP
+\fB-C\fP \fIhyperdb class\fP
+specify a tracker class - one of msg (the default), issue, file, user - to
+manipulate with -S options
+.TP
+\fB-S\fP \fIproperty=value[;property=value] pairs\fP
+specify the values to set on the class specified by -C using the same
+format as the Subject line property manipulations
+.SH DESCRIPTION
+The roundup mail gateway may be called in one of three ways:
+.IP \(bu
+with an instance home as the only argument,
+.IP \(bu
+with both an instance home and a mail spool file, or
+.IP \(bu
+with both an instance home and a pop server account.
+.PP
+\fBPIPE\fP
+.br
+In the first case, the mail gateway reads a single message from the
+standard input and submits the message to the roundup.mailgw module.
+
+\fBUNIX mailbox\fP
+.br
+In the second case, the gateway reads all messages from the mail spool
+file and submits each in turn to the roundup.mailgw module. The file is
+emptied once all messages have been successfully handled. The file is
+specified as:
+ \fImailbox /path/to/mailbox\fP
+
+In all of the following the username and password can be stored in a
+~/.netrc file. In this case only the server name need be specified on
+the command-line.
+
+The username and/or password will be prompted for if not supplied on
+the command-line or in ~/.netrc.
+
+\fBPOP\fP
+.br
+In the third case, the gateway reads all messages from the POP server
+specified and submits each in turn to the roundup.mailgw module. The
+server is specified as:
+ \fIpop username:password at server\fP
+.br
+The username and password may be omitted:
+ \fIpop username at server\fP
+ \fIpop server\fP
+.br
+are both valid.
+
+\fBAPOP\fP
+Same as POP, but using Authenticated POP:
+ \fIapop username:password at server\fP
+.SH AUTHOR
+This manpage was written by Bastian Kleineidam
+<calvin at debian.org> for the Debian distribution of roundup.
+
+The main author of roundup is Richard Jones
+<richard at users.sourceforge.net>.

Added: tracker/roundup-src/share/man/man1/roundup-server.1
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/man/man1/roundup-server.1	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,61 @@
+.TH ROUNDUP-SERVER 1 "27 July 2004"
+.SH NAME
+roundup-server \- start roundup web server
+.SH SYNOPSIS
+\fBroundup-server\fP [\fIoptions\fP] [\fBname=\fP\fItracker home\fP]*
+.SH OPTIONS
+.TP
+\fB-C\fP \fIfile\fP
+Use options read from the configuration file (see below).
+.TP
+\fB-n\fP \fIhostname\fP
+Sets the host name.
+.TP
+\fB-p\fP \fIport\fP
+Sets the port to listen on.
+.TP
+\fB-d\fP \fIfile\fP
+Daemonize, and write the server's PID to the nominated file.
+.TP
+\fB-l\fP \fIfile\fP
+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 \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
+This manpage was written by Bastian Kleineidam
+<calvin at debian.org> for the Debian distribution of roundup.
+
+The main author of roundup is Richard Jones
+<richard at users.sourceforge.net>.

Added: tracker/roundup-src/share/roundup/templates/classic/TEMPLATE-INFO.txt
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/TEMPLATE-INFO.txt	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,7 @@
+Name: classic
+Description: This is a generic issue tracker that may be used to track bugs,
+             feature requests, project issues or any number of other types
+             of issues. Most users of Roundup will find that this template
+             suits them, with perhaps a few customisations.
+Intended-For: All first-time Roundup users
+

Added: tracker/roundup-src/share/roundup/templates/classic/detectors/messagesummary.py
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/detectors/messagesummary.py	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,19 @@
+#$Id$
+
+from roundup.mailgw import parseContent
+
+def summarygenerator(db, cl, nodeid, newvalues):
+    ''' If the message doesn't have a summary, make one for it.
+    '''
+    if newvalues.has_key('summary') or not newvalues.has_key('content'):
+        return
+
+    summary, content = parseContent(newvalues['content'], config=db.config)
+    newvalues['summary'] = summary
+
+
+def init(db):
+    # fire before changes are made
+    db.msg.audit('create', summarygenerator)
+
+# vim: set filetype=python ts=4 sw=4 et si

Added: tracker/roundup-src/share/roundup/templates/classic/detectors/nosyreaction.py
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/detectors/nosyreaction.py	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,144 @@
+#
+# 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$
+
+# Python 2.3 ... 2.6 compatibility:
+from roundup.anypy.sets_ import set
+
+from roundup import roundupdb, hyperdb
+
+def nosyreaction(db, cl, nodeid, oldvalues):
+    ''' A standard detector is provided that watches for additions to the
+        "messages" property.
+        
+        When a new message is added, the detector sends it to all the users on
+        the "nosy" list for the issue that are not already on the "recipients"
+        list of the message.
+        
+        Those users are then appended to the "recipients" property on the
+        message, so multiple copies of a message are never sent to the same
+        user.
+        
+        The journal recorded by the hyperdatabase on the "recipients" property
+        then provides a log of when the message was sent to whom. 
+    '''
+    # send a copy of all new messages to the nosy list
+    for msgid in determineNewMessages(cl, nodeid, oldvalues):
+        try:
+            cl.nosymessage(nodeid, msgid, oldvalues)
+        except roundupdb.MessageSendError, message:
+            raise roundupdb.DetectorError, message
+
+def determineNewMessages(cl, nodeid, oldvalues):
+    ''' Figure a list of the messages that are being added to the given
+        node in this transaction.
+    '''
+    messages = []
+    if oldvalues is None:
+        # the action was a create, so use all the messages in the create
+        messages = cl.get(nodeid, 'messages')
+    elif oldvalues.has_key('messages'):
+        # the action was a set (so adding new messages to an existing issue)
+        m = {}
+        for msgid in oldvalues['messages']:
+            m[msgid] = 1
+        messages = []
+        # figure which of the messages now on the issue weren't there before
+        for msgid in cl.get(nodeid, 'messages'):
+            if not m.has_key(msgid):
+                messages.append(msgid)
+    return messages
+
+def updatenosy(db, cl, nodeid, newvalues):
+    '''Update the nosy list for changes to the assignedto
+    '''
+    # nodeid will be None if this is a new node
+    current_nosy = set()
+    if nodeid is None:
+        ok = ('new', 'yes')
+    else:
+        ok = ('yes',)
+        # old node, get the current values from the node if they haven't
+        # changed
+        if not newvalues.has_key('nosy'):
+            nosy = cl.get(nodeid, 'nosy')
+            for value in nosy:
+                current_nosy.add(value)
+
+    # if the nosy list changed in this transaction, init from the new value
+    if newvalues.has_key('nosy'):
+        nosy = newvalues.get('nosy', [])
+        for value in nosy:
+            if not db.hasnode('user', value):
+                continue
+            current_nosy.add(value)
+
+    new_nosy = set(current_nosy)
+
+    # add assignedto(s) to the nosy list
+    if newvalues.has_key('assignedto') and newvalues['assignedto'] is not None:
+        propdef = cl.getprops()
+        if isinstance(propdef['assignedto'], hyperdb.Link):
+            assignedto_ids = [newvalues['assignedto']]
+        elif isinstance(propdef['assignedto'], hyperdb.Multilink):
+            assignedto_ids = newvalues['assignedto']
+        for assignedto_id in assignedto_ids:
+            new_nosy.add(assignedto_id)
+
+    # see if there's any new messages - if so, possibly add the author and
+    # recipient to the nosy
+    if newvalues.has_key('messages'):
+        if nodeid is None:
+            ok = ('new', 'yes')
+            messages = newvalues['messages']
+        else:
+            ok = ('yes',)
+            # figure which of the messages now on the issue weren't
+            oldmessages = cl.get(nodeid, 'messages')
+            messages = []
+            for msgid in newvalues['messages']:
+                if msgid not in oldmessages:
+                    messages.append(msgid)
+
+        # configs for nosy modifications
+        add_author = getattr(db.config, 'ADD_AUTHOR_TO_NOSY', 'new')
+        add_recips = getattr(db.config, 'ADD_RECIPIENTS_TO_NOSY', 'new')
+
+        # now for each new message:
+        msg = db.msg
+        for msgid in messages:
+            if add_author in ok:
+                authid = msg.get(msgid, 'author')
+                new_nosy.add(authid)
+
+            # add on the recipients of the message
+            if add_recips in ok:
+                for recipient in msg.get(msgid, 'recipients'):
+                    new_nosy.add(recipient)
+
+    if current_nosy != new_nosy:
+        # that's it, save off the new nosy list
+        newvalues['nosy'] = list(new_nosy)
+
+def init(db):
+    db.issue.react('create', nosyreaction)
+    db.issue.react('set', nosyreaction)
+    db.issue.audit('create', updatenosy)
+    db.issue.audit('set', updatenosy)
+
+# vim: set filetype=python ts=4 sw=4 et si

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

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

Added: tracker/roundup-src/share/roundup/templates/classic/extensions/README.txt
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/extensions/README.txt	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,6 @@
+This directory is for tracker extensions:
+
+- CGI Actions
+- Templating functions
+
+See the customisation doc for more information.

Added: tracker/roundup-src/share/roundup/templates/classic/html/_generic.404.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/html/_generic.404.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,9 @@
+<html>
+<head>
+<title>Item Not Found</title>
+</head>
+
+<body>
+There is no <span tal:content="context/_classname" /> with id <span tal:content="context/id"/>
+</body>
+</html>

Added: tracker/roundup-src/share/roundup/templates/classic/html/_generic.calendar.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/html/_generic.calendar.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,18 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+ <head>
+  <link rel="stylesheet" type="text/css" href="@@file/style.css" />
+  <meta http-equiv="Content-Type" content="text/html; charset=utf-8;" />
+  <title tal:content="string:Roundup Calendar"></title>
+  <script language="Javascript"
+          type="text/javascript"
+          tal:content="structure string:
+          // this is the name of the field in the original form that we're working on
+          form  = window.opener.document.${request/form/form/value};
+          field = '${request/form/property/value}';" >
+  </script>
+ </head>
+ <body class="body"
+       tal:content="structure python:utils.html_calendar(request)">
+ </body>
+</html>

Added: tracker/roundup-src/share/roundup/templates/classic/html/_generic.collision.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/html/_generic.collision.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,16 @@
+<tal:block metal:use-macro="templates/page/macros/icing">
+<title metal:fill-slot="head_title" i18n:translate=""
+ ><span tal:replace="python:context._classname.capitalize()"
+ i18n:name="class" /> Edit Collision - <span i18n:name="tracker"
+ tal:replace="config/TRACKER_NAME" /></title>
+<tal:block metal:fill-slot="body_title" i18n:translate=""
+ ><span tal:replace="python:context._classname.capitalize()"
+ i18n:name="class" /> Edit Collision</tal:block>
+
+<td class="content" metal:fill-slot="content" i18n:translate="
+  There has been a collision. Another user updated this node
+  while you were editing. Please <a href='${context}'>reload</a>
+  the node and review your edits.
+"><span tal:replace="context/designator" i18n:name="context" />
+</td>
+</tal:block>

Added: tracker/roundup-src/share/roundup/templates/classic/html/_generic.help-empty.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/html/_generic.help-empty.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,8 @@
+<html>
+  <head>
+    <title>Empty page (no search performed yet)</title>
+  </head>
+  <body>
+    <p i18n:translate="">Please specify your search parameters!</p>
+  </body>
+</html>

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

Added: tracker/roundup-src/share/roundup/templates/classic/html/_generic.help-search.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/html/_generic.help-search.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,13 @@
+<html>
+  <head>
+    <title>Frame for search input fields</title>
+  </head>
+  <body>
+    <p i18n:translate="">Generic template
+    <span tal:replace="request/template" i18n:name="template">help-search</span>
+    or version for class
+    <span tal:replace="request/classname" i18n:name="classname">user</span>
+    is not yet implemented</p>
+  </body>
+</html>
+

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

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

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

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

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

Added: tracker/roundup-src/share/roundup/templates/classic/html/file.item.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/html/file.item.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,53 @@
+<tal:block metal:use-macro="templates/page/macros/icing">
+<title metal:fill-slot="head_title" i18n:translate="">File display - <span
+ i18n:name="tracker" tal:replace="config/TRACKER_NAME" /></title>
+<span metal:fill-slot="body_title" tal:omit-tag="python:1"
+ i18n:translate="">File display</span>
+
+<td class="content" metal:fill-slot="content">
+
+<p tal:condition="python:not (context.is_view_ok()
+ or request.user.hasRole('Anonymous'))" i18n:translate="">
+ You are not allowed to view this page.</p>
+
+<p tal:condition="python:not context.is_view_ok()
+ and request.user.hasRole('Anonymous')" i18n:translate="">
+ Please login with your username and password.</p>
+
+<form method="POST" onSubmit="return submit_once()"
+      enctype="multipart/form-data" tal:condition="context/is_view_ok"
+      tal:attributes="action context/designator">
+
+<table class="form">
+ <tr>
+  <th i18n:translate="">Name</th>
+  <td tal:content="structure context/name/field"></td>
+ </tr>
+ <tr>
+  <th i18n:translate="">Content Type</th>
+  <td tal:content="structure context/type/field"></td>
+ </tr>
+
+ <tr>
+  <td>
+   &nbsp;
+   <input type="hidden" name="@template" value="item">
+   <input type="hidden" name="@required" value="name,type">
+   <input type="hidden" name="@multilink"
+          tal:condition="python:request.form.has_key('@multilink')"
+          tal:attributes="value request/form/@multilink/value">
+  </td>
+  <td tal:content="structure context/submit">submit button here</td>
+ </tr>
+</table>
+</form>
+
+<a tal:condition="python:context.id and context.is_view_ok()"
+ tal:attributes="href string:file${context/id}/${context/name}"
+ i18n:translate="">download</a>
+
+<tal:block tal:condition="context/id" tal:replace="structure context/history" />
+
+</td>
+
+</tal:block>

Added: tracker/roundup-src/share/roundup/templates/classic/html/help.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/html/help.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,38 @@
+<!--
+Macros for framed help windows
+-->
+
+<!-- legend for helper search results -->
+<thead>
+<tr metal:define-macro="legend">
+  <th><b>x</b></th>
+  <th tal:repeat="prop props" tal:content="prop" i18n:translate=""></th>
+</tr>
+</thead>
+
+<table width="100%"
+  metal:define-macro="batch_navi"
+  tal:define="prev batch/previous;
+  next batch/next;
+  "
+  tal:condition="python:prev or next">
+  <tr class="navigation">
+   <th width="30%">
+    <a tal:condition="prev"
+       tal:attributes="href python:request.indexargs_url(request.classname, {'@template':'help-list', 'property': request.form['property'].value, 'properties': request.form['properties'].value, 'form': request.form['form'].value, '@startwith':prev.first, '@pagesize':prev.size})"
+       i18n:translate="" >&lt;&lt; previous</a>
+    &nbsp;
+   </th>
+   <th i18n:translate="" width="40%"><span tal:replace="batch/start" i18n:name="start"
+    />..<span tal:replace="python: batch.start + batch.length -1" i18n:name="end"
+    /> out of <span tal:replace="batch/sequence_length" i18n:name="total"
+    />
+   </th>
+   <th width="30%">
+    <a tal:condition="next"
+       tal:attributes="href python:request.indexargs_url(request.classname, {'@template':'help-list', 'property': request.form['property'].value, 'properties': request.form['properties'].value, 'form': request.form['form'].value, '@startwith':next.first, '@pagesize':next.size})"
+       i18n:translate="" >next &gt;&gt;</a>
+    &nbsp;
+   </th>
+  </tr>
+ </table>

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

Added: tracker/roundup-src/share/roundup/templates/classic/html/home.classlist.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/html/home.classlist.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,25 @@
+<tal:block metal:use-macro="templates/page/macros/icing">
+<title metal:fill-slot="head_title" i18n:translate="">List of classes - <span
+ i18n:name="tracker" tal:replace="config/TRACKER_NAME" /></title>
+<span metal:fill-slot="body_title" tal:omit-tag="python:1"
+ i18n:translate="">List of classes</span>
+<td class="content" metal:fill-slot="content">
+<table class="classlist">
+
+<tal:block tal:repeat="cl db/classes">
+ <tr>
+  <th class="header" colspan="2" align="left">
+   <a tal:attributes="href string:${cl/classname}"
+      tal:content="python:cl.classname.capitalize()">classname</a>
+  </th>
+ </tr>
+ <tr tal:repeat="prop cl/properties">
+  <th tal:content="prop/_name">name</th>
+  <td tal:content="prop/_prop">type</td>
+ </tr>
+</tal:block>
+
+</table>
+</td>
+
+</tal:block>

Added: tracker/roundup-src/share/roundup/templates/classic/html/home.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/html/home.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,10 @@
+<!--
+ This is the default body that is displayed when people visit the
+ tracker. The tag below lists the currently open issues. You may
+ replace it with a greeting message, or a different list of issues or
+ whatever. It's a good idea to have the issues on the front page though
+-->
+<span tal:replace="structure python:db.issue.renderWith('index',
+    sort=[('-', 'activity')], group=[('+', 'priority')], filter=['status'],
+    columns=['id','activity','title','creator','assignedto', 'status'],
+    filterspec={'status':['-1','1','2','3','4','5','6','7']})" />

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

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

Added: tracker/roundup-src/share/roundup/templates/classic/html/issue.search.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/html/issue.search.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,234 @@
+<tal:block metal:use-macro="templates/page/macros/icing">
+<title metal:fill-slot="head_title" i18n:translate="">Issue searching - <span
+ i18n:name="tracker" tal:replace="config/TRACKER_NAME" /></title>
+<span metal:fill-slot="body_title" tal:omit-tag="python:1"
+ i18n:translate="">Issue searching</span>
+<td class="content" metal:fill-slot="content">
+
+<form method="GET" name="itemSynopsis"
+      tal:attributes="action request/classname">
+      
+<table class="form" tal:define="
+   cols python:request.columns or 'id activity title status assignedto'.split();
+   sort_on python:request.sort and request.sort[0] or nothing;
+   sort_desc python:sort_on and sort_on[0] == '-';
+   sort_on python:(sort_on and sort_on[1]) or 'activity';
+   group_on python:request.group and request.group[0] or nothing;
+   group_desc python:group_on and group_on[0] == '-';
+   group_on python:(group_on and group_on[1]) or 'priority';
+
+   search_input templates/page/macros/search_input;
+   search_date templates/page/macros/search_date;
+   column_input templates/page/macros/column_input;
+   sort_input templates/page/macros/sort_input;
+   group_input templates/page/macros/group_input;
+   search_select templates/page/macros/search_select;
+   search_select_translated templates/page/macros/search_select_translated;
+   search_multiselect templates/page/macros/search_multiselect;">
+
+<tr>
+ <th class="header">&nbsp;</th>
+ <th class="header" i18n:translate="">Filter on</th>
+ <th class="header" i18n:translate="">Display</th>
+ <th class="header" i18n:translate="">Sort on</th>
+ <th class="header" i18n:translate="">Group on</th>
+</tr>
+
+<tr tal:define="name string:@search_text">
+  <th i18n:translate="">All text*:</th>
+  <td metal:use-macro="search_input"></td>
+  <td>&nbsp;</td>
+  <td>&nbsp;</td>
+  <td>&nbsp;</td>
+</tr>
+
+<tr tal:define="name string:title">
+  <th i18n:translate="">Title:</th>
+  <td metal:use-macro="search_input"></td>
+  <td metal:use-macro="column_input"></td>
+  <td metal:use-macro="sort_input"></td>
+  <td>&nbsp;</td>
+</tr>
+
+<tr tal:define="name string:keyword;
+                db_klass string:keyword;
+                db_content string:name;">
+  <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>
+</tr>
+
+<tr tal:define="name string:id">
+  <th i18n:translate="">ID:</th>
+  <td metal:use-macro="search_input"></td>
+  <td metal:use-macro="column_input"></td>
+  <td metal:use-macro="sort_input"></td>
+  <td>&nbsp;</td>
+</tr>
+
+<tr tal:define="name string:creation">
+  <th i18n:translate="">Creation Date:</th>
+  <td metal:use-macro="search_date"></td>
+  <td metal:use-macro="column_input"></td>
+  <td metal:use-macro="sort_input"></td>
+  <td metal:use-macro="group_input"></td>
+</tr>
+
+<tr tal:define="name string:creator;
+                db_klass string:user;
+                db_content string:username;"
+    tal:condition="db/user/is_view_ok">
+  <th i18n:translate="">Creator:</th>
+  <td metal:use-macro="search_select">
+    <option metal:fill-slot="extra_options" i18n:translate=""
+            tal:attributes="value request/user/id">created by me</option>
+  </td>
+  <td metal:use-macro="column_input"></td>
+  <td metal:use-macro="sort_input"></td>
+  <td metal:use-macro="group_input"></td>
+</tr>
+
+<tr tal:define="name string:activity">
+  <th i18n:translate="">Activity:</th>
+  <td metal:use-macro="search_date"></td>
+  <td metal:use-macro="column_input"></td>
+  <td metal:use-macro="sort_input"></td>
+  <td>&nbsp;</td>
+</tr>
+
+<tr tal:define="name string:actor;
+                db_klass string:user;
+                db_content string:username;"
+    tal:condition="db/user/is_view_ok">
+  <th i18n:translate="">Actor:</th>
+  <td metal:use-macro="search_select">
+    <option metal:fill-slot="extra_options" i18n:translate=""
+            tal:attributes="value request/user/id">done by me</option>
+  </td>
+  <td metal:use-macro="column_input"></td>
+  <td metal:use-macro="sort_input"></td>
+  <td>&nbsp;</td>
+</tr>
+
+<tr tal:define="name string:priority;
+                db_klass string:priority;
+                db_content string:name;">
+  <th i18n:translate="">Priority:</th>
+  <td metal:use-macro="search_select_translated">
+    <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>
+</tr>
+
+<tr tal:define="name string:status;
+                db_klass string:status;
+                db_content string:name;">
+  <th i18n:translate="">Status:</th>
+  <td metal:use-macro="search_select_translated">
+    <tal:block metal:fill-slot="extra_options">
+      <option value="-1,1,2,3,4,5,6,7" i18n:translate=""
+              tal:attributes="selected python:value == '-1,1,2,3,4,5,6,7'">not resolved</option>
+      <option value="-1" i18n:translate=""
+              tal:attributes="selected python:value == '-1'">not selected</option>
+    </tal:block>
+  </td>
+  <td metal:use-macro="column_input"></td>
+  <td metal:use-macro="sort_input"></td>
+  <td metal:use-macro="group_input"></td>
+</tr>
+
+<tr tal:define="name string:assignedto;
+                db_klass string:user;
+                db_content string:username;"
+    tal:condition="db/user/is_view_ok">
+  <th i18n:translate="">Assigned to:</th>
+  <td metal:use-macro="search_select">
+    <tal:block metal:fill-slot="extra_options">
+      <option tal:attributes="value request/user/id"
+       i18n:translate="">assigned to me</option>
+      <option value="-1" tal:attributes="selected python:value == '-1'"
+       i18n:translate="">unassigned</option>
+    </tal:block>
+  </td>
+  <td metal:use-macro="column_input"></td>
+  <td metal:use-macro="sort_input"></td>
+  <td metal:use-macro="group_input"></td>
+</tr>
+
+<tr>
+ <th i18n:translate="">No Sort or group:</th>
+ <td>&nbsp;</td>
+ <td>&nbsp;</td>
+ <td><input type="radio" name="@sort" value=""></td>
+ <td><input type="radio" name="@group" value=""></td>
+</tr>
+
+<tr>
+<th i18n:translate="">Pagesize:</th>
+<td><input name="@pagesize" size="3" value="50"
+           tal:attributes="value request/form/@pagesize/value | default"></td>
+</tr>
+
+<tr>
+<th i18n:translate="">Start With:</th>
+<td><input name="@startwith" size="3" value="0"
+           tal:attributes="value request/form/@startwith/value | default"></td>
+</tr>
+
+<tr>
+<th i18n:translate="">Sort Descending:</th>
+<td><input type="checkbox" name="@sortdir"
+           tal:attributes="checked sort_desc">
+</td>
+</tr>
+
+<tr>
+<th i18n:translate="">Group Descending:</th>
+<td><input type="checkbox" name="@groupdir"
+           tal:attributes="checked group_desc">
+</td>
+</tr>
+
+<tr tal:condition="python:request.user.hasPermission('Edit', 'query')">
+ <th i18n:translate="">Query name**:</th>
+ <td tal:define="value request/form/@queryname/value | nothing">
+  <input name="@queryname" tal:attributes="value value">
+  <input type="hidden" name="@old-queryname" tal:attributes="value value">
+ </td>
+</tr>
+
+<tr>
+  <td>
+   &nbsp;
+   <input type="hidden" name="@action" value="search">
+  </td>
+  <td><input type="submit" value="Search" i18n:attributes="value"></td>
+</tr>
+
+<tr><td>&nbsp;</td>
+ <td colspan="4" class="help">
+  <span i18n:translate="" tal:omit-tag="true">
+   *: The "all text" field will look in message bodies and issue titles
+  </span><br>
+  <span tal:condition="python:request.user.hasPermission('Edit', 'query')"
+   i18n:translate="" tal:omit-tag="true"
+  >
+   **: If you supply a name, the query will be saved off and available as a
+       link in the sidebar
+  </span>
+ </td>
+</tr>
+</table>
+
+</form>
+</td>
+
+</tal:block>

Added: tracker/roundup-src/share/roundup/templates/classic/html/keyword.item.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/html/keyword.item.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,55 @@
+<!-- dollarId: keyword.item,v 1.3 2002/05/22 00:32:34 richard Exp dollar-->
+<tal:block metal:use-macro="templates/page/macros/icing">
+<title metal:fill-slot="head_title" i18n:translate="">Keyword editing - <span
+ i18n:name="tracker" tal:replace="config/TRACKER_NAME" /></title>
+<span metal:fill-slot="body_title" tal:omit-tag="python:1"
+ i18n:translate="">Keyword editing</span>
+<td class="content" metal:fill-slot="content">
+
+<table class="otherinfo" tal:define="keywords db/keyword/list"
+       tal:condition="keywords">
+ <tr><th colspan="4" class="header" i18n:translate="">Existing Keywords</th></tr>
+ <tr tal:repeat="start python:range(0, len(keywords), 4)">
+  <td width="25%" tal:define="batch python:utils.Batch(keywords, 4, start)"
+      tal:repeat="keyword batch">
+    <a tal:attributes="href string:keyword${keyword/id}"
+       tal:content="keyword/name">keyword here</a>
+  </td>
+ </tr>
+ <tr>
+  <td colspan="4" style="border-top: 1px solid gray" i18n:translate="">
+   To edit an existing keyword (for spelling or typing errors),
+   click on its entry above.
+  </td>
+ </tr>
+</table>
+
+<p class="help" tal:condition="not:context/id" i18n:translate="">
+ To create a new keyword, enter it below and click "Submit New Entry".
+</p>
+
+<form method="POST" onSubmit="return submit_once()"
+      enctype="multipart/form-data"
+      tal:attributes="action context/designator">
+
+ <table class="form">
+  <tr>
+   <th i18n:translate="">Keyword</th>
+   <td tal:content="structure context/name/field">name</td>
+  </tr>
+
+  <tr>
+   <td>
+    &nbsp;
+    <input type="hidden" name="@required" value="name">
+    <input type="hidden" name="@template" value="item">
+   </td>
+   <td colspan=3 tal:content="structure context/submit">
+    submit button will go here
+   </td>
+  </tr>
+ </table>
+</form>
+</td>
+
+</tal:block>

Added: tracker/roundup-src/share/roundup/templates/classic/html/msg.index.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/html/msg.index.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,25 @@
+<tal:block metal:use-macro="templates/page/macros/icing">
+<title metal:fill-slot="head_title" i18n:translate=""
+ >List of messages - <span tal:replace="config/TRACKER_NAME"
+ i18n:name="tracker"/></title>
+<span metal:fill-slot="body_title" tal:omit-tag="python:1"
+ i18n:translate="">Message listing</span>
+<td class="content" metal:fill-slot="content">
+<table tal:define="batch request/batch" class="messages">
+ <tr><th colspan=2 class="header" i18n:translate="">Messages</th></tr>
+ <tal:block tal:repeat="msg batch">
+  <tr>
+   <th tal:content="string:Author: ${msg/author}">author</th>
+   <th tal:content="string:Date: ${msg/date}">date</th>
+  </tr>
+  <tr>
+   <td colspan="2"><pre tal:content="msg/content">content</pre></td>
+  </tr>
+ </tal:block>
+
+ <metal:block use-macro="templates/issue.index/macros/batch-footer" />
+
+</table>
+</td>
+
+</tal:block>

Added: tracker/roundup-src/share/roundup/templates/classic/html/msg.item.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/html/msg.item.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,83 @@
+<!-- dollarId: msg.item,v 1.3 2002/05/22 00:32:34 richard Exp dollar-->
+<tal:block metal:use-macro="templates/page/macros/icing">
+<title metal:fill-slot="head_title">
+<tal:block condition="context/id" i18n:translate=""
+ >Message <span tal:replace="context/id" i18n:name="id"
+ /> - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker"
+/></tal:block>
+<tal:block condition="not:context/id" i18n:translate=""
+ >New Message - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker"
+/></tal:block>
+</title>
+<tal:block metal:fill-slot="body_title">
+ <span tal:condition="python: not (context.id or context.is_edit_ok())"
+  tal:omit-tag="python:1" i18n:translate="">New Message</span>
+ <span tal:condition="python: not context.id and context.is_edit_ok()"
+  tal:omit-tag="python:1" i18n:translate="">New Message Editing</span>
+ <span tal:condition="python: context.id and not context.is_edit_ok()"
+  tal:omit-tag="python:1" i18n:translate="">Message<tal:x
+  replace="context/id" i18n:name="id" /></span>
+ <span tal:condition="python: context.id and context.is_edit_ok()"
+  tal:omit-tag="python:1" i18n:translate="">Message<tal:x
+  replace="context/id" i18n:name="id" /> Editing</span>
+</tal:block>
+<td class="content" metal:fill-slot="content">
+
+<p tal:condition="python:not (context.is_view_ok()
+ or request.user.hasRole('Anonymous'))" i18n:translate="">
+ You are not allowed to view this page.</p>
+
+<p tal:condition="python:not context.is_view_ok()
+ and request.user.hasRole('Anonymous')" i18n:translate="">
+ Please login with your username and password.</p>
+
+<div tal:condition="context/is_view_ok">
+<table class="form">
+
+<tr>
+ <th i18n:translate="">Author</th>
+ <td tal:content="context/author"></td>
+</tr>
+
+<tr>
+ <th i18n:translate="">Recipients</th>
+ <td tal:content="context/recipients"></td>
+</tr>
+
+<tr>
+ <th i18n:translate="">Date</th>
+ <td tal:content="context/date"></td>
+</tr>
+</table>
+
+<table class="messages">
+ <tr><th colspan=2 class="header" i18n:translate="">Content</th></tr>
+ <tr>
+  <td class="content" colspan=2><pre tal:content="structure context/content/hyperlinked"></pre></td>
+ </tr>
+</table>
+
+<table class="files" tal:condition="context/files">
+ <tr><th colspan="2" class="header" i18n:translate="">Files</th></tr>
+ <tr>
+  <th i18n:translate="">File name</th>
+  <th i18n:translate="">Uploaded</th>
+ </tr>
+ <tr tal:repeat="file context/files">
+  <td>
+   <a tal:attributes="href string:file${file/id}/${file/name}"
+      tal:content="file/name">dld link</a>
+  </td>
+  <td>
+   <span tal:content="file/creator">creator's name</span>,
+   <span tal:content="file/creation">creation date</span>
+  </td>
+ </tr>
+</table>
+
+<tal:block tal:replace="structure context/history" />
+
+</div>
+</td>
+
+</tal:block>

Added: tracker/roundup-src/share/roundup/templates/classic/html/page.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/html/page.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,347 @@
+<!-- vim:sw=2 sts=2
+--><tal:block metal:define-macro="icing"
+><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+<head>
+<title metal:define-slot="head_title">title goes here</title>
+<link rel="stylesheet" type="text/css" href="@@file/style.css">
+<meta http-equiv="Content-Type"
+ tal:attributes="content string:text/html;; charset=${request/client/charset}" />
+<script tal:replace="structure request/base_javascript">
+</script>
+<metal:x define-slot="more-javascript" />
+
+</head>
+<body class="body">
+
+<table class="body"
+ tal:define="
+kw_edit python:request.user.hasPermission('Edit', 'keyword');
+kw_create python:request.user.hasPermission('Create', 'keyword');
+kw_edit_link python:kw_edit and db.keyword.list();
+columns string:id,activity,title,creator,status;
+columns_showall string:id,activity,title,creator,assignedto,status;
+status_notresolved string:-1,1,2,3,4,5,6,7;
+"
+>
+
+<tr>
+ <td class="page-header-left">&nbsp;</td>
+ <td class="page-header-top">
+   <div id="body-title">
+     <h2><span metal:define-slot="body_title">body title</span></h2>
+   </div>
+   <div id="searchbox">
+     <form method="GET" action="issue">
+       <input type="hidden" name="@columns"
+	      tal:attributes="value columns_showall"
+	      value="id,activity,title,creator,assignedto,status"/>
+       <input type="hidden" name="@sort" value="activity"/>
+       <input type="hidden" name="@group" value="priority"/>
+       <input id="search-text" name="@search_text" size="10"
+              tal:attributes="value request/search_text | default" />
+       <input type="submit" id="submit" name="submit" value="Search"
+              i18n:attributes="value" />
+     </form>
+  </div>
+ </td>
+</tr>
+
+<tr>
+ <td rowspan="2" valign="top" class="sidebar">
+  <p class="classblock"
+     tal:condition="python:request.user.hasPermission('View', 'query')">
+   <span i18n:translate=""
+    ><b>Your Queries</b> (<a href="query?@template=edit">edit</a>)</span><br>
+   <tal:block tal:repeat="qs request/user/queries">
+    <a href="#" tal:attributes="href string:${qs/klass}?${qs/url}&@dispname=${qs/name}"
+       tal:content="qs/name">link</a><br>
+   </tal:block>
+  </p>
+
+  <form method="POST" tal:attributes="action request/base">
+   <p class="classblock"
+       tal:condition="python:request.user.hasPermission('View', 'issue')">
+    <b i18n:translate="">Issues</b><br>
+    <span tal:condition="python:request.user.hasPermission('Create', 'issue')">
+      <a href="issue?@template=item" i18n:translate="">Create New</a><br>
+    </span>
+    <a href="#"
+       tal:attributes="href python:request.indexargs_url('issue', {
+      '@sort': '-activity',
+      '@group': 'priority',
+      '@filter': 'status,assignedto',
+      '@columns': columns,
+      '@search_text': '',
+      'status': status_notresolved,
+      'assignedto': '-1',
+      '@dispname': i18n.gettext('Show Unassigned'),
+     })"
+       i18n:translate="">Show Unassigned</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>
+    <a href="issue?@template=search" i18n:translate="">Search</a><br>
+    <input type="submit" class="form-small" value="Show issue:"
+     i18n:attributes="value"><input class="form-small" size="4"
+     type="text" name="@number">
+    <input type="hidden" name="@type" value="issue">
+    <input type="hidden" name="@action" value="show">
+   </p>
+  </form>
+
+  <p class="classblock"
+     tal:condition="python:kw_edit or kw_create">
+   <b i18n:translate="">Keywords</b><br>
+   <span tal:condition="python:request.user.hasPermission('Create', 'keyword')">
+    <a href="keyword?@template=item" i18n:translate="">Create New</a><br>
+   </span>
+   <span tal:condition="kw_edit_link">
+    <a href="keyword?@template=item" i18n:translate="">Edit Existing</a><br>
+   </span>
+  </p>
+
+  <p class="classblock"
+       tal:condition="python:request.user.hasPermission('View', 'user')">
+   <b i18n:translate="">Administration</b><br>
+   <span tal:condition="python:request.user.hasPermission('Edit', None)">
+    <a href="home?@template=classlist" i18n:translate="">Class List</a><br>
+   </span>
+   <span tal:condition="python:request.user.hasPermission('View', 'user')
+                            or request.user.hasPermission('Edit', 'user')">
+    <a href="user"  i18n:translate="">User List</a><br>
+   </span>
+   <a tal:condition="python:request.user.hasPermission('Create', 'user')"
+      href="user?@template=item" i18n:translate="">Add User</a>
+  </p>
+
+  <form method="POST" tal:condition="python:request.user.username=='anonymous'"
+        tal:attributes="action request/base">
+   <p class="userblock">
+    <b i18n:translate="">Login</b><br>
+    <input size="10" name="__login_name"><br>
+    <input size="10" type="password" name="__login_password"><br>
+    <input type="hidden" name="@action" value="Login">
+    <input type="checkbox" name="remember" id="remember">
+    <label for="remember" i18n:translate="">Remember me?</label><br>
+    <input type="submit" value="Login" i18n:attributes="value"><br>
+    <input type="hidden" name="__came_from" tal:attributes="value string:${request/base}${request/env/PATH_INFO}">
+    <span tal:replace="structure request/indexargs_form" />
+    <a href="user?@template=register"
+       tal:condition="python:request.user.hasPermission('Create', 'user')"
+     i18n:translate="">Register</a><br>
+    <a href="user?@template=forgotten" i18n:translate="">Lost&nbsp;your&nbsp;login?</a><br>
+   </p>
+  </form>
+
+  <p class="userblock" tal:condition="python:request.user.username != 'anonymous'">
+   <b i18n:translate="">Hello, <span i18n:name="user"
+    tal:replace="python:request.user.username.plain(escape=1)">username</span></b><br>
+    <a href="#"
+       tal:attributes="href python:request.indexargs_url('issue', {
+      '@sort': '-activity',
+      '@group': 'priority',
+      '@filter': 'status,assignedto',
+      '@columns': 'id,activity,title,creator,status',
+      '@search_text': '',
+      'status': status_notresolved,
+      'assignedto': request.user.id,
+      '@dispname': i18n.gettext('Your Issues'),
+     })"
+    i18n:translate="">Your Issues</a><br>
+   <a href="#" tal:attributes="href string:user${request/user/id}"
+    i18n:translate="">Your Details</a><br>
+   <a href="#" tal:attributes="href python:request.indexargs_url('',
+       {'@action':'logout'})" i18n:translate="">Logout</a>
+  </p>
+  <p class="userblock">
+   <b i18n:translate="">Help</b><br>
+   <a href="http://roundup.sourceforge.net/doc-1.0/"
+    i18n:translate="">Roundup docs</a>
+  </p>
+ </td>
+ <td>
+  <p tal:condition="options/error_message | nothing" class="error-message"
+     tal:repeat="m options/error_message" tal:content="structure m" />
+  <p tal:condition="options/ok_message | nothing" class="ok-message">
+    <span tal:repeat="m options/ok_message"
+       tal:content="structure string:$m <br/ > " />
+     <a class="form-small" tal:attributes="href request/current_url"
+        i18n:translate="">clear this message</a>
+  </p>
+ </td>
+</tr>
+<tr>
+ <td class="content" metal:define-slot="content">Page content goes here</td>
+</tr>
+
+</table>
+
+<pre tal:condition="request/form/debug | nothing" tal:content="request">
+</pre>
+
+</body>
+</html>
+</tal:block>
+
+<!--
+The following macros are intended to be used in search pages.
+
+The invoking context must define a "name" variable which names the
+property being searched.
+
+See issue.search.html in the classic template for examples.
+-->
+
+<!-- creates a th and a label: -->
+<th metal:define-macro="th_label"
+    tal:define="required required | python:[]"
+    tal:attributes="class python:(name in required) and 'required' or nothing">
+  <label tal:attributes="for name" tal:content="label" i18n:translate="">text</label>
+	<metal:x define-slot="behind_the_label" />
+</th>
+
+<td metal:define-macro="search_input">
+  <input tal:attributes="value python:request.form.getvalue(name) or nothing;
+                         name name;
+                         id name">
+</td>
+
+<td metal:define-macro="search_date">
+  <input tal:attributes="value python:request.form.getvalue(name) or nothing;
+                         name name;
+                         id name">
+  <a class="classhelp"
+	 tal:attributes="href python:'''javascript:help_window('issue?@template=calendar&property=%s&form=itemSynopsis', 300, 200)'''%name">(cal)</a>
+</td>
+
+<td metal:define-macro="search_popup">
+  <!--
+    context needs to specify the popup "columns" as a comma-separated
+    string (eg. "id,title" or "id,name,description") as well as name
+  -->
+  <input tal:attributes="value python:request.form.getvalue(name) or nothing;
+                         name name;
+                         id name">
+  <span tal:replace="structure python:db.issue.classhelp(columns,
+                                      property=name)" />
+</td>
+
+<td metal:define-macro="search_select">
+  <select tal:attributes="name name; id name"
+          tal:define="value python:request.form.getvalue(name)">
+    <option value="" i18n:translate="">don't care</option>
+    <metal:slot define-slot="extra_options" />
+    <option value="" i18n:translate="" disabled="disabled">------------</option>
+    <option tal:repeat="s python:db[db_klass].list()"
+            tal:attributes="value s/id; selected python:value == s.id"
+            tal:content="python:s[db_content]"></option>
+  </select>
+</td>
+
+<!-- like search_select, but translates the further values.
+Could extend it (METAL 1.1 attribute "extend-macro")
+-->
+<td metal:define-macro="search_select_translated">
+  <select tal:attributes="name name; id name"
+          tal:define="value python:request.form.getvalue(name)">
+    <option value="" i18n:translate="">don't care</option>
+    <metal:slot define-slot="extra_options" />
+    <option value="" i18n:translate="" disabled="disabled">------------</option>
+    <option tal:repeat="s python:db[db_klass].list()"
+            tal:attributes="value s/id; selected python:value == s.id"
+						tal:content="python:s[db_content]"
+						i18n:translate=""></option>
+  </select>
+</td>
+
+<!-- currently, there is no convenient API to get a list of all roles -->
+<td metal:define-macro="search_select_roles"
+	  tal:define="onchange onchange | nothing">
+  <select name=roles id=roles tal:attributes="onchange onchange">
+    <option value="" i18n:translate="">don't care</option>
+    <option value="" i18n:translate="" disabled="disabled">------------</option>
+    <option value="User">User</option>
+    <option value="Admin">Admin</option>
+    <option value="Anonymous">Anonymous</option>
+  </select>
+</td>
+
+<td metal:define-macro="search_multiselect">
+  <input tal:attributes="value python:request.form.getvalue(name) or nothing;
+                         name name;
+                         id name">
+  <span tal:replace="structure python:db[db_klass].classhelp(db_content,
+                                        property=name, width='600')" />
+</td>
+
+<td metal:define-macro="search_checkboxes">
+ <ul class="search-checkboxes"
+     tal:define="value python:request.form.getvalue(name);
+                 values python:value and value.split(',') or []">
+ <li tal:repeat="s python:db[db_klass].list()">
+  <input type="checkbox" tal:attributes="name name; id string:$name-${s/id};
+    value s/id; checked python:s.id in values" />
+  <label tal:attributes="for string:$name-${s/id}"
+         tal:content="python:s[db_content]" />
+ </li>
+ <li metal:define-slot="no_value_item">
+  <input type="checkbox" value="-1" tal:attributes="name name;
+     id string:$name--1; checked python:value == '-1'" />
+  <label tal:attributes="for string:$name--1" i18n:translate="">no value</label>
+ </li>
+ </ul>
+</td>
+
+<td metal:define-macro="column_input">
+  <input type="checkbox" name="@columns"
+         tal:attributes="value name;
+                         checked python:name in cols">
+</td>
+
+<td metal:define-macro="sort_input">
+  <input type="radio" name="@sort"
+         tal:attributes="value name;
+                         checked python:name == sort_on">
+</td>
+
+<td metal:define-macro="group_input">
+  <input type="radio" name="@group"
+         tal:attributes="value name;
+                         checked python:name == group_on">
+</td>
+
+<!--
+The following macros are intended for user editing.
+
+The invoking context must define a "name" variable which names the
+property being searched; the "edit_ok" variable tells whether the
+current user is allowed to edit.
+
+See user.item.html in the classic template for examples.
+-->
+<script metal:define-macro="user_utils" type="text/javascript" src="@@file/user_utils.js"></script>
+
+<!-- src: value will be re-used for other input fields -->
+<input metal:define-macro="user_src_input"
+    type="text" tal:attributes="onblur python:edit_ok and 'split_name(this)';
+    id name; name name; value value; readonly not:edit_ok"
+    value="heinz.kunz">
+<!-- normal: no re-using -->
+<input metal:define-macro="user_normal_input" type="text"
+    tal:attributes="id name; name name; value value; readonly not:edit_ok"
+    value="heinz">
+<!-- password: type; no initial value -->
+    <input metal:define-macro="user_pw_input" type="password"
+    tal:attributes="id name; name name; readonly not:edit_ok" value="">
+    <input metal:define-macro="user_confirm_input" type="password"
+    tal:attributes="id name; name string:@confirm@$name; readonly not:edit_ok" value="">
+

Added: tracker/roundup-src/share/roundup/templates/classic/html/query.edit.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/html/query.edit.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,111 @@
+<!-- dollarId: user.item,v 1.7 2002/08/16 04:29:04 richard Exp dollar-->
+<tal:block metal:use-macro="templates/page/macros/icing">
+<title metal:fill-slot="head_title" i18n:translate=""
+ >"Your Queries" Editing - <span tal:replace="config/TRACKER_NAME"
+ i18n:name="tracker" /></title>
+<span metal:fill-slot="body_title" tal:omit-tag="python:1"
+ i18n:translate="">"Your Queries" Editing</span>
+
+<td class="content" metal:fill-slot="content">
+
+<span tal:condition="not:context/is_edit_ok"
+ i18n:translate="">You are not allowed to edit queries.</span>
+
+<script language="javascript">
+// This exists solely because I can't figure how to get the & into an
+// attributes TALES expression, and so it keeps getting quoted.
+function retire(qid) {
+    window.location = 'query'+qid+'?@action=retire&@template=edit';
+}
+</script>
+
+<form method="POST" onSubmit="return submit_once()" action="query"
+      enctype="multipart/form-data" tal:condition="context/is_edit_ok">
+
+<table class="list" width="100%"
+       tal:define="uid request/user/id; mine request/user/queries">
+
+<tr><th i18n:translate="">Query</th>
+    <th i18n:translate="">Include in "Your Queries"</th>
+    <th i18n:translate="">Edit</th>
+    <th i18n:translate="">Private to you?</th>
+    <th>&nbsp;</th>
+</tr>
+
+<tr tal:repeat="query mine">
+ <tal:block condition="query/is_retired">
+
+ <td><a tal:attributes="href string:${query/klass}?${query/url}"
+        tal:content="query/name">query</a></td>
+
+ <td metal:define-macro="include">
+  <select tal:condition="python:query.id not in mine"
+          tal:attributes="name string:user${uid}@add at queries">
+    <option value="" i18n:translate="">leave out</option>
+    <option tal:attributes="value query/id" i18n:translate="">include</option>
+  </select>
+  <select tal:condition="python:query.id in mine"
+          tal:attributes="name string:user${uid}@remove at queries">
+    <option value="" i18n:translate="">leave in</option>
+    <option tal:attributes="value query/id" i18n:translate="">remove</option>
+  </select>
+ </td>
+
+ <td colspan="3" i18n:translate="">[query is retired]</td>
+
+ <!-- <td> maybe offer "restore" some day </td> -->
+ </tal:block>
+</tr>
+
+<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>
+
+ <td metal:use-macro="template/macros/include" />
+
+ <td><a tal:attributes="href string:query${query/id}" i18n:translate="">edit</a></td>
+
+ <td>
+  <select tal:attributes="name string:query${query/id}@private_for">
+   <option tal:attributes="selected python:query.private_for == uid;
+           value uid" i18n:translate="">yes</option>
+   <option tal:attributes="selected python:query.private_for == None"
+           value="-1" i18n:translate="">no</option>
+  </select>
+ </td>
+
+ <td>
+  <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>
+
+ <td metal:use-macro="template/macros/include" />
+
+ <td colspan="3" tal:condition="query/is_edit_ok">
+  <a tal:attributes="href string:query${query/id}" i18n:translate="">edit</a>
+ </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">
+   <input type="hidden" name="@action" value="edit">
+   <input type="hidden" name="@template" value="edit">
+   <input type="submit" value="Save Selection" i18n:attributes="value">
+</td></tr>
+
+</table>
+
+</form>
+</td>
+</tal:block>

Added: tracker/roundup-src/share/roundup/templates/classic/html/query.item.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/html/query.item.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,3 @@
+<!-- query.item -->
+<span tal:replace="structure context/renderQueryForm" />
+

Added: tracker/roundup-src/share/roundup/templates/classic/html/style.css
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/html/style.css	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,433 @@
+/* main page styles */
+body.body {
+  font-family: sans-serif, Arial, Helvetica;
+  background-color: white;
+  color: #333;
+  margin: 0;
+}
+a[href]:hover {
+  color:blue;
+  text-decoration: underline;
+}
+a[href], a[href]:link {
+  color:blue;
+  text-decoration: none;
+}
+
+table.body {
+  border: 0;
+  padding: 0;
+  border-spacing: 0;
+  border-collapse: separate;
+}
+
+td.page-header-left {
+  padding: 5px;
+  border-bottom: 1px solid #444;
+}
+td.sidebar {
+  padding: 1px 0 0 1px;
+  white-space: nowrap;
+}
+
+/* don't display the sidebar when printing */
+ at media print {
+    td.page-header-left {
+        display: none;
+    }
+    td.sidebar {
+        display: none;
+    }
+    .index-controls {
+        display: none;
+    }
+    #searchbox {
+        display: none;
+    }
+}
+
+td.page-header-top {
+  padding: 5px;
+  border-bottom: 1px solid #444;
+}
+#searchbox {
+    float: right;
+}
+
+div#body-title {
+  float: left;
+}
+
+
+div#searchbox {
+  float: right;
+  padding-top: 1em;
+}
+
+div#searchbox input#search-text {
+  width: 10em;
+}
+
+form {
+  margin: 0;
+}
+
+textarea {
+    font-family: monospace;
+}
+
+td.sidebar p.classblock {
+  padding: 2px 5px 2px 5px;
+  margin: 1px;
+  border: 1px solid #444;
+  background-color: #eee;
+}
+
+td.sidebar p.userblock {
+  padding: 2px 5px 2px 5px;
+  margin: 1px 1px 1px 1px;
+  border: 1px solid #444;
+  background-color: #eef;
+}
+
+.form-small {
+  padding: 0;
+  font-size: 75%;
+}
+
+
+td.content {
+  padding: 1px 5px 1px 5px;
+  vertical-align: top;
+  width: 100%;
+}
+
+td.date, th.date { 
+  white-space: nowrap;
+}
+
+p.ok-message {
+  background-color: #22bb22;
+  padding: 5px;
+  color: white;
+  font-weight: bold;
+}
+p.error-message {
+  background-color: #bb2222;
+  padding: 5px;
+  color: white;
+  font-weight: bold;
+}
+p.error-message a[href] {
+  color: white;
+  text-decoration: underline;
+}
+
+
+/* style for search forms */
+ul.search-checkboxes {
+    display: inline;
+    padding: 0;
+    list-style: none;
+}
+ul.search-checkboxes > li {
+    display: inline;
+    padding-right: .5em;
+}
+
+
+/* style for forms */
+table.form {
+  padding: 2px;
+  border-spacing: 0;
+  border-collapse: separate;
+}
+
+table.form th {
+  color: #338;
+  text-align: right;
+  vertical-align: top;
+  font-weight: normal;
+  white-space: nowrap;
+}
+
+table.form th.header {
+  font-weight: bold;
+  background-color: #eef;
+  text-align: left;
+}
+
+table.form th.required {
+  font-weight: bold;
+}
+
+table.form td {
+  color: #333;
+  empty-cells: show;
+  vertical-align: top;
+}
+
+table.form td.optional {
+  font-weight: bold;
+  font-style: italic;
+}
+
+table.form td.html {
+  color: #777;
+}
+
+/* style for lists */
+table.list {
+  border-spacing: 0;
+  border-collapse: separate;
+  width: 100%;
+}
+
+table.list th {
+  padding: 0 4px 0 4px;
+  color: #404070;
+  background-color: #eef;
+  border: 1px solid white;
+  vertical-align: top;
+  empty-cells: show;
+}
+table.list th a[href]:hover { color: #404070 }
+table.list th a[href]:link { color: #404070 }
+table.list th a[href] { color: #404070 }
+table.list th.group {
+  background-color: #f4f4ff;
+  text-align: center;
+}
+
+table.list td {
+  padding: 0 4px 0 4px;
+  border: 1px solid white;
+  color: #404070;
+  background-color: #efefef;
+  vertical-align: top;
+  empty-cells: show;
+}
+
+table.list tr.navigation th {
+  width: 33%;
+  border-style: hidden;
+  text-align: center;
+}
+table.list tr.navigation td {
+    border: none
+}
+table.list tr.navigation th:first-child {
+  text-align: left;
+}
+table.list tr.navigation th:last-child {
+  text-align: right;
+}
+
+
+/* style for message displays */
+table.messages {
+  border-spacing: 0;
+  border-collapse: separate;
+  width: 100%;
+}
+
+table.messages th.header{
+  padding-top: 10px;
+  border-bottom: 1px solid gray;
+  font-weight: bold;
+  background-color: white;
+  color: #707040;
+}
+
+table.messages th {
+  font-weight: bold;
+  color: black;
+  text-align: left;
+  border-bottom: 1px solid #afafaf;
+}
+
+table.messages td {
+  font-family: monospace;
+  background-color: #efefef;
+  border-bottom: 1px solid #afafaf;
+  color: black;
+  empty-cells: show;
+  border-right: 1px solid #afafaf;
+  vertical-align: top;
+  padding: 2px 5px 2px 5px;
+}
+
+table.messages td:first-child {
+  border-left: 1px solid #afafaf;
+  border-right: 1px solid #afafaf;
+}
+
+/* style for file displays */
+table.files {
+  border-spacing: 0;
+  border-collapse: separate;
+  width: 100%;
+}
+
+table.files th.header{
+  padding-top: 10px;
+  border-bottom: 1px solid gray;
+  font-weight: bold;
+  background-color: white;
+  color: #707040;
+}
+
+table.files th {
+  border-bottom: 1px solid #afafaf;
+  font-weight: bold;
+  text-align: left;
+}
+
+table.files td {
+  font-family: monospace;
+  empty-cells: show;
+}
+
+/* style for history displays */
+table.history {
+  border-spacing: 0;
+  border-collapse: separate;
+  width: 100%;
+}
+
+table.history th.header{
+  padding-top: 10px;
+  border-bottom: 1px solid gray;
+  font-weight: bold;
+  background-color: white;
+  color: #707040;
+  font-size: 100%;
+}
+
+table.history th {
+  border-bottom: 1px solid #afafaf;
+  font-weight: bold;
+  text-align: left;
+  font-size: 90%;
+}
+
+table.history td {
+  font-size: 90%;
+  vertical-align: top;
+  empty-cells: show;
+}
+
+
+/* style for class list */
+table.classlist {
+  border-spacing: 0;
+  border-collapse: separate;
+  width: 100%;
+}
+
+table.classlist th.header{
+  padding-top: 10px;
+  border-bottom: 1px solid gray;
+  font-weight: bold;
+  background-color: white;
+  color: #707040;
+}
+
+table.classlist th {
+  font-weight: bold;
+  text-align: left;
+}
+
+
+/* style for class help display */
+table.classhelp {      /* the table-layout: fixed;        */ 
+  table-layout: fixed; /* compromises quality for speed   */
+  overflow: hidden;
+  font-size: .9em;
+  padding-bottom: 3em;
+}
+
+table.classhelp th {
+  font-weight: normal;
+  text-align: left;
+  color: #444;
+  background-color: #efefef;
+  border-bottom: 1px solid #afafaf;
+  border-top: 1px solid #afafaf;
+  text-transform: uppercase;
+  vertical-align: middle;
+  line-height:1.5em;
+}
+
+table.classhelp td {
+  vertical-align: middle;
+  padding-right: .2em;
+  border-bottom: 1px solid #efefef;
+  text-align: left;
+  empty-cells: show;
+  white-space: nowrap;
+  vertical-align: middle;
+}
+
+table.classhelp tr:hover {
+  background-color: #eee;
+}
+
+label.classhelp-label {
+  cursor: pointer;
+}
+
+#classhelp-controls {
+  position: fixed;
+  display: block;
+  top: auto;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  padding: .5em;
+  border-top: 2px solid #444;
+  background-color: #eee;
+}
+
+#classhelp-controls input.apply {
+  width: 7em;
+  font-weight: bold;
+  margin-right: 2em;
+  margin-left: 2em;
+}
+
+#classhelp-controls input.preview {
+   margin-right: 3em;
+   margin-left: 1em;
+}
+
+/* style for "other" displays */
+table.otherinfo {
+  border-spacing: 0;
+  border-collapse: separate;
+  width: 100%;
+}
+
+table.otherinfo th.header{
+  padding-top: 10px;
+  border-bottom: 1px solid gray;
+  font-weight: bold;
+  background-color: white;
+  color: #707040;
+}
+
+table.otherinfo th {
+  border-bottom: 1px solid #afafaf;
+  font-weight: bold;
+  text-align: left;
+}
+input[type="text"]:focus,
+input[type="checkbox"]:focus,
+input[type="radio"]:focus,
+input[type="password"]:focus,
+textarea:focus, select:focus {
+  background-color: #ffffc0;
+}
+
+/* vim: sts=2 sw=2 et
+*/

Added: tracker/roundup-src/share/roundup/templates/classic/html/user.forgotten.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/html/user.forgotten.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,43 @@
+<!-- dollarId: user.item,v 1.7 2002/08/16 04:29:04 richard Exp dollar-->
+<tal:block metal:use-macro="templates/page/macros/icing">
+<title metal:fill-slot="head_title" i18n:translate="">Password reset request - <span
+ i18n:name="tracker" tal:replace="config/TRACKER_NAME" /></title>
+<span metal:fill-slot="body_title" tal:omit-tag="python:1"
+ i18n:translate="">Password reset request</span>
+<td class="content" metal:fill-slot="content">
+
+<p i18n:translate="">You have two options if you have forgotten your password.
+If you know the email address you registered with, enter it below.</p>
+
+<form method="POST" onSubmit="return submit_once()"
+      tal:attributes="action context/designator">
+    <table class="form">
+      <tr>
+        <th i18n:translate="">Email Address:</th>
+        <td><input name="address"></td>
+      </tr>
+      <tr>
+        <td>&nbsp;</td>
+        <td>
+          <input type="hidden" name="@action" value="passrst">
+          <input type="hidden" name="@template" value="forgotten">
+          <input type="submit" value="Request password reset"
+           i18n:attributes="value">
+        </td>
+      </tr>
+</table>
+
+<p i18n:translate="">Or, if you know your username, then enter it below.</p>
+
+<table class="form">
+ <tr><th i18n:translate="">Username:</th> <td><input name="username"></td> </tr>
+ <tr><td></td><td><input type="submit" value="Request password reset"
+   i18n:attributes="value"></td></tr>
+</table>
+</form>
+
+<p i18n:translate="">A confirmation email will be sent to you -
+please follow the instructions within it to complete the reset process.</p>
+</td>
+
+</tal:block>

Added: tracker/roundup-src/share/roundup/templates/classic/html/user.help-search.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/html/user.help-search.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,85 @@
+<html
+  tal:define="form request/form/form/value;
+  field request/form/property/value"
+  >
+  <head>
+    <title>Search input for user helper</title>
+    <script language="Javascript" type="text/javascript"
+        tal:content="structure string:<!--
+        // this is the name of the field in the original form that we're working on
+        form  = parent.opener.document.${form};
+        field  = '${field}';
+        //-->">
+    </script>
+    <script type="text/javascript" src="@@file/help_controls.js"></script>
+    <link rel="stylesheet" type="text/css" href="@@file/style.css" />
+  </head>
+  <body onload="parent.submit.url='...'"
+    tal:define="
+qs request/env/QUERY_STRING;
+qs python:'&'.join([a for a in qs.split('&') if not a.startswith('@template=')])"
+>
+    <pre tal:content="request/env/QUERY_STRING" tal:condition=false />
+    <form method="GET" name="itemSynopsis"
+      target="list"
+      tal:attributes="action request/classname"
+      tal:define="
+      property request/form/property/value;
+   cols python:request.columns or 'id username address realname roles'.split();
+   sort_on request/sort | nothing;
+   sort_desc python:sort_on and request.sort[0][0] == '-';
+   sort_on python:sort_on and request.sort[0][1] or 'lastname';
+
+   search_input templates/page/macros/search_input;
+   search_select templates/page/macros/search_select;
+   search_select_roles templates/page/macros/search_select_roles;
+   required python:[];
+   th_label templates/page/macros/th_label;
+   ">
+   <input type="hidden" name="@template" value="help-list">
+   <input type="hidden" name="property" value="" tal:attributes="value property">
+   <input type="hidden" name="form" value="" tal:attributes="value request/form/form/value">
+   <table>
+<tr tal:define="name string:username; label string:Username:">
+  <th metal:use-macro="th_label">Name</th>
+  <td metal:use-macro="search_input"><input type=text></td>
+</tr>
+
+<tr tal:define="name string:phone; label string:Phone number">
+  <th metal:use-macro="th_label">Phone</th>
+  <td metal:use-macro="search_input"><input type=text></td>
+</tr>
+
+<tr tal:define="name string:roles;
+                onchange string:this.form.submit();
+                label string:Roles:"
+                >
+  <th metal:use-macro="th_label">role</th>
+  <td metal:use-macro="search_select_roles">
+    <select>
+      <option value="">jokester</option>
+    </select>
+  </td>
+</tr>
+
+<tr>
+  <td>&nbsp;</td>
+  <td>
+    <input type="hidden" name="@action" value="search">
+    <input type="submit" value="Search" i18n:attributes="value">
+    <input type="reset">
+    <input type="hidden" value="username,realname,phone,organisation,roles" name="properties">
+    <input type="text" name="@pagesize" id="sp-pagesize" value="25" size="2">
+    <label for="sp-pagesize" i18n:translate="">Pagesize</label>
+  </td>
+</tr>
+
+   </table>
+
+</form>
+<pre tal:content="request" tal:condition=false />
+<script type="text/javascript"><!--
+  focus2id('username');
+//--></script>
+  </body>
+</html>

Added: tracker/roundup-src/share/roundup/templates/classic/html/user.help.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/html/user.help.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,49 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html tal:define="property request/form/property/value;
+qs request/env/QUERY_STRING;
+qs python:'&'.join([a for a in qs.split('&') if not a.startswith('@template=')]);
+form request/form/form/value;
+field request/form/property/value">
+  <head>
+      <link rel="stylesheet" type="text/css" href="@@file/style.css" />
+      <meta http-equiv="Content-Type"
+       tal:attributes="content string:text/html;; charset=${request/client/charset}" />
+      <tal:block tal:condition="python:request.form.has_key('property')">
+      <title><tal:x i18n:translate=""><tal:x i18n:name="property"
+       tal:content="property" i18n:translate="" /> help - <span i18n:name="tracker"
+	       tal:replace="config/TRACKER_NAME" /></tal:x></title>
+      <script language="Javascript" type="text/javascript"
+	      tal:condition=false
+          tal:content="structure string:<!--
+          // this is the name of the field in the original form that we're working on
+          form  = window.opener.document.${form};
+          field  = '${field}';
+          //-->">
+      </script>
+      <script src="@@file/help_controls.js"
+     tal:condition=false type="text/javascript"><!--
+      //--></script>
+      </tal:block>
+  </head>
+<frameset rows="123,*,62">
+  <frame src="#" tal:attributes="src string:?@template=help-search&${qs}" name="search">
+  <!-- for search results: help-list -->
+  <frame
+  tal:attributes="src string:?@template=help-empty&${qs}"
+  name="list">
+  <frame
+  tal:attributes="src string:?@template=help-submit&${qs}"
+  name="submit">
+  <!-- -->
+</frameset>
+<noframes>
+  <body>
+<p i18n:translate="">
+Your browser is not capable of using frames; you should be redirected immediately,
+or visit <a href="#" tal:attributes="href string:?${qs}&template=help-noframes"
+i18n:name="link">this link</a>.
+</p>
+</body>
+</noframes>
+
+</html>

Added: tracker/roundup-src/share/roundup/templates/classic/html/user.index.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/html/user.index.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,49 @@
+<!-- dollarId: user.index,v 1.3 2002/07/09 05:29:51 richard Exp dollar-->
+<tal:block metal:use-macro="templates/page/macros/icing">
+<title metal:fill-slot="head_title" i18n:translate="">User listing - <span
+ i18n:name="tracker" tal:replace="config/TRACKER_NAME" /></title>
+<span metal:fill-slot="body_title" tal:omit-tag="python:1"
+ i18n:translate="">User listing</span>
+<td class="content" metal:fill-slot="content">
+
+<span tal:condition="python:not (context.is_view_ok()
+ or request.user.hasRole('Anonymous'))"
+ i18n:translate="">You are not allowed to view this page.</span>
+
+<span tal:condition="python:not context.is_view_ok()
+ and request.user.hasRole('Anonymous')"
+ i18n:translate="">Please login with your username and password.</span>
+
+<table width="100%" tal:condition="context/is_view_ok" class="list">
+<tr>
+ <th i18n:translate="">Username</th>
+ <th i18n:translate="">Real name</th>
+ <th i18n:translate="">Organisation</th>
+ <th i18n:translate="">Email address</th>
+ <th i18n:translate="">Phone number</th>
+ <th tal:condition="context/is_edit_ok" i18n:translate="">Retire</th>
+</tr>
+<tal:block repeat="user context/list">
+<tr tal:attributes="class python:['normal', 'alt'][repeat['user'].index%6/3]">
+ <td>
+  <a tal:attributes="href string:user${user/id}"
+     tal:content="user/username">username</a>
+ </td>
+ <td tal:content="python:user.realname.plain() or default">&nbsp;</td>
+ <td tal:content="python:user.organisation.plain() or default">&nbsp;</td>
+ <td tal:content="python:user.address.email() or default">&nbsp;</td>
+ <td tal:content="python:user.phone.plain() or default">&nbsp;</td>
+ <td tal:condition="context/is_retire_ok">
+    <form style="padding:0"
+          tal:attributes="action string:user${user/id}">
+     <input type="hidden" name="@template" value="index">
+     <input type="hidden" name="@action" value="retire">
+     <input type="submit" value="retire" i18n:attributes="value">
+    </form>
+ </td>
+</tr>
+</tal:block>
+</table>
+</td>
+
+</tal:block>

Added: tracker/roundup-src/share/roundup/templates/classic/html/user.item.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/html/user.item.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,169 @@
+<!-- dollarId: user.item,v 1.7 2002/08/16 04:29:04 richard Exp dollar-->
+<tal:doc metal:use-macro="templates/page/macros/icing"
+define="edit_ok context/is_edit_ok"
+>
+<title metal:fill-slot="head_title">
+<tal:if condition="context/id" i18n:translate=""
+ >User <tal:x content="context/id" i18n:name="id"
+ />: <tal:x content="context/username" i18n:name="title"
+ /> - <tal:x content="config/TRACKER_NAME" i18n:name="tracker"
+/></tal:if>
+<tal:if condition="not:context/id" i18n:translate=""
+ >New User - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker"
+/></tal:if>
+</title>
+<metal:slot fill-slot="more-javascript">
+<script metal:use-macro="templates/page/macros/user_utils"></script>
+<script type="text/javascript" src="@@file/help_controls.js"></script>
+</metal:slot>
+<tal:block metal:fill-slot="body_title"
+  define="edit_ok context/is_edit_ok">
+ <span tal:condition="python: not (context.id or edit_ok)"
+  tal:omit-tag="python:1" i18n:translate="">New User</span>
+ <span tal:condition="python: not context.id and edit_ok"
+  tal:omit-tag="python:1" i18n:translate="">New User Editing</span>
+ <span tal:condition="python: context.id and not edit_ok"
+  tal:omit-tag="python:1" i18n:translate="">User<tal:x
+  replace="context/id" i18n:name="id" /></span>
+ <span tal:condition="python: context.id and edit_ok"
+  tal:omit-tag="python:1" i18n:translate="">User<tal:x
+  replace="context/id" i18n:name="id" /> Editing</span>
+</tal:block>
+
+<td class="content" metal:fill-slot="content">
+
+<p tal:condition="python:not (context.is_view_ok()
+ or request.user.hasRole('Anonymous'))" i18n:translate="">
+ You are not allowed to view this page.</p>
+
+<p tal:condition="python:not context.is_view_ok()
+ and request.user.hasRole('Anonymous')" i18n:translate="">
+ Please login with your username and password.</p>
+
+<div tal:condition="context/is_view_ok">
+
+<form method="POST"
+      name="itemSynopsis"
+      tal:define="required python:'username address'.split()"
+      enctype="multipart/form-data"
+      tal:attributes="action context/designator;
+      onSubmit python:'return checkRequiredFields(\'%s\')'%'\', \''.join(required);
+      ">
+<table class="form" tal:define="
+  th_label templates/page/macros/th_label;
+  src_input templates/page/macros/user_src_input;
+  normal_input templates/page/macros/user_normal_input;
+  pw_input templates/page/macros/user_pw_input;
+  confirm_input templates/page/macros/user_confirm_input;
+  edit_ok context/is_edit_ok;
+  ">
+ <tr tal:define="name string:realname; label string:Name; value context/realname; edit_ok edit_ok">
+  <th metal:use-macro="th_label">Name</th>
+  <td><input name="realname" metal:use-macro="src_input"></td>
+ </tr>
+ <tr tal:define="name string:username; label string:Login Name; value context/username">
+   <th metal:use-macro="th_label">Login Name</th>
+   <td><input metal:use-macro="src_input"></td>
+ </tr>
+ <tal:if condition="edit_ok">
+ <tr tal:define="name string:password; label string:Login Password">
+  <th metal:use-macro="th_label">Login Password</th>
+  <td><input metal:use-macro="pw_input" type="password"></td>
+ </tr>
+ <tr tal:define="name string:password; label string:Confirm Password">
+  <th metal:use-macro="th_label">Confirm Password</th>
+  <td><input metal:use-macro="confirm_input" type="password"></td>
+ </tr>
+ </tal:if>
+ <tal:if condition="python:request.user.hasPermission('Web Roles')">
+ <tr tal:define="name string:roles; label string:Roles;">
+  <th><label for="roles" i18n:translate="">Roles</label></th>
+  <td tal:define="gips context/id">
+    <tal:subif condition=gips define="value context/roles">
+      <input metal:use-macro="normal_input">
+    </tal:subif>
+    <tal:subif condition="not:gips" define="value db/config/NEW_WEB_USER_ROLES">
+      <input metal:use-macro="normal_input">
+    </tal:subif>
+   <tal:block i18n:translate="">(to give the user more than one role,
+    enter a comma,separated,list)</tal:block>
+  </td>
+ </tr>
+ </tal:if>
+
+ <tr tal:define="name string:phone; label string:Phone; value context/phone">
+  <th metal:use-macro="th_label">Phone</th>
+  <td><input name="phone" metal:use-macro="normal_input"></td>
+ </tr>
+
+ <tr tal:define="name string:organisation; label string:Organisation; value context/organisation">
+  <th metal:use-macro="th_label">Organisation</th>
+  <td><input name="organisation" metal:use-macro="normal_input"></td>
+ </tr>
+
+ <tr tal:condition="python:edit_ok or context.timezone"
+     tal:define="name string:timezone; label string:Timezone; value context/timezone">
+  <th metal:use-macro="th_label">Timezone</th>
+  <td><input name="timezone" metal:use-macro="normal_input">
+   <tal:block tal:condition="edit_ok" i18n:translate="">(this is a numeric hour offset, the default is
+    <span tal:replace="db/config/DEFAULT_TIMEZONE" i18n:name="zone"
+    />)</tal:block>
+  </td>
+ </tr>
+
+ <tr tal:define="name string:address; label string:E-mail address; value context/address">
+  <th metal:use-macro="th_label">E-mail address</th>
+  <td tal:define="mailto python:context.address.field(id='address');
+	  mklink python:mailto and not edit_ok">
+      <a href="mailto:calvin at the-z.org"
+		  tal:attributes="href string:mailto:$value"
+		  tal:content="value"
+          tal:condition="python:mklink">calvin at the-z.org</a>
+      <tal:if condition=edit_ok>
+      <input metal:use-macro="src_input" value="calvin at the-z.org">
+      </tal:if>
+      &nbsp;
+  </td>
+ </tr>
+
+ <tr>
+  <th><label for="alternate_addresses" i18n:translate="">Alternate E-mail addresses<br>One address per line</label></th>
+  <td>
+    <textarea rows=5 cols=40 tal:replace="structure context/alternate_addresses/multiline">nobody at nowhere.org
+anybody at everywhere.net
+(alternate_addresses)
+    </textarea>
+  </td>
+ </tr>
+
+ <tr tal:condition="edit_ok">
+  <td>
+   &nbsp;
+   <input type="hidden" name="@template" value="item">
+   <input type="hidden" name="@required" value="username,address"
+          tal:attributes="value python:','.join(required)">
+  </td>
+  <td><input type="submit" value="save" tal:replace="structure context/submit"><!--submit button here-->
+    <input type=reset>
+  </td>
+ </tr>
+</table>
+</form>
+
+<tal:block tal:condition="not:context/id" i18n:translate="">
+<table class="form">
+<tr>
+ <td>Note:&nbsp;</td>
+ <th class="required">highlighted</th>
+ <td>&nbsp;fields are required.</td>
+</tr>
+</table>
+</tal:block>
+
+<tal:block tal:condition="context/id" tal:replace="structure context/history" />
+
+</div>
+
+</td>
+
+</tal:doc>

Added: tracker/roundup-src/share/roundup/templates/classic/html/user.register.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/html/user.register.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,81 @@
+<!-- dollarId: user.item,v 1.7 2002/08/16 04:29:04 richard Exp dollar-->
+<tal:block metal:use-macro="templates/page/macros/icing">
+<title metal:fill-slot="head_title"
+ i18n:translate="">Registering with <span i18n:name="tracker"
+ tal:replace="db/config/TRACKER_NAME" /></title>
+<span metal:fill-slot="body_title" tal:omit-tag="python:1"
+ i18n:translate="">Registering with <span i18n:name="tracker"
+ tal:replace="db/config/TRACKER_NAME" /></span>
+<td class="content" metal:fill-slot="content">
+
+<form method="POST" onSubmit="return submit_once()"
+      enctype="multipart/form-data"
+      tal:attributes="action context/designator">
+
+<table class="form">
+ <tr>
+  <th i18n:translate="">Name</th>
+  <td tal:content="structure context/realname/field">realname</td>
+ </tr>
+ <tr>
+  <th class="required" i18n:translate="">Login Name</th>
+  <td tal:content="structure context/username/field">username</td>
+ </tr>
+ <tr>
+  <th class="required" i18n:translate="">Login Password</th>
+  <td tal:content="structure context/password/field">password</td>
+ </tr>
+ <tr>
+  <th class="required" i18n:translate="">Confirm Password</th>
+  <td tal:content="structure context/password/confirm">password</td>
+ </tr>
+ <tr tal:condition="python:request.user.hasPermission('Web Roles')">
+  <th i18n:translate="">Roles</th>
+  <td tal:condition="exists:item"
+      tal:content="structure context/roles/field">roles</td>
+  <td tal:condition="not:exists:item">
+   <input name="roles" tal:attributes="value db/config/NEW_WEB_USER_ROLES">
+  </td>
+ </tr>
+ <tr>
+  <th i18n:translate="">Phone</th>
+  <td tal:content="structure context/phone/field">phone</td>
+ </tr>
+ <tr>
+  <th i18n:translate="">Organisation</th>
+  <td tal:content="structure context/organisation/field">organisation</td>
+ </tr>
+ <tr>
+  <th class="required" i18n:translate="">E-mail address</th>
+  <td tal:content="structure context/address/field">address</td>
+ </tr>
+ <tr>
+  <th i18n:translate="">Alternate E-mail addresses<br>One address per line</th>
+  <td tal:content="structure context/alternate_addresses/multiline">alternate_addresses</td>
+ </tr>
+
+ <tr>
+  <td>&nbsp;</td>
+  <td>
+   <input type="hidden" name="@template" value="register">
+   <input type="hidden" name="@required" value="username,password,address">
+   <input type="hidden" name="@action" value="register">
+   <input type="submit" name="submit" value="Register" i18n:attributes="value">
+  </td>
+ </tr>
+</table>
+</form>
+
+<tal:block tal:condition="not:context/id" i18n:translate="">
+<table class="form">
+<tr>
+ <td>Note:&nbsp;</td>
+ <th class="required">highlighted</th>
+ <td>&nbsp;fields are required.</td>
+</tr>
+</table>
+</tal:block>
+
+</td>
+
+</tal:block>

Added: tracker/roundup-src/share/roundup/templates/classic/html/user.rego_progress.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/html/user.rego_progress.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,16 @@
+<!-- dollarId: issue.index,v 1.2 2001/07/29 04:07:37 richard Exp dollar-->
+<tal:block metal:use-macro="templates/page/macros/icing">
+<title metal:fill-slot="head_title"
+ i18n:translate="">Registration in progress - <span i18n:name="tracker"
+ tal:replace="config/TRACKER_NAME" /></title>
+<span metal:fill-slot="body_title" tal:omit-tag="python:1"
+ i18n:translate="">Registration in progress...</span>
+<td class="content" metal:fill-slot="content">
+
+<p i18n:translate="">You will shortly receive an email
+to confirm your registration. To complete the registration process,
+visit the link indicated in the email.
+</p>
+
+</td>
+</tal:block>

Added: tracker/roundup-src/share/roundup/templates/classic/html/user_utils.js
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/html/user_utils.js	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,114 @@
+// User Editing Utilities
+
+/**
+ * for new users:
+ * Depending on the input field which calls it, takes the value
+ * and dispatches it to certain other input fields:
+ *
+ * address
+ *  +-> username
+ *  |    `-> realname
+ *  `-> organisation
+ */
+function split_name(that) {
+    var raw = that.value
+    var val = trim(raw)
+    if (val == '') {
+        return
+    }
+    var username=''
+    var realname=''
+    var address=''
+    switch (that.name) {
+        case 'address':
+            address=val
+            break
+        case 'username':
+            username=val
+            break
+        case 'realname':
+            realname=val
+            break
+        case 'firstname':
+        case 'lastname':
+           return
+        default:
+            alert('Ooops - unknown name field '+that.name+'!')
+            return
+    }
+    var the_form = that.form;
+
+    function field_empty(name) {
+        return the_form[name].value == ''
+    }
+
+    // no break statements - on purpose!
+    switch (that.name) {
+        case 'address':
+            var split1 = address.split('@')
+            if (field_empty('username')) {
+                username = split1[0]
+                the_form.username.value = username
+            }
+            if (field_empty('organisation')) {
+                the_form.organisation.value = default_organisation(split1[1])
+            }
+        case 'username':
+            if (field_empty('realname')) {
+                realname = Cap(username.split('.').join(' '))
+                the_form.realname.value = realname
+            }
+        case 'realname':
+            if (field_empty('username')) {
+                username = Cap(realname.replace(' ', '.'))
+                the_form.username.value = username
+            }
+            if (the_form.firstname && the_form.lastname) {
+                var split2 = realname.split(' ')
+                var firstname='', lastname=''
+                firstname = split2[0]
+                lastname = split2.slice(1).join(' ')
+                if (field_empty('firstname')) {
+                    the_form.firstname.value = firstname
+                }
+                if (field_empty('lastname')) {
+                    the_form.lastname.value = lastname
+                }
+            }
+    }
+}
+
+function SubCap(str) {
+    switch (str) {
+        case 'de': case 'do': case 'da':
+        case 'du': case 'von':
+            return str;
+    }
+    if (str.toLowerCase().slice(0,2) == 'mc') {
+        return 'Mc'+str.slice(2,3).toUpperCase()+str.slice(3).toLowerCase()
+    }
+    return str.slice(0,1).toUpperCase()+str.slice(1).toLowerCase()
+}
+
+function Cap(str) {
+    var liz = str.split(' ')
+    for (var i=0; i<liz.length; i++) {
+        liz[i] = SubCap(liz[i])
+    }
+    return liz.join(' ')
+}
+
+/**
+ * Takes a domain name (behind the @ part of an email address)
+ * Customise this to handle the mail domains you're interested in 
+ */
+function default_organisation(orga) {
+    switch (orga.toLowerCase()) {
+        case 'gmx':
+        case 'yahoo':
+            return ''
+        default:
+            return orga
+    }
+}
+

Added: tracker/roundup-src/share/roundup/templates/classic/initial_data.py
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/initial_data.py	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,32 @@
+#
+# TRACKER INITIAL PRIORITY AND STATUS VALUES
+#
+pri = db.getclass('priority')
+pri.create(name=''"critical", order="1")
+pri.create(name=''"urgent", order="2")
+pri.create(name=''"bug", order="3")
+pri.create(name=''"feature", order="4")
+pri.create(name=''"wish", order="5")
+
+stat = db.getclass('status')
+stat.create(name=''"unread", order="1")
+stat.create(name=''"deferred", order="2")
+stat.create(name=''"chatting", order="3")
+stat.create(name=''"need-eg", order="4")
+stat.create(name=''"in-progress", order="5")
+stat.create(name=''"testing", order="6")
+stat.create(name=''"done-cbb", order="7")
+stat.create(name=''"resolved", order="8")
+
+# create the two default users
+user = db.getclass('user')
+user.create(username="admin", password=adminpw,
+    address=admin_email, roles='Admin')
+user.create(username="anonymous", roles='Anonymous')
+
+# add any additional database creation steps here - but only if you
+# haven't initialised the database with the admin "initialise" command
+
+
+# vim: set filetype=python sts=4 sw=4 et si
+#SHA: b1da2e72a7fe9f26086f243eb744135b085101d9

Added: tracker/roundup-src/share/roundup/templates/classic/schema.py
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/classic/schema.py	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,172 @@
+
+#
+# TRACKER SCHEMA
+#
+
+# Class automatically gets these properties:
+#   creation = Date()
+#   activity = Date()
+#   creator = Link('user')
+#   actor = Link('user')
+
+# Priorities
+pri = Class(db, "priority",
+                name=String(),
+                order=Number())
+pri.setkey("name")
+
+# Statuses
+stat = Class(db, "status",
+                name=String(),
+                order=Number())
+stat.setkey("name")
+
+# Keywords
+keyword = Class(db, "keyword",
+                name=String())
+keyword.setkey("name")
+
+# User-defined saved searches
+query = Class(db, "query",
+                klass=String(),
+                name=String(),
+                url=String(),
+                private_for=Link('user'))
+
+# add any additional database schema configuration here
+
+user = Class(db, "user",
+                username=String(),
+                password=Password(),
+                address=String(),
+                realname=String(),
+                phone=String(),
+                organisation=String(),
+                alternate_addresses=String(),
+                queries=Multilink('query'),
+                roles=String(),     # comma-separated string of Role names
+                timezone=String())
+user.setkey("username")
+
+# FileClass automatically gets this property in addition to the Class ones:
+#   content = String()    [saved to disk in <tracker home>/db/files/]
+#   type = String()       [MIME type of the content, default 'text/plain']
+msg = FileClass(db, "msg",
+                author=Link("user", do_journal='no'),
+                recipients=Multilink("user", do_journal='no'),
+                date=Date(),
+                summary=String(),
+                files=Multilink("file"),
+                messageid=String(),
+                inreplyto=String())
+
+file = FileClass(db, "file",
+                name=String())
+
+# IssueClass automatically gets these properties in addition to the Class ones:
+#   title = String()
+#   messages = Multilink("msg")
+#   files = Multilink("file")
+#   nosy = Multilink("user")
+#   superseder = Multilink("issue")
+issue = IssueClass(db, "issue",
+                assignedto=Link("user"),
+                keyword=Multilink("keyword"),
+                priority=Link("priority"),
+                status=Link("status"))
+
+#
+# TRACKER SECURITY SETTINGS
+#
+# See the configuration and customisation document for information
+# about security setup.
+
+#
+# REGULAR USERS
+#
+# Give the regular users access to the web and email interface
+db.security.addPermissionToRole('User', 'Web Access')
+db.security.addPermissionToRole('User', 'Email Access')
+
+# Assign the access and edit Permissions for issue, file and message
+# to regular users now
+for cl in 'issue', 'file', 'msg', 'keyword':
+    db.security.addPermissionToRole('User', 'View', cl)
+    db.security.addPermissionToRole('User', 'Edit', cl)
+    db.security.addPermissionToRole('User', 'Create', cl)
+for cl in 'priority', 'status':
+    db.security.addPermissionToRole('User', 'View', cl)
+
+# May users view other user information? Comment these lines out
+# if you don't want them to
+db.security.addPermissionToRole('User', 'View', 'user')
+
+# Users should be able to edit their own details -- this permission is
+# limited to only the situation where the Viewed or Edited item is their own.
+def own_record(db, userid, itemid):
+    '''Determine whether the userid matches the item being accessed.'''
+    return userid == itemid
+p = db.security.addPermission(name='View', klass='user', check=own_record,
+    description="User is allowed to view their own user details")
+db.security.addPermissionToRole('User', p)
+p = db.security.addPermission(name='Edit', klass='user', check=own_record,
+    description="User is allowed to edit their own user details")
+db.security.addPermissionToRole('User', p)
+
+# Users should be able to edit and view their own queries. They should also
+# be able to view any marked as not private. They should not be able to
+# edit others' queries, even if they're not private
+def view_query(db, userid, itemid):
+    private_for = db.query.get(itemid, 'private_for')
+    if not private_for: return True
+    return userid == private_for
+def edit_query(db, userid, itemid):
+    return userid == db.query.get(itemid, 'creator')
+p = db.security.addPermission(name='View', klass='query', check=view_query,
+    description="User is allowed to view their own and public queries")
+db.security.addPermissionToRole('User', p)
+p = db.security.addPermission(name='Edit', klass='query', check=edit_query,
+    description="User is allowed to edit their queries")
+db.security.addPermissionToRole('User', p)
+p = db.security.addPermission(name='Retire', klass='query', check=edit_query,
+    description="User is allowed to retire their queries")
+db.security.addPermissionToRole('User', p)
+p = db.security.addPermission(name='Create', klass='query',
+    description="User is allowed to create queries")
+db.security.addPermissionToRole('User', p)
+
+
+#
+# ANONYMOUS USER PERMISSIONS
+#
+# Let anonymous users access the web interface. Note that almost all
+# trackers will need this Permission. The only situation where it's not
+# required is in a tracker that uses an HTTP Basic Authenticated front-end.
+db.security.addPermissionToRole('Anonymous', 'Web Access')
+
+# Let anonymous users access the email interface (note that this implies
+# that they will be registered automatically, hence they will need the
+# "Create" user Permission below)
+# This is disabled by default to stop spam from auto-registering users on
+# public trackers.
+#db.security.addPermissionToRole('Anonymous', 'Email Access')
+
+# Assign the appropriate permissions to the anonymous user's Anonymous
+# Role. Choices here are:
+# - Allow anonymous users to register
+db.security.addPermissionToRole('Anonymous', 'Create', 'user')
+
+# Allow anonymous users access to view issues (and the related, linked
+# information)
+for cl in 'issue', 'file', 'msg', 'keyword', 'priority', 'status':
+    db.security.addPermissionToRole('Anonymous', 'View', cl)
+
+# [OPTIONAL]
+# Allow anonymous users access to create or edit "issue" items (and the
+# related file and message items)
+#for cl in 'issue', 'file', 'msg':
+#   db.security.addPermissionToRole('Anonymous', 'Create', cl)
+#   db.security.addPermissionToRole('Anonymous', 'Edit', cl)
+
+
+# vim: set filetype=python sts=4 sw=4 et si :

Added: tracker/roundup-src/share/roundup/templates/minimal/TEMPLATE-INFO.txt
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/minimal/TEMPLATE-INFO.txt	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,8 @@
+Name: minimal
+Description: This is an empty tracker - it must be customised for it to be
+             useful! It only defines the bare minimum of information - the
+             user database and the two default users (admin and anonymous).
+             The rest is entirely up to you! Not recommended for first-time
+             Roundup users (it's easier to tweak the Classic tracker).
+Intended-For: Roundup experts who need a clean slate to start with.
+

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

Added: tracker/roundup-src/share/roundup/templates/minimal/extensions/README.txt
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/minimal/extensions/README.txt	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,6 @@
+This directory is for tracker extensions:
+
+- CGI Actions
+- Templating functions
+
+See the customisation doc for more information.

Added: tracker/roundup-src/share/roundup/templates/minimal/html/_generic.404.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/minimal/html/_generic.404.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,9 @@
+<html>
+<head>
+<title>Item Not Found</title>
+</head>
+
+<body>
+There is no <span tal:content="context/_classname" /> with id <span tal:content="context/id"/>
+</body>
+</html>

Added: tracker/roundup-src/share/roundup/templates/minimal/html/_generic.calendar.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/minimal/html/_generic.calendar.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,18 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+ <head>
+  <link rel="stylesheet" type="text/css" href="@@file/style.css" />
+  <meta http-equiv="Content-Type" content="text/html; charset=utf-8;" />
+  <title tal:content="string:Roundup Calendar"></title>
+  <script language="Javascript"
+          type="text/javascript"
+          tal:content="structure string:
+          // this is the name of the field in the original form that we're working on
+          form  = window.opener.document.${request/form/form/value};
+          field = '${request/form/property/value}';" >
+  </script>
+ </head>
+ <body class="body"
+       tal:content="structure python:utils.html_calendar (request)">
+ </body>
+</html>

Added: tracker/roundup-src/share/roundup/templates/minimal/html/_generic.collision.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/minimal/html/_generic.collision.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,16 @@
+<tal:block metal:use-macro="templates/page/macros/icing">
+<title metal:fill-slot="head_title" i18n:translate=""
+ ><span tal:replace="python:context._classname.capitalize()"
+ i18n:name="class" /> Edit Collision - <span i18n:name="tracker"
+ tal:replace="config/TRACKER_NAME" /></title>
+<tal:block metal:fill-slot="body_title" i18n:translate=""
+ ><span tal:replace="python:context._classname.capitalize()"
+ i18n:name="class" /> Edit Collision</tal:block>
+
+<td class="content" metal:fill-slot="content" i18n:translate="
+  There has been a collision. Another user updated this node
+  while you were editing. Please <a href='${context}'>reload</a>
+  the node and review your edits.
+"><span tal:replace="context/designator" i18n:name="context" />
+</td>
+</tal:block>

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

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

Added: tracker/roundup-src/share/roundup/templates/minimal/html/_generic.item.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/minimal/html/_generic.item.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,65 @@
+<tal:block metal:use-macro="templates/page/macros/icing">
+<title metal:fill-slot="head_title" i18n:translate=""
+ ><span tal:replace="python:context._classname.capitalize()"
+ i18n:name="class" /> editing - <span i18n:name="tracker"
+ tal:replace="config/TRACKER_NAME" /></title>
+<tal:block metal:fill-slot="body_title" i18n:translate=""
+ ><span tal:replace="python:context._classname.capitalize()"
+ i18n:name="class" /> editing</tal:block>
+
+<td class="content" metal:fill-slot="content">
+
+<span tal:condition="python:not (context.is_view_ok() or context.is_edit_ok()
+ or request.user.hasRole('Anonymous'))"
+ tal:omit-tag="python:1" i18n:translate=""
+>You are not allowed to view this page.</span>
+
+<span tal:condition="python:not (context.is_view_ok() or context.is_edit_ok())
+ and request.user.hasRole('Anonymous')"
+ tal:omit-tag="python:1" i18n:translate=""
+>Please login with your username and password.</span>
+
+<form method="POST" onSubmit="return submit_once()"
+      enctype="multipart/form-data" tal:condition="context/is_edit_ok"
+      tal:attributes="action context/designator">
+
+<input type="hidden" name="@template" value="item">
+
+<table class="form">
+
+<tr tal:repeat="prop python:db[context._classname].properties()">
+ <tal:block tal:condition="python:prop._name not in ('id',
+   'creator', 'creation', 'actor', 'activity')">
+  <th tal:content="prop/_name"></th>
+  <td tal:content="structure python:context[prop._name].field()"></td>
+ </tal:block>
+</tr>
+<tr>
+ <td>&nbsp;</td>
+ <td colspan=3 tal:content="structure context/submit">
+  submit button will go here
+ </td>
+</tr>
+</table>
+
+</form>
+
+<table class="form" tal:condition="context/is_only_view_ok">
+
+<tr tal:repeat="prop python:db[context._classname].properties()">
+ <tal:block tal:condition="python:prop._name not in ('id', 'creator',
+                                  'creation', 'activity')">
+  <th tal:content="prop/_name"></th>
+  <td tal:content="structure python:context[prop._name].field()"></td>
+ </tal:block>
+</tr>
+</table>
+
+
+<tal:block tal:condition="python:context.id and context.is_view_ok()">
+ <tal:block tal:replace="structure context/history" />
+</tal:block>
+
+</td>
+
+</tal:block>

Added: tracker/roundup-src/share/roundup/templates/minimal/html/help_controls.js
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/minimal/html/help_controls.js	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,111 @@
+// initial values for either Nosy, Superseder, Keyword and Waiting On,
+// depending on which has called
+original_field = form[field].value;
+
+// Some browsers (ok, IE) don't define the "undefined" variable.
+undefined = document.geez_IE_is_really_friggin_annoying;
+
+function trim(value) {
+  var temp = value;
+  var obj = /^(\s*)([\W\w]*)(\b\s*$)/;
+  if (obj.test(temp)) { temp = temp.replace(obj, '$2'); }
+  var obj = /  /g;
+  while (temp.match(obj)) { temp = temp.replace(obj, " "); }
+  return temp;
+}
+
+function determineList() {
+    // generate a comma-separated list of the checked items
+    var list = new String('');
+    for (box=0; box < document.frm_help.check.length; box++) {
+        if (document.frm_help.check[box].checked) {
+            if (list.length == 0) {
+                separator = '';
+            }
+            else {
+                separator = ',';
+            }
+            // we used to use an Array and push / join, but IE5.0 sux
+            list = list + separator + document.frm_help.check[box].value;
+        }
+    }
+    return list;
+}
+
+function updateList() {
+  // write back to opener window
+  if (document.frm_help.check==undefined) { return; }
+  form[field].value = determineList();
+}
+
+function updatePreview() {
+  // update the preview box
+  if (document.frm_help.check==undefined) { return; }
+  writePreview(determineList());
+}
+
+function clearList() {
+  // uncheck all checkboxes
+  if (document.frm_help.check==undefined) { return; }
+  for (box=0; box < document.frm_help.check.length; box++) {
+      document.frm_help.check[box].checked = false;
+  }
+}
+
+function reviseList(vals) {
+  // update the checkboxes based on the preview field
+  if (document.frm_help.check==undefined) { return; }
+  var to_check;
+  var list = vals.split(",");
+  if (document.frm_help.check.length==undefined) {
+      check = document.frm_help.check;
+      to_check = false;
+      for (val in list) {
+          if (check.value==trim(list[val])) {
+              to_check = true;
+              break;
+          }
+      }
+      check.checked = to_check;
+  } else {
+    for (box=0; box < document.frm_help.check.length; box++) {
+      check = document.frm_help.check[box];
+      to_check = false;
+      for (val in list) {
+          if (check.value==trim(list[val])) {
+              to_check = true;
+              break;
+          }
+      }
+      check.checked = to_check;
+    }
+  }
+}
+
+function resetList() {
+  // reset preview and check boxes to initial values
+  if (document.frm_help.check==undefined) { return; }
+  writePreview(original_field);
+  reviseList(original_field);
+}
+
+function writePreview(val) {
+   // writes a value to the text_preview
+   document.frm_help.text_preview.value = val;
+}
+
+function focusField(name) {
+    for(i=0; i < document.forms.length; ++i) {
+      var obj = document.forms[i].elements[name];
+      if (obj && obj.focus) {obj.focus();}
+    }
+}
+
+function selectField(name) {
+    for(i=0; i < document.forms.length; ++i) {
+      var obj = document.forms[i].elements[name];
+      if (obj && obj.focus){obj.focus();} 
+      if (obj && obj.select){obj.select();}
+    }
+}
+

Added: tracker/roundup-src/share/roundup/templates/minimal/html/home.classlist.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/minimal/html/home.classlist.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,25 @@
+<tal:block metal:use-macro="templates/page/macros/icing">
+<title metal:fill-slot="head_title" i18n:translate="">List of classes - <span
+ i18n:name="tracker" tal:replace="config/TRACKER_NAME" /></title>
+<span metal:fill-slot="body_title" tal:omit-tag="python:1"
+ i18n:translate="">List of classes</span>
+<td class="content" metal:fill-slot="content">
+<table class="classlist">
+
+<tal:block tal:repeat="cl db/classes">
+ <tr>
+  <th class="header" colspan="2" align="left">
+   <a tal:attributes="href string:${cl/classname}"
+      tal:content="python:cl.classname.capitalize()">classname</a>
+  </th>
+ </tr>
+ <tr tal:repeat="prop cl/properties">
+  <th tal:content="prop/_name">name</th>
+  <td tal:content="prop/_prop">type</td>
+ </tr>
+</tal:block>
+
+</table>
+</td>
+
+</tal:block>

Added: tracker/roundup-src/share/roundup/templates/minimal/html/home.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/minimal/html/home.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,25 @@
+<tal:block metal:use-macro="templates/page/macros/icing">
+<title metal:fill-slot="head_title" i18n:translate="">Tracker home - <span
+ i18n:name="tracker" tal:replace="config/TRACKER_NAME" /></title>
+<span metal:fill-slot="body_title" tal:omit-tag="python:1"
+ i18n:translate="">Tracker home</span>
+<td class="content" metal:fill-slot="content">
+
+<!--
+ This is the default body that is displayed when people visit the
+ tracker. The tag below lists the currently open issues. You may
+ replace it with a greeting message, or a different list of issues or
+ whatever. It's a good idea to have the issues on the front page though
+-->
+
+<tal:block tal:define="anon python:request.user.username == 'anonymous'">
+<p tal:condition="not:anon" class="help" i18n:translate="">
+Please select from one of the menu options on the left.
+</p>
+<p tal:condition="anon" class="help" i18n:translate="">
+Please log in or register.
+</p>
+</tal:block>
+
+</td>
+</tal:block>

Added: tracker/roundup-src/share/roundup/templates/minimal/html/page.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/minimal/html/page.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,334 @@
+<!-- vim:sw=2 sts=2
+--><tal:block metal:define-macro="icing"
+><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+<head>
+<title metal:define-slot="head_title">title goes here</title>
+<link rel="stylesheet" type="text/css" href="@@file/style.css">
+<meta http-equiv="Content-Type"
+ tal:attributes="content string:text/html;; charset=${request/client/charset}" />
+<script tal:replace="structure request/base_javascript">
+</script>
+<metal:x define-slot="more-javascript" />
+
+</head>
+<body class="body">
+
+<table class="body"
+ tal:define="
+kw_edit python:request.user.hasPermission('Edit', 'keyword');
+kw_create python:request.user.hasPermission('Create', 'keyword');
+kw_edit_link python:kw_edit and db.keyword.list();
+columns string:id,activity,title,creator,status;
+columns_showall string:id,activity,title,creator,assignedto,status;
+status_notresolved string:-1,1,2,3,4,5,6,7;
+"
+>
+
+<tr>
+ <td class="page-header-left">&nbsp;</td>
+ <td class="page-header-top">
+   <div id="body-title">
+     <h2><span metal:define-slot="body_title">body title</span></h2>
+   </div>
+   <div id="searchbox">
+     <form method="GET" action="issue">
+       <input type="hidden" name="@columns"
+	      tal:attributes="value columns_showall"
+	      value="id,activity,title,creator,assignedto,status"/>
+       <input type="hidden" name="@sort" value="activity"/>
+       <input type="hidden" name="@group" value="priority"/>
+       <input id="search-text" name="@search_text" size="10"
+              tal:attributes="value request/search_text"/>
+       <input type="submit" id="submit" name="submit" value="Search" i18n:attributes="value" />
+     </form>
+  </div>
+ </td>
+</tr>
+
+<tr>
+ <td rowspan="2" valign="top" class="sidebar">
+  <p class="classblock"
+     tal:condition="python:request.user.hasPermission('View', 'query')">
+   <span i18n:translate=""
+    ><b>Your Queries</b> (<a href="query?@template=edit">edit</a>)</span><br>
+   <tal:block tal:repeat="qs request/user/queries">
+    <a href="#" tal:attributes="href string:${qs/klass}?${qs/url}&@dispname=${qs/name}"
+       tal:content="qs/name">link</a><br>
+   </tal:block>
+  </p>
+
+  <form method="POST" tal:attributes="action request/base">
+   <p class="classblock"
+       tal:condition="python:request.user.hasPermission('View', 'issue')">
+    <b i18n:translate="">Issues</b><br>
+    <span tal:condition="python:request.user.hasPermission('Create', 'issue')">
+      <a href="issue?@template=item" i18n:translate="">Create New</a><br>
+    </span>
+    <a href="#"
+       tal:attributes="href python:request.indexargs_url('issue', {
+      '@sort': '-activity',
+      '@group': 'priority',
+      '@filter': 'status,assignedto',
+      '@columns': columns,
+      '@search_text': '',
+      'status': status_notresolved,
+      'assignedto': '-1',
+      '@dispname': i18n.gettext('Show Unassigned'),
+     })"
+       i18n:translate="">Show Unassigned</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>
+    <a href="issue?@template=search" i18n:translate="">Search</a><br>
+    <input type="submit" class="form-small" value="Show issue:"
+     i18n:attributes="value"><input class="form-small" size="4"
+     type="text" name="@number">
+    <input type="hidden" name="@type" value="issue">
+    <input type="hidden" name="@action" value="show">
+   </p>
+  </form>
+
+  <p class="classblock"
+     tal:condition="python:kw_edit or kw_create">
+   <b i18n:translate="">Keywords</b><br>
+   <span tal:condition="python:request.user.hasPermission('Create', 'keyword')">
+    <a href="keyword?@template=item" i18n:translate="">Create New</a><br>
+   </span>
+   <span tal:condition="kw_edit_link">
+    <a href="keyword?@template=item" i18n:translate="">Edit Existing</a><br>
+   </span>
+  </p>
+
+  <p class="classblock"
+       tal:condition="python:request.user.hasPermission('View', 'user')">
+   <b i18n:translate="">Administration</b><br>
+   <span tal:condition="python:request.user.hasPermission('Edit', None)">
+    <a href="home?@template=classlist" i18n:translate="">Class List</a><br>
+   </span>
+   <span tal:condition="python:request.user.hasPermission('View', 'user')
+                            or request.user.hasPermission('Edit', 'user')">
+    <a href="user"  i18n:translate="">User List</a><br>
+   </span>
+   <a tal:condition="python:request.user.hasPermission('Create', 'user')"
+      href="user?@template=item" i18n:translate="">Add User</a>
+  </p>
+
+  <form method="POST" tal:condition="python:request.user.username=='anonymous'"
+        tal:attributes="action request/base">
+   <p class="userblock">
+    <b i18n:translate="">Login</b><br>
+    <input size="10" name="__login_name"><br>
+    <input size="10" type="password" name="__login_password"><br>
+    <input type="hidden" name="@action" value="Login">
+    <input type="checkbox" name="remember" id="remember">
+    <label for="remember" i18n:translate="">Remember me?</label><br>
+    <input type="submit" value="Login" i18n:attributes="value"><br>
+    <input type="hidden" name="__came_from" tal:attributes="value string:${request/base}${request/env/PATH_INFO}">
+    <span tal:replace="structure request/indexargs_form" />
+    <a href="user?@template=register"
+       tal:condition="python:request.user.hasPermission('Create', 'user')"
+     i18n:translate="">Register</a><br>
+    <a href="user?@template=forgotten" i18n:translate="">Lost&nbsp;your&nbsp;login?</a><br>
+   </p>
+  </form>
+
+  <p class="userblock" tal:condition="python:request.user.username != 'anonymous'">
+   <b i18n:translate="">Hello, <span i18n:name="user"
+    tal:replace="python:request.user.username.plain(escape=1)">username</span></b><br>
+   <a href="#" tal:attributes="href string:user${request/user/id}"
+    i18n:translate="">Your Details</a><br>
+   <a href="#" tal:attributes="href python:request.indexargs_url('',
+       {'@action':'logout'})" i18n:translate="">Logout</a>
+  </p>
+  <p class="userblock">
+   <b i18n:translate="">Help</b><br>
+   <a href="http://roundup.sourceforge.net/doc-1.0/"
+    i18n:translate="">Roundup docs</a>
+  </p>
+ </td>
+ <td>
+  <p tal:condition="options/error_message | nothing" class="error-message"
+     tal:repeat="m options/error_message" tal:content="structure m" />
+  <p tal:condition="options/ok_message | nothing" class="ok-message">
+    <span tal:repeat="m options/ok_message"
+       tal:content="structure string:$m <br/ > " />
+     <a class="form-small" tal:attributes="href request/current_url"
+        i18n:translate="">clear this message</a>
+  </p>
+ </td>
+</tr>
+<tr>
+ <td class="content" metal:define-slot="content">Page content goes here</td>
+</tr>
+
+</table>
+
+<pre tal:condition="request/form/debug | nothing" tal:content="request">
+</pre>
+
+</body>
+</html>
+</tal:block>
+
+<!--
+The following macros are intended to be used in search pages.
+
+The invoking context must define a "name" variable which names the
+property being searched.
+
+See issue.search.html in the classic template for examples.
+-->
+
+<!-- creates a th and a label: -->
+<th metal:define-macro="th_label"
+    tal:define="required required | python:[]"
+    tal:attributes="class python:(name in required) and 'required' or nothing">
+  <label tal:attributes="for name" tal:content="label" i18n:translate="">text</label>
+	<metal:x define-slot="behind_the_label" />
+</th>
+
+<td metal:define-macro="search_input">
+  <input tal:attributes="value python:request.form.getvalue(name) or nothing;
+                         name name;
+                         id name">
+</td>
+
+<td metal:define-macro="search_date">
+  <input tal:attributes="value python:request.form.getvalue(name) or nothing;
+                         name name;
+                         id name">
+  <a class="classhelp"
+	 tal:attributes="href python:'''javascript:help_window('issue?@template=calendar&property=%s&form=itemSynopsis', 300, 200)'''%name">(cal)</a>
+</td>
+
+<td metal:define-macro="search_popup">
+  <!--
+    context needs to specify the popup "columns" as a comma-separated
+    string (eg. "id,title" or "id,name,description") as well as name
+  -->
+  <input tal:attributes="value python:request.form.getvalue(name) or nothing;
+                         name name;
+                         id name">
+  <span tal:replace="structure python:db.issue.classhelp(columns,
+                                      property=name)" />
+</td>
+
+<td metal:define-macro="search_select">
+  <select tal:attributes="name name; id name"
+          tal:define="value python:request.form.getvalue(name)">
+    <option value="" i18n:translate="">don't care</option>
+    <metal:slot define-slot="extra_options" />
+    <option value="" i18n:translate="" disabled="disabled">------------</option>
+    <option tal:repeat="s python:db[db_klass].list()"
+            tal:attributes="value s/id; selected python:value == s.id"
+            tal:content="python:s[db_content]"></option>
+  </select>
+</td>
+
+<!-- like search_select, but translates the further values.
+Could extend it (METAL 1.1 attribute "extend-macro")
+-->
+<td metal:define-macro="search_select_translated">
+  <select tal:attributes="name name; id name"
+          tal:define="value python:request.form.getvalue(name)">
+    <option value="" i18n:translate="">don't care</option>
+    <metal:slot define-slot="extra_options" />
+    <option value="" i18n:translate="" disabled="disabled">------------</option>
+    <option tal:repeat="s python:db[db_klass].list()"
+            tal:attributes="value s/id; selected python:value == s.id"
+						tal:content="python:s[db_content]"
+						i18n:translate=""></option>
+  </select>
+</td>
+
+<!-- currently, there is no convenient API to get a list of all roles -->
+<td metal:define-macro="search_select_roles"
+	  tal:define="onchange onchange | nothing">
+  <select name=roles id=roles tal:attributes="onchange onchange">
+    <option value="" i18n:translate="">don't care</option>
+    <option value="" i18n:translate="" disabled="disabled">------------</option>
+    <option value="User">User</option>
+    <option value="Admin">Admin</option>
+    <option value="Anonymous">Anonymous</option>
+  </select>
+</td>
+
+<td metal:define-macro="search_multiselect">
+  <input tal:attributes="value python:request.form.getvalue(name) or nothing;
+                         name name;
+                         id name">
+  <span tal:replace="structure python:db[db_klass].classhelp(db_content,
+                                        property=name, width='600')" />
+</td>
+
+<td metal:define-macro="search_checkboxes">
+ <ul class="search-checkboxes"
+     tal:define="value python:request.form.getvalue(name);
+                 values python:value and value.split(',') or []">
+ <li tal:repeat="s python:db[db_klass].list()">
+  <input type="checkbox" tal:attributes="name name; id string:$name-${s/id};
+    value s/id; checked python:s.id in values" />
+  <label tal:attributes="for string:$name-${s/id}"
+         tal:content="python:s[db_content]" />
+ </li>
+ <li metal:define-slot="no_value_item">
+  <input type="checkbox" value="-1" tal:attributes="name name;
+     id string:$name--1; checked python:value == '-1'" />
+  <label tal:attributes="for string:$name--1" i18n:translate="">no value</label>
+ </li>
+ </ul>
+</td>
+
+<td metal:define-macro="column_input">
+  <input type="checkbox" name="@columns"
+         tal:attributes="value name;
+                         checked python:name in cols">
+</td>
+
+<td metal:define-macro="sort_input">
+  <input type="radio" name="@sort"
+         tal:attributes="value name;
+                         checked python:name == sort_on">
+</td>
+
+<td metal:define-macro="group_input">
+  <input type="radio" name="@group"
+         tal:attributes="value name;
+                         checked python:name == group_on">
+</td>
+
+<!--
+The following macros are intended for user editing.
+
+The invoking context must define a "name" variable which names the
+property being searched; the "edit_ok" variable tells whether the
+current user is allowed to edit.
+
+See user.item.html in the classic template for examples.
+-->
+<script metal:define-macro="user_utils" type="text/javascript" src="@@file/user_utils.js"></script>
+
+<!-- src: value will be re-used for other input fields -->
+<input metal:define-macro="user_src_input"
+    type="text" tal:attributes="onblur python:edit_ok and 'split_name(this)';
+    id name; name name; value value; readonly not:edit_ok"
+    value="heinz.kunz">
+<!-- normal: no re-using -->
+<input metal:define-macro="user_normal_input" type="text"
+    tal:attributes="id name; name name; value value; readonly not:edit_ok"
+    value="heinz">
+<!-- password: type; no initial value -->
+    <input metal:define-macro="user_pw_input" type="password"
+    tal:attributes="id name; name name; readonly not:edit_ok" value="">
+    <input metal:define-macro="user_confirm_input" type="password"
+    tal:attributes="id name; name string:@confirm@$name; readonly not:edit_ok" value="">
+

Added: tracker/roundup-src/share/roundup/templates/minimal/html/style.css
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/minimal/html/style.css	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,423 @@
+/* main page styles */
+body.body {
+  font-family: sans-serif, Arial, Helvetica;
+  background-color: white;
+  color: #333;
+  margin: 0;
+}
+a[href]:hover {
+  color:blue;
+  text-decoration: underline;
+}
+a[href], a[href]:link {
+  color:blue;
+  text-decoration: none;
+}
+
+table.body {
+  border: 0;
+  padding: 0;
+  border-spacing: 0;
+  border-collapse: separate;
+}
+
+td.page-header-left {
+  padding: 5px;
+  border-bottom: 1px solid #444;
+}
+td.sidebar {
+  padding: 1px 0 0 1px;
+  white-space: nowrap;
+}
+
+/* don't display the sidebar when printing */
+ at media print {
+    td.page-header-left {
+        display: none;
+    }
+    td.sidebar {
+        display: none;
+    }
+    .index-controls {
+        display: none;
+    }
+    #searchbox {
+        display: none;
+    }
+}
+
+td.page-header-top {
+  padding: 5px;
+  border-bottom: 1px solid #444;
+}
+#searchbox {
+    float: right;
+}
+
+div#body-title {
+  float: left;
+}
+
+
+div#searchbox {
+  float: right;
+  padding-top: 1em;
+}
+
+div#searchbox input#search-text {
+  width: 10em;
+}
+
+form {
+  margin: 0;
+}
+
+textarea {
+    font-family: monospace;
+}
+
+td.sidebar p.classblock {
+  padding: 2px 5px 2px 5px;
+  margin: 1px;
+  border: 1px solid #444;
+  background-color: #eee;
+}
+
+td.sidebar p.userblock {
+  padding: 2px 5px 2px 5px;
+  margin: 1px 1px 1px 1px;
+  border: 1px solid #444;
+  background-color: #eef;
+}
+
+.form-small {
+  padding: 0;
+  font-size: 75%;
+}
+
+
+td.content {
+  padding: 1px 5px 1px 5px;
+  vertical-align: top;
+  width: 100%;
+}
+
+td.date, th.date { 
+  white-space: nowrap;
+}
+
+p.ok-message {
+  background-color: #22bb22;
+  padding: 5px;
+  color: white;
+  font-weight: bold;
+}
+p.error-message {
+  background-color: #bb2222;
+  padding: 5px;
+  color: white;
+  font-weight: bold;
+}
+p.error-message a[href] {
+  color: white;
+  text-decoration: underline;
+}
+
+
+/* style for search forms */
+ul.search-checkboxes {
+    display: inline;
+    padding: none;
+    list-style: none;
+}
+ul.search-checkboxes > li {
+    display: inline;
+    padding-right: .5em;
+}
+
+
+/* style for forms */
+table.form {
+  padding: 2px;
+  border-spacing: 0;
+  border-collapse: separate;
+}
+
+table.form th {
+  color: #338;
+  text-align: right;
+  vertical-align: top;
+  font-weight: normal;
+  white-space: nowrap;
+}
+
+table.form th.header {
+  font-weight: bold;
+  background-color: #eef;
+  text-align: left;
+}
+
+table.form th.required {
+  font-weight: bold;
+}
+
+table.form td {
+  color: #333;
+  empty-cells: show;
+  vertical-align: top;
+}
+
+table.form td.optional {
+  font-weight: bold;
+  font-style: italic;
+}
+
+table.form td.html {
+  color: #777;
+}
+
+/* style for lists */
+table.list {
+  border-spacing: 0;
+  border-collapse: separate;
+  width: 100%;
+}
+
+table.list th {
+  padding: 0 4px 0 4px;
+  color: #404070;
+  background-color: #eef;
+  border: 1px solid white;
+  vertical-align: top;
+  empty-cells: show;
+}
+table.list th a[href]:hover { color: #404070 }
+table.list th a[href]:link { color: #404070 }
+table.list th a[href] { color: #404070 }
+table.list th.group {
+  background-color: #f4f4ff;
+  text-align: center;
+}
+
+table.list td {
+  padding: 0 4px 0 4px;
+  border: 1px solid white;
+  color: #404070;
+  background-color: #efefef;
+  vertical-align: top;
+  empty-cells: show;
+}
+
+table.list tr.navigation th {
+  width: 33%;
+  border-style: hidden;
+  text-align: center;
+}
+table.list tr.navigation td {
+    border: none
+}
+table.list tr.navigation th:first-child {
+  text-align: left;
+}
+table.list tr.navigation th:last-child {
+  text-align: right;
+}
+
+
+/* style for message displays */
+table.messages {
+  border-spacing: 0;
+  border-collapse: separate;
+  width: 100%;
+}
+
+table.messages th.header{
+  padding-top: 10px;
+  border-bottom: 1px solid gray;
+  font-weight: bold;
+  background-color: white;
+  color: #707040;
+}
+
+table.messages th {
+  font-weight: bold;
+  color: black;
+  text-align: left;
+  border-bottom: 1px solid #afafaf;
+}
+
+table.messages td {
+  font-family: monospace;
+  background-color: #efefef;
+  border-bottom: 1px solid #afafaf;
+  color: black;
+  empty-cells: show;
+  border-right: 1px solid #afafaf;
+  vertical-align: top;
+  padding: 2px 5px 2px 5px;
+}
+
+table.messages td:first-child {
+  border-left: 1px solid #afafaf;
+  border-right: 1px solid #afafaf;
+}
+
+/* style for file displays */
+table.files {
+  border-spacing: 0;
+  border-collapse: separate;
+  width: 100%;
+}
+
+table.files th.header{
+  padding-top: 10px;
+  border-bottom: 1px solid gray;
+  font-weight: bold;
+  background-color: white;
+  color: #707040;
+}
+
+table.files th {
+  border-bottom: 1px solid #afafaf;
+  font-weight: bold;
+  text-align: left;
+}
+
+table.files td {
+  font-family: monospace;
+  empty-cells: show;
+}
+
+/* style for history displays */
+table.history {
+  border-spacing: 0;
+  border-collapse: separate;
+  width: 100%;
+}
+
+table.history th.header{
+  padding-top: 10px;
+  border-bottom: 1px solid gray;
+  font-weight: bold;
+  background-color: white;
+  color: #707040;
+  font-size: 100%;
+}
+
+table.history th {
+  border-bottom: 1px solid #afafaf;
+  font-weight: bold;
+  text-align: left;
+  font-size: 90%;
+}
+
+table.history td {
+  font-size: 90%;
+  vertical-align: top;
+  empty-cells: show;
+}
+
+
+/* style for class list */
+table.classlist {
+  border-spacing: 0;
+  border-collapse: separate;
+  width: 100%;
+}
+
+table.classlist th.header{
+  padding-top: 10px;
+  border-bottom: 1px solid gray;
+  font-weight: bold;
+  background-color: white;
+  color: #707040;
+}
+
+table.classlist th {
+  font-weight: bold;
+  text-align: left;
+}
+
+
+/* style for class help display */
+table.classhelp {      /* the table-layout: fixed;        */ 
+  table-layout: fixed; /* compromises quality for speed   */
+  overflow: hidden;
+  font-size: .9em;
+  padding-bottom: 3em;
+}
+
+table.classhelp th {
+  font-weight: normal;
+  text-align: left;
+  color: #444;
+  background-color: #efefef;
+  border-bottom: 1px solid #afafaf;
+  border-top: 1px solid #afafaf;
+  text-transform: uppercase;
+  vertical-align: middle;
+  line-height:1.5em;
+}
+
+table.classhelp td {
+  vertical-align: middle;
+  padding-right: .2em;
+  border-bottom: 1px solid #efefef;
+  text-align: left;
+  empty-cells: show;
+  white-space: nowrap;
+  vertical-align: middle;
+}
+
+table.classhelp tr:hover {
+  background-color: #eee;
+}
+
+label.classhelp-label {
+  cursor: pointer;
+}
+
+#classhelp-controls {
+  position: fixed;
+  display: block;
+  top: auto;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  padding: .5em;
+  border-top: 2px solid #444;
+  background-color: #eee;
+}
+
+#classhelp-controls input.apply {
+  width: 7em;
+  font-weight: bold;
+  margin-right: 2em;
+  margin-left: 2em;
+}
+
+#classhelp-controls input.preview {
+   margin-right: 3em;
+   margin-left: 1em;
+}
+
+/* style for "other" displays */
+table.otherinfo {
+  border-spacing: 0;
+  border-collapse: separate;
+  width: 100%;
+}
+
+table.otherinfo th.header{
+  padding-top: 10px;
+  border-bottom: 1px solid gray;
+  font-weight: bold;
+  background-color: white;
+  color: #707040;
+}
+
+table.otherinfo th {
+  border-bottom: 1px solid #afafaf;
+  font-weight: bold;
+  text-align: left;
+}

Added: tracker/roundup-src/share/roundup/templates/minimal/html/user.index.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/minimal/html/user.index.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,35 @@
+<!-- dollarId: user.index,v 1.3 2002/07/09 05:29:51 richard Exp dollar-->
+<tal:block metal:use-macro="templates/page/macros/icing">
+<title metal:fill-slot="head_title" i18n:translate="">User listing - <span
+ i18n:name="tracker" tal:replace="config/TRACKER_NAME" /></title>
+<span metal:fill-slot="body_title" tal:omit-tag="python:1"
+ i18n:translate="">User listing</span>
+<td class="content" metal:fill-slot="content">
+
+<span tal:condition="python:not (context.is_view_ok()
+ or request.user.hasRole('Anonymous'))"
+ i18n:translate="">You are not allowed to view this page.</span>
+
+<span tal:condition="python:not context.is_view_ok()
+ and request.user.hasRole('Anonymous')"
+ i18n:translate="">Please login with your username and password.</span>
+
+<table width="100%" tal:condition="context/is_view_ok" class="list">
+<tr>
+ <th i18n:translate="">Username</th>
+ <th i18n:translate="">Email address</th>
+</tr>
+<tal:block repeat="user context/list">
+ <tr tal:condition="user/is_view_ok"
+    tal:attributes="class python:['normal', 'alt'][repeat['user'].index%6/3]">
+ <td>
+  <a tal:attributes="href string:user${user/id}"
+     tal:content="user/username">username</a>
+ </td>
+ <td tal:content="python:user.address.email()">address</td>
+ </tr>
+</tal:block>
+</table>
+</td>
+
+</tal:block>

Added: tracker/roundup-src/share/roundup/templates/minimal/html/user.item.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/minimal/html/user.item.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,169 @@
+<!-- dollarId: user.item,v 1.7 2002/08/16 04:29:04 richard Exp dollar-->
+<tal:doc metal:use-macro="templates/page/macros/icing"
+define="edit_ok context/is_edit_ok"
+>
+<title metal:fill-slot="head_title">
+<tal:if condition="context/id" i18n:translate=""
+ >User <tal:x content="context/id" i18n:name="id"
+ />: <tal:x content="context/username" i18n:name="title"
+ /> - <tal:x content="config/TRACKER_NAME" i18n:name="tracker"
+/></tal:if>
+<tal:if condition="not:context/id" i18n:translate=""
+ >New User - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker"
+/></tal:if>
+</title>
+<metal:slot fill-slot="more-javascript">
+<script metal:use-macro="templates/page/macros/user_utils"></script>
+<script type="text/javascript" src="@@file/help_controls.js"></script>
+</metal:slot>
+<tal:block metal:fill-slot="body_title"
+  define="edit_ok context/is_edit_ok">
+ <span tal:condition="python: not (context.id or edit_ok)"
+  tal:omit-tag="python:1" i18n:translate="">New User</span>
+ <span tal:condition="python: not context.id and edit_ok"
+  tal:omit-tag="python:1" i18n:translate="">New User Editing</span>
+ <span tal:condition="python: context.id and not edit_ok"
+  tal:omit-tag="python:1" i18n:translate="">User<tal:x
+  replace="context/id" i18n:name="id" /></span>
+ <span tal:condition="python: context.id and edit_ok"
+  tal:omit-tag="python:1" i18n:translate="">User<tal:x
+  replace="context/id" i18n:name="id" /> Editing</span>
+</tal:block>
+
+<td class="content" metal:fill-slot="content">
+
+<p tal:condition="python:not (context.is_view_ok()
+ or request.user.hasRole('Anonymous'))" i18n:translate="">
+ You are not allowed to view this page.</p>
+
+<p tal:condition="python:not context.is_view_ok()
+ and request.user.hasRole('Anonymous')" i18n:translate="">
+ Please login with your username and password.</p>
+
+<div tal:condition="context/is_view_ok">
+
+<form method="POST"
+      name="itemSynopsis"
+      tal:define="required python:'username address'.split()"
+      enctype="multipart/form-data"
+      tal:attributes="action context/designator;
+      onSubmit python:'return checkRequiredFields(\'%s\')'%'\', \''.join(required);
+      ">
+<table class="form" tal:define="
+  th_label templates/page/macros/th_label;
+  src_input templates/page/macros/user_src_input;
+  normal_input templates/page/macros/user_normal_input;
+  pw_input templates/page/macros/user_pw_input;
+  confirm_input templates/page/macros/user_confirm_input;
+  edit_ok context/is_edit_ok;
+  ">
+ <tr tal:define="name string:realname; label string:Name; value context/realname; edit_ok edit_ok">
+  <th metal:use-macro="th_label">Name</th>
+  <td><input name="realname" metal:use-macro="src_input"></td>
+ </tr>
+ <tr tal:define="name string:username; label string:Login Name; value context/username">
+   <th metal:use-macro="th_label">Login Name</th>
+   <td><input metal:use-macro="src_input"></td>
+ </tr>
+ <tal:if condition="edit_ok">
+ <tr tal:define="name string:password; label string:Login Password">
+  <th metal:use-macro="th_label">Login Password</th>
+  <td><input metal:use-macro="pw_input" type="password"></td>
+ </tr>
+ <tr tal:define="name string:password; label string:Confirm Password">
+  <th metal:use-macro="th_label">Confirm Password</th>
+  <td><input metal:use-macro="confirm_input" type="password"></td>
+ </tr>
+ </tal:if>
+ <tal:if condition="python:request.user.hasPermission('Web Roles')">
+ <tr tal:define="name string:roles; label string:Roles;">
+  <th><label for="roles" i18n:translate="">Roles</label></th>
+  <td tal:define="gips context/id">
+    <tal:subif condition=gips define="value context/roles">
+      <input metal:use-macro="normal_input">
+    </tal:subif>
+    <tal:subif condition="not:gips" define="value db/config/NEW_WEB_USER_ROLES">
+      <input metal:use-macro="normal_input">
+    </tal:subif>
+   <tal:block i18n:translate="">(to give the user more than one role,
+    enter a comma,separated,list)</tal:block>
+  </td>
+ </tr>
+ </tal:if>
+
+ <tr tal:define="name string:phone; label string:Phone; value context/phone">
+  <th metal:use-macro="th_label">Phone</th>
+  <td><input name="phone" metal:use-macro="normal_input"></td>
+ </tr>
+
+ <tr tal:define="name string:organisation; label string:Organisation; value context/organisation">
+  <th metal:use-macro="th_label">Organisation</th>
+  <td><input name="organisation" metal:use-macro="normal_input"></td>
+ </tr>
+
+ <tr tal:condition="python:edit_ok or context.timezone"
+     tal:define="name string:timezone; label string:Timezone; value context/timezone">
+  <th metal:use-macro="th_label">Timezone</th>
+  <td><input name="timezone" metal:use-macro="normal_input">
+   <tal:block tal:condition="edit_ok" i18n:translate="">(this is a numeric hour offset, the default is
+    <span tal:replace="db/config/DEFAULT_TIMEZONE" i18n:name="zone"
+    />)</tal:block>
+  </td>
+ </tr>
+
+ <tr tal:define="name string:address; label string:E-mail address; value context/address">
+  <th metal:use-macro="th_label">E-mail address</th>
+  <td tal:define="mailto python:context.address.field(id='address');
+	  mklink python:mailto and not edit_ok">
+      <a href="mailto:calvin at the-z.org"
+		  tal:attributes="href string:mailto:$value"
+		  tal:content="value"
+          tal:condition="python:mklink">calvin at the-z.org</a>
+      <tal:if condition=edit_ok>
+      <input metal:use-macro="src_input" value="calvin at the-z.org">
+      </tal:if>
+      &nbsp;
+  </td>
+ </tr>
+
+ <tr>
+  <th><label for="alternate_addresses" i18n:translate="">Alternate E-mail addresses<br>One address per line</label></th>
+  <td>
+    <textarea rows=5 cols=40 tal:replace="structure context/alternate_addresses/multiline">nobody at nowhere.org
+anybody at everywhere.net
+(alternate_addresses)
+    </textarea>
+  </td>
+ </tr>
+
+ <tr tal:condition="edit_ok">
+  <td>
+   &nbsp;
+   <input type="hidden" name="@template" value="item">
+   <input type="hidden" name="@required" value="username,address"
+          tal:attributes="value python:','.join(required)">
+  </td>
+  <td><input type="submit" value="save" tal:replace="structure context/submit"><!--submit button here-->
+    <input type=reset>
+  </td>
+ </tr>
+</table>
+</form>
+
+<tal:block tal:condition="not:context/id" i18n:translate="">
+<table class="form">
+<tr>
+ <td>Note:&nbsp;</td>
+ <th class="required">highlighted</th>
+ <td>&nbsp;fields are required.</td>
+</tr>
+</table>
+</tal:block>
+
+<tal:block tal:condition="context/id" tal:replace="structure context/history" />
+
+</div>
+
+</td>
+
+</tal:doc>

Added: tracker/roundup-src/share/roundup/templates/minimal/html/user.register.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/minimal/html/user.register.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,73 @@
+<!-- dollarId: user.item,v 1.7 2002/08/16 04:29:04 richard Exp dollar-->
+<tal:block metal:use-macro="templates/page/macros/icing">
+<title metal:fill-slot="head_title"
+ i18n:translate="">Registering with <span i18n:name="tracker"
+ tal:replace="db/config/TRACKER_NAME" /></title>
+<span metal:fill-slot="body_title" tal:omit-tag="python:1"
+ i18n:translate="">Registering with <span i18n:name="tracker"
+ tal:replace="db/config/TRACKER_NAME" /></span>
+<td class="content" metal:fill-slot="content">
+
+<tal:block tal:define=" editok python:request.user.username=='anonymous' and
+           request.user.hasPermission('Web Registration')">
+
+<span tal:condition="python:not (editok or request.user.hasRole('Anonymous'))"
+ i18n:translate="">You are not allowed to view this page.</span>
+
+<span tal:condition="python:not editok and request.user.hasRole('Anonymous')"
+ i18n:translate="">Please login with your username and password.</span>
+
+<tal:block tal:condition="editok">
+<form method="POST" onSubmit="return submit_once()" enctype="multipart/form-data">
+<input type="hidden" name=":template" value="register">
+<input type="hidden" name=":required" value="username">
+<input type="hidden" name=":required" value="password">
+<input type="hidden" name=":required" value="address">
+
+<table class="form">
+ <tr>
+  <th i18n:translate="">Login Name</th>
+  <td tal:content="structure context/username/field">username</td>
+ </tr>
+ <tr>
+  <th i18n:translate="">Login Password</th>
+  <td tal:content="structure context/password/field">password</td>
+ </tr>
+ <tr>
+  <th i18n:translate="">Confirm Password</th>
+  <td tal:content="structure context/password/confirm">password</td>
+ </tr>
+ <tr tal:condition="python:request.user.hasPermission('Web Roles')">
+  <th i18n:translate="">Roles</th>
+  <td tal:condition="exists:item"
+      tal:content="structure context/roles/field">roles</td>
+  <td tal:condition="not:exists:item">
+   <input name="roles" tal:attributes="value db/config/NEW_WEB_USER_ROLES">
+  </td>
+ </tr>
+ <tr>
+  <th i18n:translate="">E-mail address</th>
+  <td tal:content="structure context/address/field">address</td>
+ </tr>
+ <tr>
+  <th i18n:translate="">Alternate E-mail addresses<br>One address per line</th>
+  <td tal:content="structure context/alternate_addresses/multiline">alternate_addresses</td>
+ </tr>
+
+ <tr>
+  <td>&nbsp;</td>
+  <td>
+   <input type="hidden" name=":action" value="register">
+   <input type="submit" name="submit" value="Register" i18n:attributes="value">
+  </td>
+ </tr>
+</table>
+</form>
+
+</tal:block>
+
+</tal:block>
+
+</td>
+
+</tal:block>

Added: tracker/roundup-src/share/roundup/templates/minimal/html/user.rego_progress.html
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/minimal/html/user.rego_progress.html	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,16 @@
+<!-- dollarId: issue.index,v 1.2 2001/07/29 04:07:37 richard Exp dollar-->
+<tal:block metal:use-macro="templates/page/macros/icing">
+<title metal:fill-slot="head_title"
+ i18n:translate="">Registration in progress - <span i18n:name="tracker"
+ tal:replace="config/TRACKER_NAME" /></title>
+<span metal:fill-slot="body_title" tal:omit-tag="python:1"
+ i18n:translate="">Registration in progress...</span>
+<td class="content" metal:fill-slot="content">
+
+<p i18n:translate="">You will shortly receive an email
+to confirm your registration. To complete the registration process,
+visit the link indicated in the email.
+</p>
+
+</td>
+</tal:block>

Added: tracker/roundup-src/share/roundup/templates/minimal/initial_data.py
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/minimal/initial_data.py	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,14 @@
+#
+# TRACKER DATABASE INITIALIZATION
+#
+
+# create the two default users
+user = db.getclass('user')
+user.create(username="admin", password=adminpw,
+    address=admin_email, roles='Admin')
+user.create(username="anonymous", roles='Anonymous')
+
+# add any additional database creation steps here - but only if you
+# haven't initialised the database with the admin "initialise" command
+
+# vim: set et sts=4 sw=4 :

Added: tracker/roundup-src/share/roundup/templates/minimal/schema.py
==============================================================================
--- (empty file)
+++ tracker/roundup-src/share/roundup/templates/minimal/schema.py	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,65 @@
+#
+# TRACKER SCHEMA
+#
+
+# Class automatically gets these properties:
+#   creation = Date()
+#   activity = Date()
+#   creator = Link('user')
+#   actor = Link('user')
+
+# The "Minimal" template gets only one class, the required "user"
+# class. That's it. And even that has the bare minimum of properties.
+
+# Note: roles is a comma-separated string of Role names
+user = Class(db, "user", username=String(), password=Password(),
+    address=String(), alternate_addresses=String(), roles=String())
+user.setkey("username")
+#
+# TRACKER SECURITY SETTINGS
+#
+# See the configuration and customisation document for information
+# about security setup.
+
+#
+# REGULAR USERS
+#
+# Give the regular users access to the web and email interface
+db.security.addPermissionToRole('User', 'Web Access')
+db.security.addPermissionToRole('User', 'Email Access')
+
+# May users view other user information?
+# Comment these lines out if you don't want them to
+db.security.addPermissionToRole('User', 'View', 'user')
+
+# Users should be able to edit their own details -- this permission is
+# limited to only the situation where the Viewed or Edited item is their own.
+def own_record(db, userid, itemid):
+    '''Determine whether the userid matches the item being accessed.'''
+    return userid == itemid
+p = db.security.addPermission(name='View', klass='user', check=own_record,
+    description="User is allowed to view their own user details")
+db.security.addPermissionToRole('User', p)
+p = db.security.addPermission(name='Edit', klass='user', check=own_record,
+    description="User is allowed to edit their own user details")
+db.security.addPermissionToRole('User', p)
+
+#
+# ANONYMOUS USER PERMISSIONS
+#
+# Let anonymous users access the web interface. Note that almost all
+# trackers will need this Permission. The only situation where it's not
+# required is in a tracker that uses an HTTP Basic Authenticated front-end.
+db.security.addPermissionToRole('Anonymous', 'Web Access')
+
+# Let anonymous users access the email interface (note that this implies
+# that they will be registered automatically, hence they will need the
+# "Create" user Permission below)
+db.security.addPermissionToRole('Anonymous', 'Email Access')
+
+# Assign the appropriate permissions to the anonymous user's
+# Anonymous Role. Choices here are:
+# - Allow anonymous users to register
+db.security.addPermissionToRole('Anonymous', 'Create', 'user')
+
+# vim: set et sts=4 sw=4 :

Modified: tracker/roundup-src/test/README.txt
==============================================================================
--- tracker/roundup-src/test/README.txt	(original)
+++ tracker/roundup-src/test/README.txt	Sun Mar 15 22:43:30 2009
@@ -1,4 +1,4 @@
-$Id: README.txt,v 1.3 2004/10/24 08:37:58 a1s Exp $
+$Id: README.txt,v 1.3 2004-10-24 08:37:58 a1s Exp $
 
 Structure of the tests:
 

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 15 22:43:30 2009
@@ -15,9 +15,11 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-# $Id: db_test_base.py,v 1.96 2008/02/07 03:28:34 richard Exp $
+# $Id: db_test_base.py,v 1.101 2008-08-19 01:40:59 richard Exp $
 
-import unittest, os, shutil, errno, imp, sys, time, pprint, sets, base64, os.path
+import unittest, os, shutil, errno, imp, sys, time, pprint, base64, os.path
+# Python 2.3 ... 2.6 compatibility:
+from roundup.anypy.sets_ import set
 
 from roundup.hyperdb import String, Password, Link, Multilink, Date, \
     Interval, DatabaseError, Boolean, Number, Node
@@ -55,13 +57,18 @@
     except OSError, error:
         if error.errno not in (errno.ENOENT, errno.ESRCH): raise
     # create the instance
-    init.install(dirname, os.path.join(os.path.dirname(__file__), '..',
-        'templates/classic'))
+    init.install(dirname, os.path.join(os.path.dirname(__file__),
+                                       '..',
+                                       'share',
+                                       'roundup',
+                                       'templates',
+                                       'classic'))
     init.write_select_db(dirname, backend)
     config.save(os.path.join(dirname, 'config.ini'))
     tracker = instance.open(dirname)
     if tracker.exists():
         tracker.nuke()
+        init.write_select_db(dirname, backend)
     tracker.init(password.Password('sekrit'))
     return tracker
 
@@ -80,7 +87,8 @@
     issue = module.IssueClass(db, "issue", title=String(indexme="yes"),
         status=Link("status"), nosy=Multilink("user"), deadline=Date(),
         foo=Interval(), files=Multilink("file"), assignedto=Link('user'),
-        priority=Link('priority'), spam=Multilink('msg'))
+        priority=Link('priority'), spam=Multilink('msg'),
+        feedback=Link('msg'))
     stuff = module.Class(db, "stuff", stuff=String())
     session = module.Class(db, 'session', title=String())
     msg = module.FileClass(db, "msg", date=Date(),
@@ -288,12 +296,12 @@
             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]))
+            self.db.issue.set(nid, nosy=set([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):
@@ -329,6 +337,19 @@
                 c = self.db.issue.get(nid, "deadline")
                 self.assertEqual(c, d)
 
+    def testDateLeapYear(self):
+        nid = self.db.issue.create(title='spam', status='1',
+            deadline=date.Date('2008-02-29'))
+        self.assertEquals(str(self.db.issue.get(nid, 'deadline')),
+            '2008-02-29.00:00:00')
+        self.assertEquals(self.db.issue.filter(None,
+            {'deadline': '2008-02-29'}), [nid])
+        self.db.issue.set(nid, deadline=date.Date('2008-03-01'))
+        self.assertEquals(str(self.db.issue.get(nid, 'deadline')),
+            '2008-03-01.00:00:00')
+        self.assertEquals(self.db.issue.filter(None,
+            {'deadline': '2008-02-29'}), [])
+
     def testDateUnset(self):
         for commit in (0,1):
             nid = self.db.issue.create(title="spam", status='1')
@@ -468,12 +489,12 @@
         others = nodeids[:]
         others.remove('1')
 
-        self.assertEqual(sets.Set(self.db.status.getnodeids()),
-            sets.Set(nodeids))
-        self.assertEqual(sets.Set(self.db.status.getnodeids(retired=True)),
-            sets.Set(['1']))
-        self.assertEqual(sets.Set(self.db.status.getnodeids(retired=False)),
-            sets.Set(others))
+        self.assertEqual(set(self.db.status.getnodeids()),
+            set(nodeids))
+        self.assertEqual(set(self.db.status.getnodeids(retired=True)),
+            set(['1']))
+        self.assertEqual(set(self.db.status.getnodeids(retired=False)),
+            set(others))
 
         self.assert_(self.db.status.is_retired('1'))
 
@@ -844,6 +865,15 @@
         # unindexed stopword
         self.assertEquals(self.db.indexer.search(['the'], self.db.issue), {})
 
+    def testIndexerSearchingLink(self):
+        m1 = self.db.msg.create(content="one two")
+        i1 = self.db.issue.create(messages=[m1])
+        m2 = self.db.msg.create(content="two three")
+        i2 = self.db.issue.create(feedback=m2)
+        self.db.commit()
+        self.assertEquals(self.db.indexer.search(['two'], self.db.issue),
+            {i1: {'messages': [m1]}, i2: {'feedback': [m2]}})
+
     def testIndexerSearchMulti(self):
         m1 = self.db.msg.create(content="one two")
         m2 = self.db.msg.create(content="two three")
@@ -1711,7 +1741,7 @@
         keys = props.keys()
         keys.sort()
         self.assertEqual(keys, ['activity', 'actor', 'assignedto', 'creation',
-            'creator', 'deadline', 'files', 'fixer', 'foo', 'id', 'messages',
+            'creator', 'deadline', 'feedback', 'files', 'fixer', 'foo', 'id', 'messages',
             'nosy', 'priority', 'spam', 'status', 'superseder', 'title'])
         self.assertEqual(self.db.issue.get('1', "fixer"), None)
 
@@ -1725,7 +1755,7 @@
         keys = props.keys()
         keys.sort()
         self.assertEqual(keys, ['activity', 'actor', 'assignedto', 'creation',
-            'creator', 'deadline', 'files', 'foo', 'id', 'messages',
+            'creator', 'deadline', 'feedback', 'files', 'foo', 'id', 'messages',
             'nosy', 'priority', 'spam', 'status', 'superseder'])
         self.assertEqual(self.db.issue.list(), ['1'])
 
@@ -1740,8 +1770,8 @@
         keys = props.keys()
         keys.sort()
         self.assertEqual(keys, ['activity', 'actor', 'assignedto', 'creation',
-            'creator', 'deadline', 'files', 'fixer', 'foo', 'id', 'messages',
-            'nosy', 'priority', 'spam', 'status', 'superseder'])
+            'creator', 'deadline', 'feedback', 'files', 'fixer', 'foo', 'id',
+            'messages', 'nosy', 'priority', 'spam', 'status', 'superseder'])
         self.assertEqual(self.db.issue.list(), ['1'])
 
     def testNosyMail(self) :
@@ -1763,16 +1793,16 @@
                 messages = [m], nosy = [db.user.lookup("fred")])
 
             db.issue.nosymessage(i, m, {})
-            mail_msg = res["mail_msg"].getvalue()
+            mail_msg = str(res["mail_msg"])
             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(base64.encodestring("xxx").rstrip() in mail_msg)
             self.failUnless("File 'test2.txt' not attached" in mail_msg)
-            self.failIf(base64.b64encode("yyy") in mail_msg)
+            self.failIf(base64.encodestring("yyy").rstrip() in mail_msg)
         finally :
             Mailer.smtp_send = backup
 
@@ -2026,7 +2056,7 @@
         self.db.getjournal('a', aid)
 
 class RDBMSTest:
-    ''' tests specific to RDBMS backends '''
+    """ tests specific to RDBMS backends """
     def test_indexTest(self):
         self.assertEqual(self.db.sql_index_exists('_issue', '_issue_id_idx'), 1)
         self.assertEqual(self.db.sql_index_exists('_issue', '_issue_x_idx'), 0)

Modified: tracker/roundup-src/test/test_anydbm.py
==============================================================================
--- tracker/roundup-src/test/test_anydbm.py	(original)
+++ tracker/roundup-src/test/test_anydbm.py	Sun Mar 15 22:43:30 2009
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: test_anydbm.py,v 1.4 2004/11/03 01:34:21 richard Exp $ 
+# $Id: test_anydbm.py,v 1.4 2004-11-03 01:34:21 richard Exp $ 
 
 import unittest, os, shutil, time
 from roundup.backends import get_backend

Added: tracker/roundup-src/test/test_anypy_hashlib.py
==============================================================================
--- (empty file)
+++ tracker/roundup-src/test/test_anypy_hashlib.py	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,139 @@
+#! /usr/bin/env python
+import unittest
+import warnings
+
+import roundup.anypy.hashlib_
+
+class UntestableWarning(Warning):
+    pass
+
+# suppress deprecation warnings; -> warnings.filters[0]:
+warnings.simplefilter(action='ignore',
+                      category=DeprecationWarning,
+                      append=0)
+
+try:
+    import sha
+except:
+    warnings.warn('sha module functions', UntestableWarning)
+    sha = None
+
+try:
+    import md5
+except:
+    warnings.warn('md5 module functions', UntestableWarning)
+    md5 = None
+
+try:
+    import hashlib
+except:
+    warnings.warn('hashlib module functions', UntestableWarning)
+    hashlib = None
+
+# preserve other warning filters set elsewhere:
+del warnings.filters[0]
+
+if not ((sha or md5) and hashlib):
+    warnings.warn('anypy.hashlib_ continuity', UntestableWarning)
+
+class TestCase_anypy_hashlib(unittest.TestCase):
+    """test the hashlib compatibility layer"""
+
+    testdata = (
+           ('',
+            'da39a3ee5e6b4b0d3255bfef95601890afd80709',
+            'd41d8cd98f00b204e9800998ecf8427e'),
+           ('Strange women lying in ponds distributing swords'
+            ' is no basis for a system of government.',
+            'da9b2b00466b00411038c057681fe67349f92d7d',
+            'b71c5178d316ec446c25386f4857d4f9'),
+           ('Ottos Mops hopst fort',
+            'fdf7e6c54cf07108c86edd8d47c90450671c2c81',
+            'a3dce74bee59ee92f1038263e5252500'),
+           ('Dieser Satz kein Verb',
+            '3030aded8a079b92043a39dc044a35443959dcdd',
+            '2f20c69d514228011fb0d32e14dd5d80'),
+           )
+
+    # the following two are always excecuted: 
+    def test_sha1_expected_anypy(self):
+        """...anypy.hashlib_.sha1().hexdigest() yields expected results"""
+        for src, SHA, MD5 in self.testdata:
+            self.assertEqual(roundup.anypy.hashlib_.sha1(src).hexdigest(), SHA)
+
+    def test_md5_expected_anypy(self):
+        """...anypy.hashlib_.md5().hexdigest() yields expected results"""
+        for src, SHA, MD5 in self.testdata:
+            self.assertEqual(roundup.anypy.hashlib_.md5(src).hexdigest(), MD5)
+
+    # execution depending on availability of modules: 
+    if md5 and hashlib:
+        def test_md5_continuity(self):
+            """md5.md5().digest() == hashlib.md5().digest()"""
+            if md5.md5 is hashlib.md5:
+                return
+            else:
+                for s, i1, i2 in self.testdata:
+                    self.assertEqual(md5.md5(s).digest(),
+                                     hashlib.md5().digest())
+
+    if md5:
+        def test_md5_expected(self):
+            """md5.md5().hexdigest() yields expected results"""
+            for src, SHA, MD5 in self.testdata:
+                self.assertEqual(md5.md5(src).hexdigest(), MD5)
+
+        def test_md5_new_expected(self):
+            """md5.new is md5.md5, or at least yields expected results"""
+            if md5.new is md5.md5:
+                return
+            else:
+                for src, SHA, MD5 in self.testdata:
+                    self.assertEqual(md5.new(src).hexdigest(), MD5)
+
+    if sha and hashlib:
+        def test_sha1_continuity(self):
+            """sha.sha().digest() == hashlib.sha1().digest()"""
+            if sha.sha is hashlib.sha1:
+                return
+            else:
+                for s in self.testdata:
+                    self.assertEqual(sha.sha(s).digest(),
+                                     hashlib.sha1().digest())
+
+    if sha:
+        def test_sha_expected(self):
+            """sha.sha().hexdigest() yields expected results"""
+            for src, SHA, MD5 in self.testdata:
+                self.assertEqual(sha.sha(src).hexdigest(), SHA)
+
+        # fails for me with Python 2.3; unittest module bug?
+        def test_sha_new_expected(self):
+            """sha.new is sha.sha, or at least yields expected results"""
+            if sha.new is sha.sha:
+                return
+            else:
+                for src, SHA, MD5 in self.testdata:
+                    self.assertEqual(sha.new(src).hexdigest(), SHA)
+
+    if hashlib:
+        def test_sha1_expected_hashlib(self):
+            """hashlib.sha1().hexdigest() yields expected results"""
+            for src, SHA, MD5 in self.testdata:
+                self.assertEqual(hashlib.sha1(src).hexdigest(), SHA)
+
+        def test_md5_expected_hashlib(self):
+            """hashlib.md5().hexdigest() yields expected results"""
+            for src, SHA, MD5 in self.testdata:
+                self.assertEqual(hashlib.md5(src).hexdigest(), MD5)
+
+def test_suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(TestCase_anypy_hashlib))
+    return suite
+
+if __name__ == '__main__':
+    runner = unittest.TextTestRunner()
+    unittest.main(testRunner=runner)
+
+# vim: ts=8 et sts=4 sw=4 si

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 15 22:43:30 2009
@@ -8,7 +8,7 @@
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 #
-# $Id: test_cgi.py,v 1.33 2007/10/05 03:07:14 richard Exp $
+# $Id: test_cgi.py,v 1.36 2008-08-07 06:12:57 richard Exp $
 
 import unittest, os, shutil, errno, sys, difflib, cgi, re
 
@@ -77,7 +77,7 @@
             string=hyperdb.String(), number=hyperdb.Number(),
             boolean=hyperdb.Boolean(), link=hyperdb.Link('test'),
             multilink=hyperdb.Multilink('test'), date=hyperdb.Date(),
-            interval=hyperdb.Interval())
+            messages=hyperdb.Multilink('msg'), interval=hyperdb.Interval())
 
         # compile the labels re
         classes = '|'.join(self.db.classes.keys())
@@ -85,10 +85,11 @@
             re.VERBOSE)
 
     def parseForm(self, form, classname='test', nodeid=None):
-        cl = client.Client(self.instance, None, {'PATH_INFO':'/'},
-            makeForm(form))
+        cl = client.Client(self.instance, None, {'PATH_INFO':'/',
+            'REQUEST_METHOD':'POST'}, makeForm(form))
         cl.classname = classname
         cl.nodeid = nodeid
+        cl.language = ('en',)
         cl.db = self.db
         return cl.parsePropsFromForm(create=1)
 
@@ -204,6 +205,7 @@
         cl.classname = 'issue'
         cl.nodeid = issue
         cl.db = self.db
+        cl.language = ('en',)
         item = HTMLItem(cl, 'issue', issue)
         self.assertEqual(item.status.id, '1')
         self.assertEqual(item.status.name, '2')
@@ -222,6 +224,7 @@
         cl.classname = 'issue'
         cl.nodeid = issue
         cl.db = self.db
+        cl.language = ('en',)
         cl.userid = '1'
         item = HTMLItem(cl, 'issue', issue)
         for keyword in item.keyword:
@@ -304,6 +307,7 @@
         cl.classname = 'issue'
         cl.nodeid = None
         cl.db = self.db
+        cl.language = ('en',)
         self.assertEqual(cl.parsePropsFromForm(create=1),
             ({('issue', None): {'nosy': ['1','2', '3']}}, []))
 
@@ -564,13 +568,24 @@
             }),
             ({('test', None): {'string': 'a'},
               ('issue', '-1'): {'nosy': ['1']},
-              ('issue', '-2'): {}
              },
              [('issue', '-2', 'superseder', [('issue', '-1')])
              ]
             )
         )
 
+    def testMessages(self):
+        self.assertEqual(self.parseForm({
+            'msg-1 at content': 'asdf',
+            'msg-2 at content': 'qwer',
+            '@link at messages': 'msg-1, msg-2'}),
+            ({('test', None): {},
+              ('msg', '-2'): {'content': 'qwer'},
+              ('msg', '-1'): {'content': 'asdf'}},
+             [('test', None, 'messages', [('msg', '-1'), ('msg', '-2')])]
+            )
+        )
+
     def testLinkBadDesignator(self):
         self.assertRaises(FormError, self.parseForm,
             {'test-1 at link@link': 'blah'})
@@ -600,12 +615,13 @@
     #
     # 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 = client.Client(self.instance, None, {'PATH_INFO':'/',
+            'REQUEST_METHOD':'POST'}, makeForm(form))
         cl.classname = 'user'
         cl.nodeid = '1'
         cl.db = self.db
         cl.userid = '2'
+        cl.language = ('en',)
         return cl
 
     def testClassPermission(self):

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 15 22:43:30 2009
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-# $Id: test_dates.py,v 1.44 2007/12/23 00:23:23 richard Exp $
+# $Id: test_dates.py,v 1.45 2008-03-07 01:11:55 richard Exp $
 from __future__ import nested_scopes
 
 import unittest
@@ -37,7 +37,9 @@
         ae(str(date), '2000-02-29.00:00:00')
         date = Date("2001-02-27 + 2d")
         ae(str(date), '2001-03-01.00:00:00')
-
+        date = Date("2009", add_granularity=True)
+        self.assertRaises(ValueError, Date, ". +30d", add_granularity=True)
+        
     def testDate(self):
         ae = self.assertEqual
         date = Date("2000-04-17")
@@ -68,6 +70,9 @@
         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 testLeapYear(self):
+        self.assertEquals(str(Date('2008-02-29')), '2008-02-29.00:00:00')
+
     def testDateError(self):
         self.assertRaises(ValueError, Date, "12")
         # Date cannot handle dates before year 1

Modified: tracker/roundup-src/test/test_hyperdbvals.py
==============================================================================
--- tracker/roundup-src/test/test_hyperdbvals.py	(original)
+++ tracker/roundup-src/test/test_hyperdbvals.py	Sun Mar 15 22:43:30 2009
@@ -8,9 +8,10 @@
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 #
-# $Id: test_hyperdbvals.py,v 1.3 2006/08/18 01:26:19 richard Exp $
+# $Id: test_hyperdbvals.py,v 1.3 2006-08-18 01:26:19 richard Exp $
 
-import unittest, os, shutil, errno, sys, difflib, cgi, re, sha
+import unittest, os, shutil, errno, sys, difflib, cgi, re
+from roundup.anypy.hashlib_ import sha1
 
 from roundup import init, instance, password, hyperdb, date
 
@@ -80,7 +81,7 @@
         self.assert_(isinstance(val, password.Password))
         val = self._test('password', '{crypt}a string')
         self.assert_(isinstance(val, password.Password))
-        s = sha.sha('a string').hexdigest()
+        s = sha1('a string').hexdigest()
         val = self._test('password', '{SHA}'+s)
         self.assert_(isinstance(val, password.Password))
         self.assertEqual(val, 'a string')

Modified: tracker/roundup-src/test/test_indexer.py
==============================================================================
--- tracker/roundup-src/test/test_indexer.py	(original)
+++ tracker/roundup-src/test/test_indexer.py	Sun Mar 15 22:43:30 2009
@@ -18,10 +18,19 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
-# $Id: test_indexer.py,v 1.10 2006/02/07 04:59:05 richard Exp $
+# $Id: test_indexer.py,v 1.13 2008-09-11 19:10:30 schlatterbeck Exp $
 
 import os, unittest, shutil
 
+from roundup.backends import get_backend, have_backend
+from roundup.backends.indexer_rdbms import Indexer
+
+# borrow from other tests
+from db_test_base import setupSchema, config
+from test_postgresql import postgresqlOpener
+from test_mysql import mysqlOpener
+from test_sqlite import sqliteOpener
+
 class db:
     class config(dict):
         DATABASE = 'test-index'
@@ -38,29 +47,40 @@
         self.dex = Indexer(db)
         self.dex.load_index()
 
+    def assertSeqEqual(self, s1, s2):
+        # First argument is the db result we're testing, second is the
+        # desired result. Some db results don't have iterable rows, so we
+        # have to work around that.
+        # Also work around some dbs not returning items in the expected
+        # order.
+        s1 = list([tuple([r[n] for n in range(len(r))]) for r in s1])
+        s1.sort()
+        if s1 != s2:
+            self.fail('contents of %r != %r'%(s1, s2))
+
     def test_basics(self):
         self.dex.add_text(('test', '1', 'foo'), 'a the hello world')
         self.dex.add_text(('test', '2', 'foo'), 'blah blah the world')
-        self.assertEqual(self.dex.find(['world']), [('test', '1', 'foo'),
+        self.assertSeqEqual(self.dex.find(['world']), [('test', '1', 'foo'),
                                                     ('test', '2', 'foo')])
-        self.assertEqual(self.dex.find(['blah']), [('test', '2', 'foo')])
-        self.assertEqual(self.dex.find(['blah', 'hello']), [])
+        self.assertSeqEqual(self.dex.find(['blah']), [('test', '2', 'foo')])
+        self.assertSeqEqual(self.dex.find(['blah', 'hello']), [])
 
     def test_change(self):
         self.dex.add_text(('test', '1', 'foo'), 'a the hello world')
         self.dex.add_text(('test', '2', 'foo'), 'blah blah the world')
-        self.assertEqual(self.dex.find(['world']), [('test', '1', 'foo'),
+        self.assertSeqEqual(self.dex.find(['world']), [('test', '1', 'foo'),
                                                     ('test', '2', 'foo')])
         self.dex.add_text(('test', '1', 'foo'), 'a the hello')
-        self.assertEqual(self.dex.find(['world']), [('test', '2', 'foo')])
+        self.assertSeqEqual(self.dex.find(['world']), [('test', '2', 'foo')])
 
     def test_clear(self):
         self.dex.add_text(('test', '1', 'foo'), 'a the hello world')
         self.dex.add_text(('test', '2', 'foo'), 'blah blah the world')
-        self.assertEqual(self.dex.find(['world']), [('test', '1', 'foo'),
+        self.assertSeqEqual(self.dex.find(['world']), [('test', '1', 'foo'),
                                                     ('test', '2', 'foo')])
         self.dex.add_text(('test', '1', 'foo'), '')
-        self.assertEqual(self.dex.find(['world']), [('test', '2', 'foo')])
+        self.assertSeqEqual(self.dex.find(['world']), [('test', '2', 'foo')])
 
     def tearDown(self):
         shutil.rmtree('test-index')
@@ -75,15 +95,74 @@
     def tearDown(self):
         shutil.rmtree('test-index')
 
+class RDBMSIndexerTest(IndexerTest):
+    def setUp(self):
+        # remove previous test, ignore errors
+        if os.path.exists(config.DATABASE):
+            shutil.rmtree(config.DATABASE)
+        self.db = self.module.Database(config, 'admin')
+        self.dex = Indexer(self.db)
+    def tearDown(self):
+        if hasattr(self, 'db'):
+            self.db.close()
+        if os.path.exists(config.DATABASE):
+            shutil.rmtree(config.DATABASE)
+
+class postgresqlIndexerTest(postgresqlOpener, RDBMSIndexerTest):
+    def setUp(self):
+        postgresqlOpener.setUp(self)
+        RDBMSIndexerTest.setUp(self)
+    def tearDown(self):
+        RDBMSIndexerTest.tearDown(self)
+        postgresqlOpener.tearDown(self)
+
+class mysqlIndexerTest(mysqlOpener, RDBMSIndexerTest):
+    def setUp(self):
+        mysqlOpener.setUp(self)
+        RDBMSIndexerTest.setUp(self)
+    def tearDown(self):
+        RDBMSIndexerTest.tearDown(self)
+        mysqlOpener.tearDown(self)
+
+class sqliteIndexerTest(sqliteOpener, RDBMSIndexerTest):
+    pass
+
 def test_suite():
     suite = unittest.TestSuite()
+
     suite.addTest(unittest.makeSuite(IndexerTest))
+
     try:
         import xapian
         suite.addTest(unittest.makeSuite(XapianIndexerTest))
     except ImportError:
         print "Skipping Xapian indexer tests"
         pass
+
+    if have_backend('postgresql'):
+        # make sure we start with a clean slate
+        if postgresqlOpener.module.db_exists(config):
+            postgresqlOpener.module.db_nuke(config, 1)
+        suite.addTest(unittest.makeSuite(postgresqlIndexerTest))
+    else:
+        print "Skipping postgresql indexer tests"
+
+    if have_backend('mysql'):
+        # make sure we start with a clean slate
+        if mysqlOpener.module.db_exists(config):
+            mysqlOpener.module.db_nuke(config)
+        suite.addTest(unittest.makeSuite(mysqlIndexerTest))
+    else:
+        print "Skipping mysql indexer tests"
+
+    if have_backend('sqlite'):
+        # make sure we start with a clean slate
+        if sqliteOpener.module.db_exists(config):
+            sqliteOpener.module.db_nuke(config)
+        suite.addTest(unittest.makeSuite(sqliteIndexerTest))
+    else:
+        print "Skipping sqlite indexer tests"
+
     return suite
 
 if __name__ == '__main__':

Modified: tracker/roundup-src/test/test_locking.py
==============================================================================
--- tracker/roundup-src/test/test_locking.py	(original)
+++ tracker/roundup-src/test/test_locking.py	Sun Mar 15 22:43:30 2009
@@ -18,7 +18,7 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
-# $Id: test_locking.py,v 1.4 2003/10/25 22:53:26 richard Exp $
+# $Id: test_locking.py,v 1.4 2003-10-25 22:53:26 richard Exp $
 
 import os, unittest, tempfile
 

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 15 22:43:30 2009
@@ -1,3 +1,4 @@
+# -*- encoding: utf-8 -*-
 #
 # Copyright (c) 2001 Richard Jones, richard at bofh.asn.au.
 # This module is free software, and you may redistribute it and/or modify
@@ -8,7 +9,7 @@
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 #
-# $Id: test_mailgw.py,v 1.93 2008/02/07 03:55:14 richard Exp $
+# $Id: test_mailgw.py,v 1.96 2008-08-19 01:40:59 richard Exp $
 
 # TODO: test bcc
 
@@ -23,6 +24,7 @@
 from roundup.mailgw import MailGW, Unauthorized, uidFromAddress, \
     parseContent, IgnoreLoop, IgnoreBulk, MailUsageError, MailUsageHelp
 from roundup import init, instance, password, rfc2822, __version__
+from roundup.anypy.sets_ import set
 
 import db_test_base
 
@@ -39,19 +41,27 @@
     def compareMessages(self, new, old):
         """Compare messages for semantic equivalence."""
         new, old = Message(new), Message(old)
+
+        # all Roundup-generated messages have "Precedence: bulk"
+        old['Precedence'] = 'bulk'
+
+        # don't try to compare the date
         del new['date'], old['date']
 
         if not new == old:
             res = []
 
             for key in new.keys():
+                if key.startswith('from '):
+                    # skip the unix from line
+                    continue
                 if key.lower() == 'x-roundup-version':
                     # version changes constantly, so handle it specially
                     if new[key] != __version__:
-                        res.append('  %s: %s != %s' % (key, __version__,
+                        res.append('  %s: %r != %r' % (key, __version__,
                             new[key]))
                 elif new.get(key, '') != old.get(key, ''):
-                    res.append('  %s: %s != %s' % (key, old.get(key, ''),
+                    res.append('  %s: %r != %r' % (key, old.get(key, ''),
                         new.get(key, '')))
 
             body_diff = self.compareStrings(new.fp.read(), old.fp.read())
@@ -230,7 +240,7 @@
         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
+Content-Type: text/plain; charset="utf-8"
 Subject: [issue1] Testing...
 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>
@@ -274,7 +284,7 @@
         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
+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>
@@ -315,7 +325,7 @@
         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
+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>
@@ -344,9 +354,7 @@
 _______________________________________________________________________
 ''')
 
-    multipart_msg = '''Content-Type: text/plain;
-  charset="iso-8859-1"
-From: mary <mary at test.test>
+    multipart_msg = '''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>
@@ -460,7 +468,7 @@
         self.compareMessages(self._get_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
+Content-Type: text/plain; charset="utf-8"
 Subject: [issue1] Testing...
 To: chef at bork.bork.bork, richard at test.test
 From: "Contrary, Mary" <issue_tracker at your.tracker.email.domain.example>
@@ -508,7 +516,7 @@
         self.compareMessages(self._get_mail(),
 '''FROM: roundup-admin at your.tracker.email.domain.example
 TO: chef at bork.bork.bork, john at test.test, mary at test.test
-Content-Type: text/plain; charset=utf-8
+Content-Type: text/plain; charset="utf-8"
 Subject: [issue1] Testing...
 To: chef at bork.bork.bork, john at test.test, mary at test.test
 From: richard <issue_tracker at your.tracker.email.domain.example>
@@ -556,7 +564,7 @@
         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
+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>
@@ -569,7 +577,7 @@
 Content-Transfer-Encoding: quoted-printable
 
 
-Changes by Bork, Chef <chef at bork.bork.bork>:
+Change by Bork, Chef <chef at bork.bork.bork>:
 
 
 ----------
@@ -599,7 +607,7 @@
         self.compareMessages(self._get_mail(),
 '''FROM: roundup-admin at your.tracker.email.domain.example
 TO: chef at bork.bork.bork, john at test.test, mary at test.test
-Content-Type: text/plain; charset=utf-8
+Content-Type: text/plain; charset="utf-8"
 Subject: [issue1] Testing...
 To: chef at bork.bork.bork, john at test.test, mary at test.test
 From: richard <issue_tracker at your.tracker.email.domain.example>
@@ -679,7 +687,10 @@
 
 This is a followup
 '''), nodeid)
-        # now try a longer interval
+
+
+    def testFollowupTitleMatchInterval(self):
+        nodeid = self.doNewIssue()
         self.db.config.MAILGW_SUBJECT_CONTENT_MATCH = 'creation +1d'
         self.assertEqual(self._handle_mail('''Content-Type: text/plain;
   charset="iso-8859-1"
@@ -709,7 +720,7 @@
         self.compareMessages(self._get_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
+Content-Type: text/plain; charset="utf-8"
 Subject: [issue1] Testing...
 To: chef at bork.bork.bork, richard at test.test
 From: John Doe <issue_tracker at your.tracker.email.domain.example>
@@ -755,7 +766,7 @@
         self.compareMessages(self._get_mail(),
 '''FROM: roundup-admin at your.tracker.email.domain.example
 TO: chef at bork.bork.bork
-Content-Type: text/plain; charset=utf-8
+Content-Type: text/plain; charset="utf-8"
 Subject: [issue1] Testing...
 To: chef at bork.bork.bork
 From: richard <issue_tracker at your.tracker.email.domain.example>
@@ -801,7 +812,7 @@
         self.compareMessages(self._get_mail(),
 '''FROM: roundup-admin at your.tracker.email.domain.example
 TO: chef at bork.bork.bork, john at test.test, richard at test.test
-Content-Type: text/plain; charset=utf-8
+Content-Type: text/plain; charset="utf-8"
 Subject: [issue1] Testing...
 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>
@@ -846,7 +857,7 @@
         self.compareMessages(self._get_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
+Content-Type: text/plain; charset="utf-8"
 Subject: [issue1] Testing...
 To: chef at bork.bork.bork, richard at test.test
 From: John Doe <issue_tracker at your.tracker.email.domain.example>
@@ -891,7 +902,7 @@
         self.compareMessages(self._get_mail(),
 '''FROM: roundup-admin at your.tracker.email.domain.example
 TO: chef at bork.bork.bork
-Content-Type: text/plain; charset=utf-8
+Content-Type: text/plain; charset="utf-8"
 Subject: [issue1] Testing...
 To: chef at bork.bork.bork
 From: richard <issue_tracker at your.tracker.email.domain.example>
@@ -1053,6 +1064,29 @@
         m.sort()
         self.assertNotEqual(l, m)
 
+    def testNewUserAuthorHighBit(self):
+        l = set(self.db.user.list())
+        # From: name has Euro symbol in it
+        message = '''Content-Type: text/plain;
+  charset="iso-8859-1"
+From: =?utf8?b?SOKCrGxsbw==?= <fubar at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Message-Id: <dummy_test_message_id>
+Subject: [issue] Testing...
+
+This is a test submission of a new issue.
+'''
+        p = [
+            self.db.security.getPermission('Create', 'user'),
+            self.db.security.getPermission('Email Access', None),
+        ]
+        self.db.security.role['anonymous'].permissions=p
+        self._handle_mail(message)
+        m = set(self.db.user.list())
+        new = list(m - l)[0]
+        name = self.db.user.get(new, 'realname')
+        self.assertEquals(name, 'H€llo')
+
     def testEnc01(self):
         self.doNewIssue()
         self._handle_mail('''Content-Type: text/plain;
@@ -1072,7 +1106,7 @@
         self.compareMessages(self._get_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
+Content-Type: text/plain; charset="utf-8"
 Subject: [issue1] Testing...
 To: chef at bork.bork.bork, richard at test.test
 From: "Contrary, Mary" <issue_tracker at your.tracker.email.domain.example>
@@ -1099,6 +1133,53 @@
 _______________________________________________________________________
 ''')
 
+    def testEncNonUTF8(self):
+        self.doNewIssue()
+        self.instance.config.EMAIL_CHARSET = 'iso-8859-1'
+        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: [issue1] Testing...
+Content-Type: text/plain;
+        charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+A message with encoding (encoded oe =F6)
+
+''')
+        self.compareMessages(self._get_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="iso-8859-1"
+Subject: [issue1] Testing...
+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
+Message-Id: <followup_dummy_id>
+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.test> added the comment:
+
+A message with encoding (encoded oe =F6)
+
+----------
+status: unread -> chatting
+
+_______________________________________________________________________
+Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
+<http://tracker.example/cgi-bin/roundup.cgi/bugs/issue1>
+_______________________________________________________________________
+''')
+
 
     def testMultipartEnc01(self):
         self.doNewIssue()
@@ -1126,7 +1207,7 @@
         self.compareMessages(self._get_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
+Content-Type: text/plain; charset="utf-8"
 Subject: [issue1] Testing...
 To: chef at bork.bork.bork, richard at test.test
 From: "Contrary, Mary" <issue_tracker at your.tracker.email.domain.example>
@@ -1203,7 +1284,7 @@
         self.compareMessages(self._get_mail(),
 '''FROM: roundup-admin at your.tracker.email.domain.example
 TO: chef at bork.bork.bork
-Content-Type: text/plain; charset=utf-8
+Content-Type: text/plain; charset="utf-8"
 Subject: [issue1] Testing...
 To: chef at bork.bork.bork
 From: richard <issue_tracker at your.tracker.email.domain.example>

Modified: tracker/roundup-src/test/test_mailsplit.py
==============================================================================
--- tracker/roundup-src/test/test_mailsplit.py	(original)
+++ tracker/roundup-src/test/test_mailsplit.py	Sun Mar 15 22:43:30 2009
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: test_mailsplit.py,v 1.15 2003/10/25 22:53:26 richard Exp $
+# $Id: test_mailsplit.py,v 1.15 2003-10-25 22:53:26 richard Exp $
 
 import unittest, cStringIO
 

Deleted: tracker/roundup-src/test/test_metakit.py
==============================================================================

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 15 22:43:30 2009
@@ -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.8 2007/09/22 07:25:35 jpend Exp $ 
+# $Id: test_multipart.py,v 1.8 2007-09-22 07:25:35 jpend Exp $ 
 
 import unittest
 from cStringIO import StringIO

Modified: tracker/roundup-src/test/test_mysql.py
==============================================================================
--- tracker/roundup-src/test/test_mysql.py	(original)
+++ tracker/roundup-src/test/test_mysql.py	Sun Mar 15 22:43:30 2009
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-# $Id: test_mysql.py,v 1.15 2004/11/10 22:22:59 richard Exp $
+# $Id: test_mysql.py,v 1.15 2004-11-10 22:22:59 richard Exp $
 
 import unittest, os, shutil, time, imp
 

Modified: tracker/roundup-src/test/test_postgresql.py
==============================================================================
--- tracker/roundup-src/test/test_postgresql.py	(original)
+++ tracker/roundup-src/test/test_postgresql.py	Sun Mar 15 22:43:30 2009
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-# $Id: test_postgresql.py,v 1.13 2006/08/23 12:57:10 schlatterbeck Exp $
+# $Id: test_postgresql.py,v 1.13 2006-08-23 12:57:10 schlatterbeck Exp $
 
 import unittest
 

Modified: tracker/roundup-src/test/test_schema.py
==============================================================================
--- tracker/roundup-src/test/test_schema.py	(original)
+++ tracker/roundup-src/test/test_schema.py	Sun Mar 15 22:43:30 2009
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-# $Id: test_schema.py,v 1.15 2004/10/16 12:43:11 a1s Exp $
+# $Id: test_schema.py,v 1.15 2004-10-16 12:43:11 a1s Exp $
 
 import unittest, os, shutil
 

Modified: tracker/roundup-src/test/test_security.py
==============================================================================
--- tracker/roundup-src/test/test_security.py	(original)
+++ tracker/roundup-src/test/test_security.py	Sun Mar 15 22:43:30 2009
@@ -18,7 +18,7 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
-# $Id: test_security.py,v 1.10 2006/02/03 04:04:37 richard Exp $
+# $Id: test_security.py,v 1.10 2006-02-03 04:04:37 richard Exp $
 
 import os, unittest, shutil
 

Modified: tracker/roundup-src/test/test_sqlite.py
==============================================================================
--- tracker/roundup-src/test/test_sqlite.py	(original)
+++ tracker/roundup-src/test/test_sqlite.py	Sun Mar 15 22:43:30 2009
@@ -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: test_sqlite.py,v 1.5 2004/11/03 01:34:21 richard Exp $ 
+#
+# $Id: test_sqlite.py,v 1.6 2008-09-01 00:43:02 richard Exp $
 
 import unittest, os, shutil, time
 from roundup.backends import get_backend, have_backend

Modified: tracker/roundup-src/test/test_templating.py
==============================================================================
--- tracker/roundup-src/test/test_templating.py	(original)
+++ tracker/roundup-src/test/test_templating.py	Sun Mar 15 22:43:30 2009
@@ -12,7 +12,8 @@
     def setUp(self):
         self.form = FieldStorage()
         self.client = MockNull()
-        self.client.db = MockDatabase()
+        self.client.db = db = MockDatabase()
+        db.security.hasPermission = lambda *args, **kw: True
         self.client.form = self.form
 
 class HTMLDatabaseTestCase(TemplatingTestCase):
@@ -69,6 +70,87 @@
         self.assertEqual(lookupKeys(shrubbery, 'spam', ['ok','2']), ['ok',
             'eggs'])
 
+class HTMLClassTestCase(TemplatingTestCase) :
+
+    def test_link(self):
+        """Make sure lookup of a Link property works even in the
+        presence of multiple values in the form."""
+        def lookup(key) :
+            self.assertEqual(key, key.strip())
+            return "Status%s"%key
+        self.form.list.append(MiniFieldStorage("status", "1"))
+        self.form.list.append(MiniFieldStorage("status", "2"))
+        status = hyperdb.Link("status")
+        self.client.db.classes = dict \
+            ( issue = MockNull(getprops = lambda : dict(status = status))
+            , status  = MockNull(get = lambda id, name : id, lookup = lookup)
+            )
+        cls = HTMLClass(self.client, "issue")
+        cls["status"]
+
+    def test_multilink(self):
+        """`lookup` of an item will fail if leading or trailing whitespace
+           has not been stripped.
+        """
+        def lookup(key) :
+            self.assertEqual(key, key.strip())
+            return "User%s"%key
+        self.form.list.append(MiniFieldStorage("nosy", "1, 2"))
+        nosy = hyperdb.Multilink("user")
+        self.client.db.classes = dict \
+            ( issue = MockNull(getprops = lambda : dict(nosy = nosy))
+            , user  = MockNull(get = lambda id, name : id, lookup = lookup)
+            )
+        cls = HTMLClass(self.client, "issue")
+        cls["nosy"]
+
+    def test_url_match(self):
+        '''Test the URL regular expression in StringHTMLProperty.
+        '''
+        def t(s, nothing=False, **groups):
+            m = StringHTMLProperty.hyper_re.search(s)
+            if nothing:
+                if m:
+                    self.assertEquals(m, None, '%r matched (%r)'%(s, m.groupdict()))
+                return
+            else:
+                self.assertNotEquals(m, None, '%r did not match'%s)
+            d = m.groupdict()
+            for g in groups:
+                self.assertEquals(d[g], groups[g], '%s %r != %r in %r'%(g, d[g],
+                    groups[g], s))
+
+        #t('123.321.123.321', 'url')
+        t('http://localhost/', url='http://localhost/')
+        t('http://roundup.net/', url='http://roundup.net/')
+        t('http://richard@localhost/', url='http://richard@localhost/')
+        t('http://richard:sekrit@localhost/',
+            url='http://richard:sekrit@localhost/')
+        t('<HTTP://roundup.net/>', url='HTTP://roundup.net/')
+        t('www.a.ex', url='www.a.ex')
+        t('foo.a.ex', nothing=True)
+        t('StDevValidTimeSeries.GetObservation', nothing=True)
+        t('http://a.ex', url='http://a.ex')
+        t('http://a.ex/?foo&bar=baz\\.@!$%()qwerty',
+            url='http://a.ex/?foo&bar=baz\\.@!$%()qwerty')
+        t('www.foo.net', url='www.foo.net')
+        t('richard at com.example', email='richard at com.example')
+        t('r at a.com', email='r at a.com')
+        t('i1', **{'class':'i', 'id':'1'})
+        t('item123', **{'class':'item', 'id':'123'})
+        t('www.user:pass at host.net', email='pass at host.net')
+        t('user:pass at www.host.net', url='user:pass at www.host.net')
+        t('123.35', nothing=True)
+        t('-.3535', nothing=True)
+
+    def test_url_replace(self):
+        p = StringHTMLProperty(self.client, 'test', '1', None, 'test', '')
+        def t(s): return p.hyper_re.sub(p._hyper_repl, s)
+        ae = self.assertEquals
+        ae(t('http://roundup.net/'), '<a href="http://roundup.net/">http://roundup.net/</a>')
+        ae(t('&lt;HTTP://roundup.net/&gt;'), '&lt;<a href="HTTP://roundup.net/">HTTP://roundup.net/</a>&gt;')
+        ae(t('&lt;www.roundup.net&gt;'), '&lt;<a href="http://www.roundup.net">www.roundup.net</a>&gt;')
+
 '''
 class HTMLPermissions:
     def is_edit_ok(self):
@@ -243,6 +325,7 @@
     suite = unittest.TestSuite()
     suite.addTest(unittest.makeSuite(HTMLDatabaseTestCase))
     suite.addTest(unittest.makeSuite(FunctionsTestCase))
+    suite.addTest(unittest.makeSuite(HTMLClassTestCase))
     return suite
 
 if __name__ == '__main__':

Modified: tracker/roundup-src/test/test_token.py
==============================================================================
--- tracker/roundup-src/test/test_token.py	(original)
+++ tracker/roundup-src/test/test_token.py	Sun Mar 15 22:43:30 2009
@@ -8,7 +8,7 @@
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 #
-# $Id: test_token.py,v 1.3 2003/10/25 22:53:26 richard Exp $
+# $Id: test_token.py,v 1.3 2003-10-25 22:53:26 richard Exp $
 
 import unittest, time
 

Modified: tracker/roundup-src/test/test_tsearch2.py
==============================================================================
--- tracker/roundup-src/test/test_tsearch2.py	(original)
+++ tracker/roundup-src/test/test_tsearch2.py	Sun Mar 15 22:43:30 2009
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-# $Id: test_tsearch2.py,v 1.1 2004/12/16 22:22:55 jlgijsbers Exp $
+# $Id: test_tsearch2.py,v 1.1 2004-12-16 22:22:55 jlgijsbers Exp $
 
 import unittest
 

Added: tracker/roundup-src/test/test_userauditor.py
==============================================================================
--- (empty file)
+++ tracker/roundup-src/test/test_userauditor.py	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,112 @@
+# $Id$
+
+import os, unittest, shutil
+from db_test_base import setupTracker
+
+class UserAuditorTest(unittest.TestCase):
+    def setUp(self):
+        self.dirname = '_test_user_auditor'
+        self.instance = setupTracker(self.dirname)
+        self.db = self.instance.open('admin')
+
+        try:
+            import pytz
+            self.pytz = True
+        except ImportError:
+            self.pytz = False
+
+        self.db.user.create(username='kyle', address='kyle at example.com',
+            realname='Kyle Broflovski', roles='User')
+
+    def tearDown(self):
+        self.db.close()
+        try:
+            shutil.rmtree(self.dirname)
+        except OSError, error:
+            if error.errno not in (errno.ENOENT, errno.ESRCH): raise
+
+    def testBadTimezones(self):
+        self.assertRaises(ValueError, self.db.user.create, username='eric', timezone='24')
+
+        userid = self.db.user.lookup('kyle')
+
+        self.assertRaises(ValueError, self.db.user.set, userid, timezone='3000')
+        self.assertRaises(ValueError, self.db.user.set, userid, timezone='24')
+        self.assertRaises(ValueError, self.db.user.set, userid, timezone='-24')
+        self.assertRaises(ValueError, self.db.user.set, userid, timezone='-3000')
+
+        if self.pytz:
+            try:
+                from pytz import UnknownTimeZoneError
+            except:
+                UnknownTimeZoneError = ValueError
+            self.assertRaises(UnknownTimeZoneError, self.db.user.set, userid, timezone='MiddleOf/Nowhere')
+
+    def testGoodTimezones(self):
+        self.db.user.create(username='test_user01', timezone='12')
+
+        if self.pytz:
+            self.db.user.create(username='test_user02', timezone='MST')
+
+        userid = self.db.user.lookup('kyle')
+
+        # TODO: roundup should accept non-integer offsets since those are valid
+        # this is the offset for Tehran, Iran
+        #self.db.user.set(userid, timezone='3.5')
+
+        self.db.user.set(userid, timezone='-23')
+        self.db.user.set(userid, timezone='23')
+        self.db.user.set(userid, timezone='0')
+
+        if self.pytz:
+            self.db.user.set(userid, timezone='US/Eastern')
+
+    def testBadEmailAddresses(self):
+        userid = self.db.user.lookup('kyle')
+        self.assertRaises(ValueError, self.db.user.set, userid, address='kyle @ example.com')
+        self.assertRaises(ValueError, self.db.user.set, userid, address='one at example.com,two at example.com')
+        self.assertRaises(ValueError, self.db.user.set, userid, address='weird@@example.com')
+        self.assertRaises(ValueError, self.db.user.set, userid, address='embedded\nnewline at example.com')
+        # verify that we check alternates as well
+        self.assertRaises(ValueError, self.db.user.set, userid, alternate_addresses='kyle @ example.com')
+        # make sure we accept local style addresses
+        self.db.user.set(userid, address='kyle')
+        # verify we are case insensitive
+        self.db.user.set(userid, address='kyle at EXAMPLE.COM')
+
+    def testUniqueEmailAddresses(self):
+        self.db.user.create(username='kenny', address='kenny at example.com', alternate_addresses='sp_ken at example.com')
+        self.assertRaises(ValueError, self.db.user.create, username='test_user01', address='kenny at example.com')
+        uid = self.db.user.create(username='eric', address='eric at example.com')
+        self.assertRaises(ValueError, self.db.user.set, uid, address='kenny at example.com')
+
+        # make sure we check alternates
+        self.assertRaises(ValueError, self.db.user.set, uid, address='kenny at example.com')
+        self.assertRaises(ValueError, self.db.user.set, uid, address='sp_ken at example.com')
+        self.assertRaises(ValueError, self.db.user.set, uid, alternate_addresses='kenny at example.com')
+
+    def testBadRoles(self):
+        userid = self.db.user.lookup('kyle')
+        self.assertRaises(ValueError, self.db.user.set, userid, roles='BadRole')
+        self.assertRaises(ValueError, self.db.user.set, userid, roles='User,BadRole')
+
+    def testGoodRoles(self):
+        userid = self.db.user.lookup('kyle')
+        # make sure we handle commas in weird places
+        self.db.user.set(userid, roles='User,')
+        self.db.user.set(userid, roles=',User')
+        # make sure we strip whitespace
+        self.db.user.set(userid, roles='    User   ')
+        # check for all-whitespace (treat as no role)
+        self.db.user.set(userid, roles='   ')
+
+def test_suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(UserAuditorTest))
+    return suite
+
+if __name__ == '__main__':
+    runner = unittest.TextTestRunner()
+    unittest.main(testRunner=runner)
+
+# vim: filetype=python sts=4 sw=4 et si

Added: tracker/roundup-src/test/test_xmlrpc.py
==============================================================================
--- (empty file)
+++ tracker/roundup-src/test/test_xmlrpc.py	Sun Mar 15 22:43:30 2009
@@ -0,0 +1,126 @@
+#
+# Copyright (C) 2007 Stefan Seefeld
+# All rights reserved.
+# For license terms see the file COPYING.txt.
+#
+
+import unittest, os, shutil, errno, sys, difflib, cgi, re
+
+from roundup.cgi.exceptions import *
+from roundup import init, instance, password, hyperdb, date
+from roundup.xmlrpc import RoundupInstance
+from roundup.backends import list_backends
+
+import db_test_base
+
+NEEDS_INSTANCE = 1
+
+class TestCase(unittest.TestCase):
+
+    backend = None
+
+    def setUp(self):
+        self.dirname = '_test_xmlrpc'
+        # set up and open a tracker
+        self.instance = db_test_base.setupTracker(self.dirname, self.backend)
+
+        # open the database
+        self.db = self.instance.open('admin')
+        self.joeid = 'user' + self.db.user.create(username='joe',
+            password=password.Password('random'), address='random at home.org',
+            realname='Joe Random', roles='User')
+
+        self.db.commit()
+        self.db.close()
+        self.db = self.instance.open('joe')
+        self.server = RoundupInstance(self.db, self.instance.actions, None)
+
+    def tearDown(self):
+        self.db.close()
+        try:
+            shutil.rmtree(self.dirname)
+        except OSError, error:
+            if error.errno not in (errno.ENOENT, errno.ESRCH): raise
+
+    def testAccess(self):
+        # Retrieve all three users.
+        results = self.server.list('user', 'id')
+        self.assertEqual(len(results), 3)
+
+        # Obtain data for 'joe'.
+        results = self.server.display(self.joeid)
+        self.assertEqual(results['username'], 'joe')
+        self.assertEqual(results['realname'], 'Joe Random')
+
+    def testChange(self):
+        # Reset joe's 'realname'.
+        results = self.server.set(self.joeid, 'realname=Joe Doe')
+        results = self.server.display(self.joeid, 'realname')
+        self.assertEqual(results['realname'], 'Joe Doe')
+
+        # check we can't change admin's details
+        self.assertRaises(Unauthorised, self.server.set, 'user1', 'realname=Joe Doe')
+
+    def testCreate(self):
+        results = self.server.create('issue', 'title=foo')
+        issueid = 'issue' + results
+        results = self.server.display(issueid, 'title')
+        self.assertEqual(results['title'], 'foo')
+
+    def testFileCreate(self):
+        results = self.server.create('file', 'content=hello\r\nthere')
+        fileid = 'file' + results
+        results = self.server.display(fileid, 'content')
+        self.assertEqual(results['content'], 'hello\r\nthere')
+
+    def testAction(self):
+        # As this action requires special previledges, we temporarily switch
+        # to 'admin'
+        self.db.setCurrentUser('admin')
+        users_before = self.server.list('user')
+        try:
+            tmp = 'user' + self.db.user.create(username='tmp')
+            self.server.action('retire', tmp)
+        finally:
+            self.db.setCurrentUser('joe')
+        users_after = self.server.list('user')
+        self.assertEqual(users_before, users_after)
+
+    def testAuthDeniedEdit(self):
+        # Wrong permissions (caught by roundup security module).
+        self.assertRaises(Unauthorised, self.server.set,
+                          'user1', 'realname=someone')
+
+    def testAuthDeniedCreate(self):
+        self.assertRaises(Unauthorised, self.server.create,
+                          'user', {'username': 'blah'})
+
+    def testAuthAllowedEdit(self):
+        self.db.setCurrentUser('admin')
+        try:
+            self.server.set('user2', 'realname=someone')
+        except Unauthorised, err:
+            self.fail('raised %s'%err)
+        finally:
+            self.db.setCurrentUser('joe')
+
+    def testAuthAllowedCreate(self):
+        self.db.setCurrentUser('admin')
+        try:
+            self.server.create('user', 'username=blah')
+        except Unauthorised, err:
+            self.fail('raised %s'%err)
+        finally:
+            self.db.setCurrentUser('joe')
+
+def test_suite():
+    suite = unittest.TestSuite()
+    for l in list_backends():
+        dct = dict(backend = l)
+        subcls = type(TestCase)('TestCase_%s'%l, (TestCase,), dct)
+        suite.addTest(unittest.makeSuite(subcls))
+    return suite
+
+if __name__ == '__main__':
+    runner = unittest.TextTestRunner()
+    unittest.main(testRunner=runner)

Modified: tracker/roundup-src/tools/load_tracker.py
==============================================================================
--- tracker/roundup-src/tools/load_tracker.py	(original)
+++ tracker/roundup-src/tools/load_tracker.py	Sun Mar 15 22:43:30 2009
@@ -1,5 +1,5 @@
 #! /usr/bin/env python
-# $Id: load_tracker.py,v 1.6 2005/06/08 02:24:06 anthonybaxter Exp $
+# $Id: load_tracker.py,v 1.6 2005-06-08 02:24:06 anthonybaxter Exp $
 
 '''
 Usage: %s <tracker home> <N>

Modified: tracker/roundup-src/tools/pygettext.py
==============================================================================
--- tracker/roundup-src/tools/pygettext.py	(original)
+++ tracker/roundup-src/tools/pygettext.py	Sun Mar 15 22:43:30 2009
@@ -1,10 +1,11 @@
-#! /usr/bin/env python
+#! /usr/bin/env python2.5
+# -*- coding: iso-8859-1 -*-
 # Originally written by Barry Warsaw <barry at zope.com>
 #
-# Minimally patched to make it even more xgettext compatible 
+# Minimally patched to make it even more xgettext compatible
 # by Peter Funk <pf at artcom-gmbh.de>
 #
-# 2001-12-18 Jürgen Hermann <jh at web.de>
+# 2002-11-22 Jürgen Hermann <jh at web.de>
 # Added checks that _() only contains string literals, and
 # command line args are resolved to module lists, i.e. you
 # can now pass a filename, a module or package name, or a
@@ -24,17 +25,17 @@
 Many systems (Solaris, Linux, Gnu) provide extensive tools that ease the
 internationalization of C programs. Most of these tools are independent of
 the programming language and can be used from within Python programs.
-Martin von Loewis' work[1] helps considerably in this regard. 
+Martin von Loewis' work[1] helps considerably in this regard.
 
 There's one problem though; xgettext is the program that scans source code
 looking for message strings, but it groks only C (or C++). Python
 introduces a few wrinkles, such as dual quoting characters, triple quoted
-strings, and raw strings. xgettext understands none of this. 
+strings, and raw strings. xgettext understands none of this.
 
 Enter pygettext, which uses Python's standard tokenize module to scan
 Python source code, generating .pot files identical to what GNU xgettext[2]
 generates for C and C++ code. From there, the standard GNU tools can be
-used. 
+used.
 
 A word about marking Python strings as candidates for translation. GNU
 xgettext recognizes the following keywords: gettext, dgettext, dcgettext,
@@ -42,7 +43,7 @@
 code. C and C++ have a trick: they use the C preprocessor. Most
 internationalized C source includes a #define for gettext() to _() so that
 what has to be written in the source is much less. Thus these are both
-translatable strings: 
+translatable strings:
 
     gettext("Translatable String")
     _("Translatable String")
@@ -58,7 +59,7 @@
 xgettext where ever possible. However some options are still missing or are
 not fully implemented. Also, xgettext's use of command line switches with
 option arguments is broken, and in these cases, pygettext just defines
-additional switches. 
+additional switches.
 
 Usage: pygettext [options] inputfile ...
 
@@ -155,7 +156,9 @@
 """)
 
 import os
+import imp
 import sys
+import glob
 import time
 import getopt
 import token
@@ -255,19 +258,17 @@
 
 
 def containsAny(str, set):
-    """ Check whether 'str' contains ANY of the chars in 'set'
-    """
+    """Check whether 'str' contains ANY of the chars in 'set'"""
     return 1 in [c in str for c in set]
 
 
 def _visit_pyfiles(list, dirname, names):
-    """ Helper for getFilesForName().
-    """
+    """Helper for getFilesForName()."""
     # get extension for python source files
     if not globals().has_key('_py_ext'):
-        import imp
         global _py_ext
-        _py_ext = [triple[0] for triple in imp.get_suffixes() if triple[2] == imp.PY_SOURCE][0]
+        _py_ext = [triple[0] for triple in imp.get_suffixes()
+                   if triple[2] == imp.PY_SOURCE][0]
 
     # don't recurse into CVS directories
     if 'CVS' in names:
@@ -275,20 +276,18 @@
 
     # add all *.py files to list
     list.extend(
-        [os.path.join(dirname, file)
-            for file in names
-                if os.path.splitext(file)[1] == _py_ext])
+        [os.path.join(dirname, file) for file in names
+         if os.path.splitext(file)[1] == _py_ext]
+        )
 
 
 def _get_modpkg_path(dotted_name, pathlist=None):
-    """ Get the filesystem path for a module or a package.
+    """Get the filesystem path for a module or a package.
 
-        Return the file system path to a file for a module,
-        and to a directory for a package. Return None if
-        the name is not found, or is a builtin or extension module.
+    Return the file system path to a file for a module, and to a directory for
+    a package. Return None if the name is not found, or is a builtin or
+    extension module.
     """
-    import imp
-
     # split off top-most name
     parts = dotted_name.split('.', 1)
 
@@ -309,8 +308,10 @@
     else:
         # plain name
         try:
-            file, pathname, description = imp.find_module(dotted_name, pathlist)
-            if file: file.close()
+            file, pathname, description = imp.find_module(
+                dotted_name, pathlist)
+            if file:
+                file.close()
             if description[2] not in [imp.PY_SOURCE, imp.PKG_DIRECTORY]:
                 pathname = None
         except ImportError:
@@ -320,15 +321,12 @@
 
 
 def getFilesForName(name):
-    """ Get a list of module files for a filename, a module or package name,
-        or a directory.
+    """Get a list of module files for a filename, a module or package name,
+    or a directory.
     """
-    import imp
-
     if not os.path.exists(name):
         # check for glob chars
         if containsAny(name, "*?[]"):
-            import glob
             files = glob.glob(name)
             list = []
             for file in files:
@@ -414,7 +412,7 @@
     def __openseen(self, ttype, tstring, lineno):
         if ttype == tokenize.OP and tstring == ')':
             # We've seen the last of the translatable strings.  Record the
-            # line number of the first line of the strings and update the list 
+            # line number of the first line of the strings and update the list
             # of messages seen.  Reset state for the next batch.  If there
             # were no strings inside _(), then just ignore this entry.
             if self.__data:
@@ -425,8 +423,13 @@
         elif ttype not in [tokenize.COMMENT, token.INDENT, token.DEDENT,
                            token.NEWLINE, tokenize.NL]:
             # warn if we see anything else than STRING or whitespace
-            print >>sys.stderr, _('*** %(file)s:%(lineno)s: Seen unexpected token "%(token)s"') % {
-                'token': tstring, 'file': self.__curfile, 'lineno': self.__lineno}
+            print >> sys.stderr, _(
+                '*** %(file)s:%(lineno)s: Seen unexpected token "%(token)s"'
+                ) % {
+                'token': tstring,
+                'file': self.__curfile,
+                'lineno': self.__lineno
+                }
             self.__state = self.__waiting
 
     def __addentry(self, msg, lineno=None, isdocstring=0):
@@ -442,7 +445,7 @@
 
     def write(self, fp):
         options = self.__options
-        timestamp = time.ctime(time.time())
+        timestamp = time.strftime('%Y-%m-%d %H:%M+%Z')
         # The time stamp in the header doesn't have the same format as that
         # generated by xgettext...
         print >> fp, pot_header % {'time': timestamp, 'version': __version__}
@@ -661,6 +664,6 @@
     main()
     # some more test strings
     _(u'a unicode string')
-    _('*** Seen unexpected token "%(token)s"' % {'token': 'test'}) # this one creates a warning
+    # this one creates a warning
+    _('*** Seen unexpected token "%(token)s"') % {'token': 'test'}
     _('more' 'than' 'one' 'string')
-


More information about the Python-checkins mailing list